diff --git a/include/ghostty.h b/include/ghostty.h index 5523654c4..efcd2901e 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1134,6 +1134,7 @@ GHOSTTY_API ghostty_app_t ghostty_surface_app(ghostty_surface_t); GHOSTTY_API ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); GHOSTTY_API void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); GHOSTTY_API bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_is_at_prompt(ghostty_surface_t); GHOSTTY_API bool ghostty_surface_process_exited(ghostty_surface_t); GHOSTTY_API void ghostty_surface_refresh(ghostty_surface_t); GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t); diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 31cd5c4f1..57d0e5d03 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -1613,6 +1613,17 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: bool * */ GHOSTTY_TERMINAL_DATA_VT_GROUND = 38, + + /** + * Whether the cursor is currently at a semantic shell prompt or input area. + * + * This depends on semantic prompt markers such as OSC 133. Returns false + * when semantic prompt information is unavailable or the alternate screen + * is active. + * + * Output type: bool * + */ + GHOSTTY_TERMINAL_DATA_CURSOR_AT_PROMPT = 39, GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalData; diff --git a/src/Surface.zig b/src/Surface.zig index 7ef2038b8..be8cbde8f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -967,6 +967,17 @@ pub fn needsConfirmQuit(self: *Surface) bool { }; } +/// True if the surface cursor is currently at a shell prompt according to +/// semantic prompt integration. If shell integration is unavailable, this +/// conservatively returns false. +pub fn isAtPrompt(self: *Surface) bool { + if (self.child_exited) return false; + + self.renderer_state.mutex.lockUncancelable(global.io()); + defer self.renderer_state.mutex.unlock(global.io()); + return self.io.terminal.cursorIsAtPrompt(); +} + /// Called from the app thread to handle mailbox messages to our specific /// surface. pub fn handleMessage(self: *Surface, msg: Message) !void { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a380dc50c..689a3e4dc 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1606,6 +1606,11 @@ pub const CAPI = struct { return surface.core_surface.needsConfirmQuit(); } + /// Returns true if the surface cursor is currently at a semantic prompt. + export fn ghostty_surface_is_at_prompt(surface: *Surface) bool { + return surface.core_surface.isAtPrompt(); + } + /// Returns true if the surface process has exited. export fn ghostty_surface_process_exited(surface: *Surface) bool { return surface.core_surface.child_exited; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 37cd1406b..24c5edb39 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -1355,6 +1355,7 @@ pub const TerminalData = enum(c_int) { continuation_max_bytes = 36, mode = 37, vt_ground = 38, + cursor_at_prompt = 39, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { @@ -1367,6 +1368,7 @@ pub const TerminalData = enum(c_int) { .viewport_active, .vt_processing_error, .vt_ground, + .cursor_at_prompt, => bool, .active_screen => TerminalScreen, .kitty_keyboard_flags => u8, @@ -1533,6 +1535,7 @@ fn getTyped( const mode = out.toMode() orelse return .invalid_value; out.value = t.modes.get(mode); }, + .cursor_at_prompt => out.* = t.cursorIsAtPrompt(), } return .success; @@ -2864,6 +2867,31 @@ test "get cursor_visible" { try testing.expect(!visible); } +test "get cursor_at_prompt" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + 80, + 24, + )); + defer free(t); + + var at_prompt: bool = undefined; + try testing.expectEqual(Result.success, get(t, .cursor_at_prompt, @ptrCast(&at_prompt))); + try testing.expect(!at_prompt); + + const prompt_start = "\x1b]133;A\x07"; + vt_write(t, prompt_start, prompt_start.len); + try testing.expectEqual(Result.success, get(t, .cursor_at_prompt, @ptrCast(&at_prompt))); + try testing.expect(at_prompt); + + const alternate_screen = "\x1b[?1049h"; + vt_write(t, alternate_screen, alternate_screen.len); + try testing.expectEqual(Result.success, get(t, .cursor_at_prompt, @ptrCast(&at_prompt))); + try testing.expect(!at_prompt); +} + test "get active_screen" { var t: Terminal = null; try testing.expectEqual(Result.success, new(