diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 3cf34cc4c..014d2c8b0 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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); diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 741496231..cafef912f 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -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" { diff --git a/src/terminal/stream_terminal.zig b/src/terminal/stream_terminal.zig index 1c27c5c08..1e4dc24be 100644 --- a/src/terminal/stream_terminal.zig +++ b/src/terminal/stream_terminal.zig @@ -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), diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index f5d9580eb..3f8b3d418 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -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( diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2438003f5..91dcb7d3e 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -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(); }