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`).
pull/12900/head
Riccardo Mazzarini 2026-06-02 17:57:41 +02:00
parent 6246c288ae
commit 2444e4d557
No known key found for this signature in database
GPG Key ID: 38165222613796F5
3 changed files with 74 additions and 2 deletions

View File

@ -232,6 +232,26 @@ typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_TERMINAL_SCREEN_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, GHOSTTY_TERMINAL_SCREEN_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalScreen; } 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. * Scrollbar state for the terminal viewport.
* *
@ -608,6 +628,15 @@ typedef enum GHOSTTY_ENUM_TYPED {
* Input type: GhosttySelection* * Input type: GhosttySelection*
*/ */
GHOSTTY_TERMINAL_OPT_SELECTION = 21, 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, GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttyTerminalOption; } GhosttyTerminalOption;

View File

@ -5,6 +5,7 @@ const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator; const CAllocator = lib.alloc.Allocator;
pub const ZigTerminal = @import("../Terminal.zig"); pub const ZigTerminal = @import("../Terminal.zig");
const Stream = @import("../stream_terminal.zig").Stream; const Stream = @import("../stream_terminal.zig").Stream;
const Screen = @import("../Screen.zig");
const ScreenSet = @import("../ScreenSet.zig"); const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig"); const PageList = @import("../PageList.zig");
const apc = @import("../apc.zig"); const apc = @import("../apc.zig");
@ -326,6 +327,7 @@ pub const Option = enum(c_int) {
apc_max_bytes = 19, apc_max_bytes = 19,
apc_max_bytes_kitty = 20, apc_max_bytes_kitty = 20,
selection = 21, selection = 21,
default_cursor_style = 22,
/// Input type expected for setting the option. /// Input type expected for setting the option.
pub fn InType(comptime self: Option) type { pub fn InType(comptime self: Option) type {
@ -349,6 +351,7 @@ pub const Option = enum(c_int) {
=> ?*const bool, => ?*const bool,
.apc_max_bytes, .apc_max_bytes_kitty => ?*const usize, .apc_max_bytes, .apc_max_bytes_kitty => ?*const usize,
.selection => ?*const selection_c.CSelection, .selection => ?*const selection_c.CSelection,
.default_cursor_style => ?*const TerminalCursorStyle,
}; };
} }
}; };
@ -464,10 +467,36 @@ fn setTyped(
wrapper.terminal.screens.active.clearSelection(); 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; 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 /// C: GhosttyDeviceAttributes
pub const DeviceAttributes = Effects.CDeviceAttributes; pub const DeviceAttributes = Effects.CDeviceAttributes;

View File

@ -43,6 +43,10 @@ pub const Handler = struct {
/// the kitty graphics protocol. /// the kitty graphics protocol.
apc_handler: apc.Handler = .{}, 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 { pub const Effects = struct {
/// Called when the terminal needs to write data back to the pty, /// 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 /// 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, self.terminal.screens.active.cursor.x + 1,
), ),
.cursor_style => { .cursor_style => {
self.default_cursor = false;
const blink = switch (value) { const blink = switch (value) {
.default, .steady_block, .steady_bar, .steady_underline => false, .default, .steady_block, .steady_bar, .steady_underline => false,
.blinking_block, .blinking_bar, .blinking_underline => true, .blinking_block, .blinking_bar, .blinking_underline => true,
}; };
const style: Screen.CursorStyle = switch (value) { 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_bar, .steady_bar => .bar,
.blinking_underline, .steady_underline => .underline, .blinking_underline, .steady_underline => .underline,
}; };
@ -228,7 +238,11 @@ pub const Handler = struct {
}, },
.active_status_display => self.terminal.status_display = value, .active_status_display => self.terminal.status_display = value,
.decaln => try self.terminal.decaln(), .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), .start_hyperlink => try self.terminal.screens.active.startHyperlink(value.uri, value.id),
.end_hyperlink => self.terminal.screens.active.endHyperlink(), .end_hyperlink => self.terminal.screens.active.endHyperlink(),
.semantic_prompt => try self.terminal.semanticPrompt(value), .semantic_prompt => try self.terminal.semanticPrompt(value),