mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 04:39:01 +00:00
terminal: move cursor defaults into terminal state
Cursor defaults were duplicated across stream handlers and it was a pretty significant amount of simple and yet non-trivial logic to understand. Store these on Terminal itself and have methods to route things like DECSCUSR through for consistent behaviors.
This commit is contained in:
@@ -81,6 +81,9 @@ previous_char: ?u21 = null,
|
||||
/// The modes that this terminal currently has active.
|
||||
modes: modespkg.ModeState = .{},
|
||||
|
||||
/// Terminal-level cursor state.
|
||||
cursor: Cursor = .{},
|
||||
|
||||
/// The most recently set mouse shape for the terminal.
|
||||
mouse_shape: mouse.Shape = .text,
|
||||
|
||||
@@ -236,6 +239,19 @@ pub const ScrollingRegion = struct {
|
||||
right: size.CellCountInt,
|
||||
};
|
||||
|
||||
/// Terminal-level cursor state shared by all screens.
|
||||
pub const Cursor = struct {
|
||||
/// Whether the current cursor appearance follows the configured defaults.
|
||||
is_default: bool = true,
|
||||
|
||||
/// Configured style restored by DECSCUSR default and RIS.
|
||||
default_style: Screen.CursorStyle = .block,
|
||||
|
||||
/// Configured blink restored by DECSCUSR default and RIS. Null selects
|
||||
/// the terminal emulator default, which is blinking.
|
||||
default_blink: ?bool = false,
|
||||
};
|
||||
|
||||
pub const Options = struct {
|
||||
cols: size.CellCountInt,
|
||||
rows: size.CellCountInt,
|
||||
@@ -246,6 +262,10 @@ pub const Options = struct {
|
||||
/// will revert back to this state.
|
||||
default_modes: modespkg.ModePacked = .{},
|
||||
|
||||
/// Cursor state restored by DECSCUSR default and RIS.
|
||||
default_cursor_style: Screen.CursorStyle = .block,
|
||||
default_cursor_blink: ?bool = false,
|
||||
|
||||
/// The total storage limit for Kitty images in bytes. Has no effect
|
||||
/// if kitty images are disabled at build-time.
|
||||
kitty_image_storage_limit: usize = switch (build_options.artifact) {
|
||||
@@ -284,7 +304,7 @@ pub fn init(
|
||||
});
|
||||
errdefer screen_set.deinit(alloc);
|
||||
|
||||
return .{
|
||||
var result: Terminal = .{
|
||||
.cols = cols,
|
||||
.rows = rows,
|
||||
.screens = screen_set,
|
||||
@@ -302,7 +322,13 @@ pub fn init(
|
||||
.values = opts.default_modes,
|
||||
.default = opts.default_modes,
|
||||
},
|
||||
.cursor = .{
|
||||
.default_style = opts.default_cursor_style,
|
||||
.default_blink = opts.default_cursor_blink,
|
||||
},
|
||||
};
|
||||
result.setCursorStyle(.default);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Terminal, alloc: Allocator) void {
|
||||
@@ -337,6 +363,63 @@ pub fn vtHandler(self: *Terminal) Stream.Handler {
|
||||
return .init(self);
|
||||
}
|
||||
|
||||
/// Change the cursor's current shape and blink behavior.
|
||||
///
|
||||
/// The terminal parser uses this for DECSCUSR (`CSI Ps SP q`), but the behavior
|
||||
/// is general: `.default` selects the configured defaults, while any other
|
||||
/// value selects a concrete appearance until it is changed again or reset.
|
||||
pub fn setCursorStyle(self: *Terminal, value: ansi.CursorStyle) void {
|
||||
// Remember whether future configuration changes should update the visible
|
||||
// cursor. An explicit appearance must remain in effect until the program
|
||||
// selects the default again.
|
||||
self.cursor.is_default = value == .default;
|
||||
|
||||
// Convert the request into the concrete values used by the renderer and
|
||||
// terminal mode state. A null default blink means the emulator default.
|
||||
self.modes.set(.cursor_blinking, switch (value) {
|
||||
.default => self.cursor.default_blink orelse true,
|
||||
.steady_block, .steady_bar, .steady_underline => false,
|
||||
.blinking_block, .blinking_bar, .blinking_underline => true,
|
||||
});
|
||||
self.screens.active.cursor.cursor_style = switch (value) {
|
||||
.default => self.cursor.default_style,
|
||||
.blinking_block, .steady_block => .block,
|
||||
.blinking_bar, .steady_bar => .bar,
|
||||
.blinking_underline, .steady_underline => .underline,
|
||||
};
|
||||
}
|
||||
|
||||
/// Change the default cursor shape.
|
||||
///
|
||||
/// If the cursor currently follows its defaults, the visible shape changes
|
||||
/// immediately. Otherwise the new shape is saved for the next reset or default
|
||||
/// selection, such as DECSCUSR `CSI 0 SP q`.
|
||||
pub fn setDefaultCursorStyle(
|
||||
self: *Terminal,
|
||||
configured_style: Screen.CursorStyle,
|
||||
) void {
|
||||
// Always retain the new default, even while an explicit appearance is
|
||||
// active, so a later reset or default request can restore it.
|
||||
self.cursor.default_style = configured_style;
|
||||
|
||||
// Do not overwrite an appearance explicitly selected by the program.
|
||||
if (self.cursor.is_default) self.setCursorStyle(.default);
|
||||
}
|
||||
|
||||
/// Change the default cursor blink behavior.
|
||||
///
|
||||
/// Null selects the terminal emulator default (blinking). Like the default
|
||||
/// shape, this is applied immediately only when the cursor currently follows
|
||||
/// its defaults; otherwise it is saved for the next reset or default selection.
|
||||
pub fn setDefaultCursorBlink(self: *Terminal, blink: ?bool) void {
|
||||
// Keep the configured value separate from the currently resolved mode so
|
||||
// null can continue to mean "use the emulator default."
|
||||
self.cursor.default_blink = blink;
|
||||
|
||||
// Do not overwrite blink behavior explicitly selected by the program.
|
||||
if (self.cursor.is_default) self.setCursorStyle(.default);
|
||||
}
|
||||
|
||||
/// The general allocator we should use for this terminal.
|
||||
pub fn gpa(self: *Terminal) Allocator {
|
||||
return self.screens.active.alloc;
|
||||
@@ -4331,6 +4414,7 @@ pub fn fullReset(self: *Terminal) void {
|
||||
.left = 0,
|
||||
.right = self.cols - 1,
|
||||
};
|
||||
self.setCursorStyle(.default);
|
||||
|
||||
// Always mark dirty so we redraw everything
|
||||
self.flags.dirty.clear = true;
|
||||
@@ -14134,6 +14218,64 @@ test "Terminal: cursorIsAtPrompt alternate screen" {
|
||||
try testing.expect(!t.cursorIsAtPrompt());
|
||||
}
|
||||
|
||||
test "Terminal: cursor defaults update current default cursor" {
|
||||
var t = try init(testing.allocator, .{
|
||||
.cols = 10,
|
||||
.rows = 10,
|
||||
.default_cursor_style = .bar,
|
||||
.default_cursor_blink = true,
|
||||
});
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Initialization applies the configured defaults.
|
||||
try testing.expect(t.cursor.is_default);
|
||||
try testing.expectEqual(.bar, t.screens.active.cursor.cursor_style);
|
||||
try testing.expect(t.modes.get(.cursor_blinking));
|
||||
|
||||
// Configuration changes are immediately visible while the cursor still
|
||||
// follows its defaults.
|
||||
t.setDefaultCursorStyle(.underline);
|
||||
t.setDefaultCursorBlink(false);
|
||||
try testing.expect(t.cursor.is_default);
|
||||
try testing.expectEqual(.underline, t.screens.active.cursor.cursor_style);
|
||||
try testing.expect(!t.modes.get(.cursor_blinking));
|
||||
|
||||
// Null restores the terminal emulator's blinking default.
|
||||
t.setDefaultCursorBlink(null);
|
||||
try testing.expect(t.modes.get(.cursor_blinking));
|
||||
}
|
||||
|
||||
test "Terminal: cursor defaults do not override explicit cursor" {
|
||||
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
t.setCursorStyle(.blinking_bar);
|
||||
try testing.expect(!t.cursor.is_default);
|
||||
try testing.expectEqual(.bar, t.screens.active.cursor.cursor_style);
|
||||
try testing.expect(t.modes.get(.cursor_blinking));
|
||||
|
||||
// New defaults are retained without replacing the explicit appearance.
|
||||
t.setDefaultCursorStyle(.underline);
|
||||
t.setDefaultCursorBlink(false);
|
||||
try testing.expectEqual(.underline, t.cursor.default_style);
|
||||
try testing.expectEqual(false, t.cursor.default_blink);
|
||||
try testing.expectEqual(.bar, t.screens.active.cursor.cursor_style);
|
||||
try testing.expect(t.modes.get(.cursor_blinking));
|
||||
|
||||
// Selecting the default applies the values that changed above.
|
||||
t.setCursorStyle(.default);
|
||||
try testing.expect(t.cursor.is_default);
|
||||
try testing.expectEqual(.underline, t.screens.active.cursor.cursor_style);
|
||||
try testing.expect(!t.modes.get(.cursor_blinking));
|
||||
|
||||
// A full reset also leaves the cursor on the configured defaults.
|
||||
t.setCursorStyle(.steady_block);
|
||||
t.fullReset();
|
||||
try testing.expect(t.cursor.is_default);
|
||||
try testing.expectEqual(.underline, t.screens.active.cursor.cursor_style);
|
||||
try testing.expect(!t.modes.get(.cursor_blinking));
|
||||
}
|
||||
|
||||
test "Terminal: fullReset with a non-empty pen" {
|
||||
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
@@ -589,17 +589,11 @@ fn setTyped(
|
||||
},
|
||||
.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;
|
||||
}
|
||||
wrapper.terminal.setDefaultCursorStyle(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);
|
||||
}
|
||||
wrapper.terminal.setDefaultCursorBlink(blink);
|
||||
},
|
||||
}
|
||||
return .success;
|
||||
@@ -1837,6 +1831,12 @@ test "set default cursor 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));
|
||||
|
||||
// RIS also restores cursor defaults from Terminal-owned state.
|
||||
vt_write(t, "\x1b[2 q", 5);
|
||||
reset(t);
|
||||
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" {
|
||||
|
||||
@@ -60,11 +60,6 @@ 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,
|
||||
default_cursor_blink: bool = false,
|
||||
|
||||
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
|
||||
@@ -228,26 +223,7 @@ pub const Handler = struct {
|
||||
self.terminal.screens.active.cursor.y + 1 +| value.value,
|
||||
self.terminal.screens.active.cursor.x + 1,
|
||||
),
|
||||
.cursor_style => {
|
||||
self.default_cursor = false;
|
||||
|
||||
const blink = switch (value) {
|
||||
.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) {
|
||||
.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,
|
||||
};
|
||||
self.terminal.modes.set(.cursor_blinking, blink);
|
||||
self.terminal.screens.active.cursor.cursor_style = style;
|
||||
},
|
||||
.cursor_style => self.terminal.setCursorStyle(value),
|
||||
.erase_display_below => self.terminal.eraseDisplay(.below, value),
|
||||
.erase_display_above => self.terminal.eraseDisplay(.above, value),
|
||||
.erase_display_complete => self.terminal.eraseDisplay(.complete, value),
|
||||
@@ -312,12 +288,7 @@ pub const Handler = struct {
|
||||
},
|
||||
.active_status_display => self.terminal.status_display = value,
|
||||
.decaln => try self.terminal.decaln(),
|
||||
.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;
|
||||
},
|
||||
.full_reset => self.terminal.fullReset(),
|
||||
.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),
|
||||
|
||||
@@ -245,6 +245,8 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
||||
.rows = grid_size.rows,
|
||||
.max_scrollback = opts.full_config.@"scrollback-limit",
|
||||
.default_modes = default_modes,
|
||||
.default_cursor_style = opts.config.cursor_style,
|
||||
.default_cursor_blink = opts.config.cursor_blink,
|
||||
.colors = .{
|
||||
.background = .init(opts.config.background.toTerminalRGB()),
|
||||
.foreground = .init(opts.config.foreground.toTerminalRGB()),
|
||||
@@ -261,9 +263,6 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
||||
});
|
||||
errdefer term.deinit(alloc);
|
||||
|
||||
// Set our default cursor style
|
||||
term.screens.active.cursor.cursor_style = opts.config.cursor_style;
|
||||
|
||||
// Setup our terminal size in pixels for certain requests.
|
||||
term.width_px = term.cols * opts.size.cell.width;
|
||||
term.height_px = term.rows * opts.size.cell.height;
|
||||
@@ -286,8 +285,6 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
||||
.osc_color_report_format = opts.config.osc_color_report_format,
|
||||
.clipboard_write = opts.config.clipboard_write,
|
||||
.enquiry_response = opts.config.enquiry_response,
|
||||
.default_cursor_style = opts.config.cursor_style,
|
||||
.default_cursor_blink = opts.config.cursor_blink,
|
||||
};
|
||||
|
||||
const thread_enter_state = try ThreadEnterState.create(
|
||||
|
||||
@@ -40,12 +40,6 @@ pub const StreamHandler = struct {
|
||||
/// a repaint should happen.
|
||||
renderer_wakeup: xev.Async,
|
||||
|
||||
/// The default cursor state. This is used with CSI q. This is
|
||||
/// set to true when we're currently in the default cursor state.
|
||||
default_cursor: bool = true,
|
||||
default_cursor_style: terminal.CursorStyle,
|
||||
default_cursor_blink: ?bool,
|
||||
|
||||
/// The response to use for ENQ requests. The memory is owned by
|
||||
/// whoever owns StreamHandler.
|
||||
enquiry_response: []const u8,
|
||||
@@ -110,13 +104,8 @@ pub const StreamHandler = struct {
|
||||
self.osc_color_report_format = config.osc_color_report_format;
|
||||
self.clipboard_write = config.clipboard_write;
|
||||
self.enquiry_response = config.enquiry_response;
|
||||
self.default_cursor_style = config.cursor_style;
|
||||
self.default_cursor_blink = config.cursor_blink;
|
||||
|
||||
// If our cursor is the default, then we update it immediately.
|
||||
if (self.default_cursor) self.setCursorStyle(.default) catch |err| {
|
||||
log.warn("failed to set default cursor style: {}", .{err});
|
||||
};
|
||||
self.terminal.setDefaultCursorStyle(config.cursor_style);
|
||||
self.terminal.setDefaultCursorBlink(config.cursor_blink);
|
||||
|
||||
// The config could have changed any of our colors so update mode 2031
|
||||
self.messageWriter(.{ .color_scheme_report = .{ .force = false } });
|
||||
@@ -242,7 +231,7 @@ pub const StreamHandler = struct {
|
||||
self.terminal.screens.active.cursor.y + 1 +| value.value,
|
||||
self.terminal.screens.active.cursor.x + 1,
|
||||
),
|
||||
.cursor_style => try self.setCursorStyle(value),
|
||||
.cursor_style => self.terminal.setCursorStyle(value),
|
||||
.erase_display_below => self.terminal.eraseDisplay(.below, value),
|
||||
.erase_display_above => self.terminal.eraseDisplay(.above, value),
|
||||
.erase_display_complete => {
|
||||
@@ -687,7 +676,7 @@ pub const StreamHandler = struct {
|
||||
// their shell config when they render prompts to ensure the
|
||||
// cursor is exactly as they request.
|
||||
if (mode == .cursor_blinking and
|
||||
self.default_cursor_blink != null)
|
||||
self.terminal.cursor.default_blink != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -894,55 +883,6 @@ pub const StreamHandler = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setCursorStyle(
|
||||
self: *StreamHandler,
|
||||
style: terminal.CursorStyleReq,
|
||||
) !void {
|
||||
// Assume we're setting to a non-default.
|
||||
self.default_cursor = false;
|
||||
|
||||
switch (style) {
|
||||
.default => {
|
||||
self.default_cursor = true;
|
||||
self.terminal.screens.active.cursor.cursor_style = self.default_cursor_style;
|
||||
self.terminal.modes.set(
|
||||
.cursor_blinking,
|
||||
self.default_cursor_blink orelse true,
|
||||
);
|
||||
},
|
||||
|
||||
.blinking_block => {
|
||||
self.terminal.screens.active.cursor.cursor_style = .block;
|
||||
self.terminal.modes.set(.cursor_blinking, true);
|
||||
},
|
||||
|
||||
.steady_block => {
|
||||
self.terminal.screens.active.cursor.cursor_style = .block;
|
||||
self.terminal.modes.set(.cursor_blinking, false);
|
||||
},
|
||||
|
||||
.blinking_underline => {
|
||||
self.terminal.screens.active.cursor.cursor_style = .underline;
|
||||
self.terminal.modes.set(.cursor_blinking, true);
|
||||
},
|
||||
|
||||
.steady_underline => {
|
||||
self.terminal.screens.active.cursor.cursor_style = .underline;
|
||||
self.terminal.modes.set(.cursor_blinking, false);
|
||||
},
|
||||
|
||||
.blinking_bar => {
|
||||
self.terminal.screens.active.cursor.cursor_style = .bar;
|
||||
self.terminal.modes.set(.cursor_blinking, true);
|
||||
},
|
||||
|
||||
.steady_bar => {
|
||||
self.terminal.screens.active.cursor.cursor_style = .bar;
|
||||
self.terminal.modes.set(.cursor_blinking, false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub inline fn decaln(self: *StreamHandler) !void {
|
||||
try self.terminal.decaln();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user