From 2444e4d557e47ecb1a63d8198b7f28f3a94fa8f8 Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Tue, 2 Jun 2026 17:57:41 +0200 Subject: [PATCH 1/3] libghostty: add option to set default cursor style Adds an option to `libghostty-vt` to configure the default cursor style that should be displayed when an app sends a DECSCUSR reset sequence (`CSI 0 q`). --- include/ghostty/vt/terminal.h | 29 +++++++++++++++++++++++++++++ src/terminal/c/terminal.zig | 29 +++++++++++++++++++++++++++++ src/terminal/stream_terminal.zig | 18 ++++++++++++++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index ddfcb9c0d..9ddc2b54b 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -232,6 +232,26 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_TERMINAL_SCREEN_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalScreen; +/** + * Visual style of the terminal cursor. + * + * @ingroup terminal + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Bar cursor (DECSCUSR 5, 6). */ + GHOSTTY_TERMINAL_CURSOR_STYLE_BAR = 0, + + /** Block cursor (DECSCUSR 1, 2). */ + GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK = 1, + + /** Underline cursor (DECSCUSR 3, 4). */ + GHOSTTY_TERMINAL_CURSOR_STYLE_UNDERLINE = 2, + + /** Hollow block cursor. */ + GHOSTTY_TERMINAL_CURSOR_STYLE_BLOCK_HOLLOW = 3, + GHOSTTY_TERMINAL_CURSOR_STYLE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttyTerminalCursorStyle; + /** * Scrollbar state for the terminal viewport. * @@ -608,6 +628,15 @@ typedef enum GHOSTTY_ENUM_TYPED { * Input type: GhosttySelection* */ GHOSTTY_TERMINAL_OPT_SELECTION = 21, + + /** + * Set the default cursor style used by DECSCUSR reset (CSI 0 q). + * + * A NULL value pointer resets to the built-in default block cursor. + * + * Input type: GhosttyTerminalCursorStyle* + */ + GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_STYLE = 22, GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalOption; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 5a5db2d6b..59fac7e70 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -5,6 +5,7 @@ const lib = @import("../lib.zig"); const CAllocator = lib.alloc.Allocator; pub const ZigTerminal = @import("../Terminal.zig"); const Stream = @import("../stream_terminal.zig").Stream; +const Screen = @import("../Screen.zig"); const ScreenSet = @import("../ScreenSet.zig"); const PageList = @import("../PageList.zig"); const apc = @import("../apc.zig"); @@ -326,6 +327,7 @@ pub const Option = enum(c_int) { apc_max_bytes = 19, apc_max_bytes_kitty = 20, selection = 21, + default_cursor_style = 22, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -349,6 +351,7 @@ pub const Option = enum(c_int) { => ?*const bool, .apc_max_bytes, .apc_max_bytes_kitty => ?*const usize, .selection => ?*const selection_c.CSelection, + .default_cursor_style => ?*const TerminalCursorStyle, }; } }; @@ -464,10 +467,36 @@ fn setTyped( wrapper.terminal.screens.active.clearSelection(); } }, + .default_cursor_style => { + const style = (if (value) |ptr| ptr.* else TerminalCursorStyle.block).toZig() orelse return .invalid_value; + wrapper.stream.handler.default_cursor_style = style; + if (wrapper.stream.handler.default_cursor) { + wrapper.terminal.screens.active.cursor.cursor_style = style; + } + }, } return .success; } +/// C: GhosttyTerminalCursorStyle +pub const TerminalCursorStyle = enum(c_int) { + bar = 0, + block = 1, + underline = 2, + block_hollow = 3, + _, + + fn toZig(self: TerminalCursorStyle) ?Screen.CursorStyle { + return switch (self) { + .bar => .bar, + .block => .block, + .underline => .underline, + .block_hollow => .block_hollow, + _ => null, + }; + } +}; + /// C: GhosttyDeviceAttributes pub const DeviceAttributes = Effects.CDeviceAttributes; diff --git a/src/terminal/stream_terminal.zig b/src/terminal/stream_terminal.zig index 51ef63422..560e12c2a 100644 --- a/src/terminal/stream_terminal.zig +++ b/src/terminal/stream_terminal.zig @@ -43,6 +43,10 @@ pub const Handler = struct { /// the kitty graphics protocol. apc_handler: apc.Handler = .{}, + /// Default cursor style used by DECSCUSR reset (CSI 0 q). + default_cursor: bool = true, + default_cursor_style: Screen.CursorStyle = .block, + pub const Effects = struct { /// Called when the terminal needs to write data back to the pty, /// e.g. in response to a DECRQM query. The data is only valid @@ -152,12 +156,18 @@ pub const Handler = struct { self.terminal.screens.active.cursor.x + 1, ), .cursor_style => { + self.default_cursor = false; + const blink = switch (value) { .default, .steady_block, .steady_bar, .steady_underline => false, .blinking_block, .blinking_bar, .blinking_underline => true, }; const style: Screen.CursorStyle = switch (value) { - .default, .blinking_block, .steady_block => .block, + .default => style: { + self.default_cursor = true; + break :style self.default_cursor_style; + }, + .blinking_block, .steady_block => .block, .blinking_bar, .steady_bar => .bar, .blinking_underline, .steady_underline => .underline, }; @@ -228,7 +238,11 @@ pub const Handler = struct { }, .active_status_display => self.terminal.status_display = value, .decaln => try self.terminal.decaln(), - .full_reset => self.terminal.fullReset(), + .full_reset => { + self.terminal.fullReset(); + self.default_cursor = true; + self.terminal.screens.active.cursor.cursor_style = self.default_cursor_style; + }, .start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id), .end_hyperlink => self.terminal.screens.active.endHyperlink(), .semantic_prompt => try self.terminal.semanticPrompt(value), From 66950a4a537d1c21fe63a7c53d3c254bbe83679a Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Tue, 2 Jun 2026 18:03:45 +0200 Subject: [PATCH 2/3] libghostty: add option to set default cursor blink Adds an option to `libghostty-vt` to configure whether the default cursor displayed when an app sends a DECSCUSR reset sequence should blink. --- include/ghostty/vt/terminal.h | 10 ++++++++++ src/terminal/c/terminal.zig | 9 +++++++++ src/terminal/stream_terminal.zig | 5 ++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 9ddc2b54b..9b573c36a 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -637,6 +637,16 @@ typedef enum GHOSTTY_ENUM_TYPED { * Input type: GhosttyTerminalCursorStyle* */ GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_STYLE = 22, + + /** + * Set whether the default cursor should blink when reset by DECSCUSR + * (CSI 0 q). + * + * A NULL value pointer resets to the built-in default of not blinking. + * + * Input type: bool* + */ + GHOSTTY_TERMINAL_OPT_DEFAULT_CURSOR_BLINK = 23, GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalOption; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 59fac7e70..6d5ca4c5c 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -328,6 +328,7 @@ pub const Option = enum(c_int) { apc_max_bytes_kitty = 20, selection = 21, default_cursor_style = 22, + default_cursor_blink = 23, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -352,6 +353,7 @@ pub const Option = enum(c_int) { .apc_max_bytes, .apc_max_bytes_kitty => ?*const usize, .selection => ?*const selection_c.CSelection, .default_cursor_style => ?*const TerminalCursorStyle, + .default_cursor_blink => ?*const bool, }; } }; @@ -474,6 +476,13 @@ fn setTyped( wrapper.terminal.screens.active.cursor.cursor_style = style; } }, + .default_cursor_blink => { + const blink = if (value) |ptr| ptr.* else false; + wrapper.stream.handler.default_cursor_blink = blink; + if (wrapper.stream.handler.default_cursor) { + wrapper.terminal.modes.set(.cursor_blinking, blink); + } + }, } return .success; } diff --git a/src/terminal/stream_terminal.zig b/src/terminal/stream_terminal.zig index 560e12c2a..113521072 100644 --- a/src/terminal/stream_terminal.zig +++ b/src/terminal/stream_terminal.zig @@ -46,6 +46,7 @@ pub const Handler = struct { /// Default cursor style used by DECSCUSR reset (CSI 0 q). default_cursor: bool = true, default_cursor_style: Screen.CursorStyle = .block, + default_cursor_blink: bool = false, pub const Effects = struct { /// Called when the terminal needs to write data back to the pty, @@ -159,7 +160,8 @@ pub const Handler = struct { self.default_cursor = false; const blink = switch (value) { - .default, .steady_block, .steady_bar, .steady_underline => false, + .default => self.default_cursor_blink, + .steady_block, .steady_bar, .steady_underline => false, .blinking_block, .blinking_bar, .blinking_underline => true, }; const style: Screen.CursorStyle = switch (value) { @@ -241,6 +243,7 @@ pub const Handler = struct { .full_reset => { self.terminal.fullReset(); self.default_cursor = true; + self.terminal.modes.set(.cursor_blinking, self.default_cursor_blink); self.terminal.screens.active.cursor.cursor_style = self.default_cursor_style; }, .start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id), From e7b506c69d2ae8f071b29b1deb3c6ff93cf73a8d Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Tue, 2 Jun 2026 18:12:17 +0200 Subject: [PATCH 3/3] Test setting/resetting the default cursor style and blink --- src/terminal/c/terminal.zig | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 6d5ca4c5c..97633d18e 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -1415,6 +1415,45 @@ test "get invalid" { try testing.expectEqual(Result.invalid_value, get(t, .invalid, null)); } +test "set default cursor style and blink" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }, + )); + defer free(t); + + var default_style: TerminalCursorStyle = .bar; + var default_blink = true; + try testing.expectEqual(Result.success, set(t, .default_cursor_style, @ptrCast(&default_style))); + try testing.expectEqual(Result.success, set(t, .default_cursor_blink, @ptrCast(&default_blink))); + + // Setting defaults applies them immediately while the cursor is still default. + try testing.expectEqual(Screen.CursorStyle.bar, t.?.terminal.screens.active.cursor.cursor_style); + try testing.expect(t.?.terminal.modes.get(.cursor_blinking)); + + // An explicit DECSCUSR style overrides the configured defaults. + vt_write(t, "\x1b[2 q", 5); + try testing.expectEqual(Screen.CursorStyle.block, t.?.terminal.screens.active.cursor.cursor_style); + try testing.expect(!t.?.terminal.modes.get(.cursor_blinking)); + + // Changing defaults does not override an explicit cursor style. + default_style = .underline; + try testing.expectEqual(Result.success, set(t, .default_cursor_style, @ptrCast(&default_style))); + try testing.expectEqual(Screen.CursorStyle.block, t.?.terminal.screens.active.cursor.cursor_style); + try testing.expect(!t.?.terminal.modes.get(.cursor_blinking)); + + // DECSCUSR reset restores the configured default style and blink. + vt_write(t, "\x1b[0 q", 5); + try testing.expectEqual(Screen.CursorStyle.underline, t.?.terminal.screens.active.cursor.cursor_style); + try testing.expect(t.?.terminal.modes.get(.cursor_blinking)); +} + test "set and get selection" { var t: Terminal = null; try testing.expectEqual(Result.success, new(