diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index fa299efd0..e349f6b90 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -57,6 +57,7 @@ const Allocator = std.mem.Allocator; const utf8proc = @import("utf8proc"); const trace = @import("tracy").trace; const ansi = @import("ansi.zig"); +const modes = @import("modes.zig"); const sgr = @import("sgr.zig"); const color = @import("color.zig"); const kitty = @import("kitty.zig"); @@ -107,6 +108,17 @@ pub const Cursor = struct { /// is determined by mode 12 (modes.zig). This mode is synchronized /// with CSI q, the same as xterm. pub const Style = enum { bar, block, underline }; + + /// Saved cursor state. This contains more than just Cursor members + /// because additional state is stored. + pub const Saved = struct { + x: usize, + y: usize, + pen: Cell, + pending_wrap: bool, + origin: bool, + charset: CharsetState, + }; }; /// This is a single item within the storage buffer. We use a union to @@ -931,7 +943,7 @@ history: usize, cursor: Cursor = .{}, /// Saved cursor saved with DECSC (ESC 7). -saved_cursor: Cursor = .{}, +saved_cursor: ?Cursor.Saved = null, /// The selection for this screen (if any). selection: ?Selection = null, @@ -945,15 +957,6 @@ kitty_images: kitty.graphics.ImageStorage = .{}, /// The charset state charset: CharsetState = .{}, -/// The saved charset state. This state is saved / restored along with the -/// cursor state -saved_charset: CharsetState = .{}, - -/// The saved state of origin mode. This mode gets special handling in Screen -/// because it's state is saved/restored with the cursor and must have a state -/// independent to each screen (primary and alternate) -saved_origin_mode: bool = false, - /// The current or most recent protected mode. Once a protection mode is /// set, this will never become "off" again until the screen is reset. /// The current state of whether protection attributes should be set is diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index f029b6bba..ed1cc3c36 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -416,9 +416,14 @@ fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 { /// is kept per screen (main / alternative). If for the current screen state /// was already saved it is overwritten. pub fn saveCursor(self: *Terminal) void { - self.screen.saved_cursor = self.screen.cursor; - self.screen.saved_charset = self.screen.charset; - self.screen.saved_origin_mode = self.modes.get(.origin); + self.screen.saved_cursor = .{ + .x = self.screen.cursor.x, + .y = self.screen.cursor.y, + .pen = self.screen.cursor.pen, + .pending_wrap = self.screen.cursor.pending_wrap, + .origin = self.modes.get(.origin), + .charset = self.screen.charset, + }; } /// Restore cursor position and other state. @@ -426,9 +431,21 @@ pub fn saveCursor(self: *Terminal) void { /// The primary and alternate screen have distinct save state. /// If no save was done before values are reset to their initial values. pub fn restoreCursor(self: *Terminal) void { - self.screen.cursor = self.screen.saved_cursor; - self.screen.charset = self.screen.saved_charset; - self.modes.set(.origin, self.screen.saved_origin_mode); + const saved: Screen.Cursor.Saved = self.screen.saved_cursor orelse .{ + .x = 0, + .y = 0, + .pen = .{}, + .pending_wrap = false, + .origin = false, + .charset = .{}, + }; + + self.screen.cursor.pen = saved.pen; + self.screen.charset = saved.charset; + self.modes.set(.origin, saved.origin); + self.screen.cursor.x = saved.x; + self.screen.cursor.y = saved.y; + self.screen.cursor.pending_wrap = saved.pending_wrap; } /// TODO: test @@ -1993,7 +2010,7 @@ pub fn fullReset(self: *Terminal, alloc: Allocator) void { self.flags = .{}; self.tabstops.reset(TABSTOP_INTERVAL); self.screen.cursor = .{}; - self.screen.saved_cursor = .{}; + self.screen.saved_cursor = null; self.screen.selection = null; self.screen.kitty_keyboard = .{}; self.screen.protected_mode = .off; @@ -4760,6 +4777,66 @@ test "Terminal: saveCursor with screen change" { try testing.expect(t.modes.get(.origin)); } +test "Terminal: saveCursor position" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + t.setCursorPos(1, 5); + try t.print('A'); + t.saveCursor(); + t.setCursorPos(1, 1); + try t.print('B'); + t.restoreCursor(); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("B AX", str); + } +} + +test "Terminal: saveCursor pending wrap state" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + t.setCursorPos(1, 5); + try t.print('A'); + t.saveCursor(); + t.setCursorPos(1, 1); + try t.print('B'); + t.restoreCursor(); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("B A\nX", str); + } +} + +test "Terminal: saveCursor origin mode" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 5); + defer t.deinit(alloc); + + t.modes.set(.origin, true); + t.saveCursor(); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(3, 5); + t.setTopAndBottomMargin(2, 4); + t.restoreCursor(); + try t.print('X'); + + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("X", str); + } +} + test "Terminal: setProtectedMode" { const alloc = testing.allocator; var t = try init(alloc, 3, 3); diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index b7d2e567e..ddce8c7f0 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -162,7 +162,13 @@ const ModeEntry = struct { name: []const u8, value: comptime_int, default: bool = false, + + /// True if this is an ANSI mode, false if its a DEC mode (?-prefixed). ansi: bool = false, + + /// If true, this mode is disabled and Ghostty will not allow it to be + /// set or queried. The mode enum still has it, allowing Ghostty developers + /// to develop a mode without exposing it to real users. disabled: bool = false, }; diff --git a/website/app/vt/decrc/page.mdx b/website/app/vt/decrc/page.mdx new file mode 100644 index 000000000..4b6cace82 --- /dev/null +++ b/website/app/vt/decrc/page.mdx @@ -0,0 +1,14 @@ +import VTSequence from "@/components/VTSequence"; + +# Restore Cursor (DECRC) + + + +Restore the cursor-related state saved via [Save Cursor (DECSC)](/vt/decsc). + +If a cursor was never previously saved, this sets all the typically saved +values to their default values. + +## Validation + +Validation is shared with [Save Cursor (DECSC)](/vt/decsc). diff --git a/website/app/vt/decsc/page.mdx b/website/app/vt/decsc/page.mdx new file mode 100644 index 000000000..d7ba4f068 --- /dev/null +++ b/website/app/vt/decsc/page.mdx @@ -0,0 +1,83 @@ +import VTSequence from "@/components/VTSequence"; + +# Save Cursor (DECSC) + + + +Save various cursor-related state that can be restored with +[Restore Cursor (DECRC)](/vt/decrc). + +The following attributes are saved: + +- Cursor row and column in absolute screen coordinates +- Character sets +- Pending wrap state +- SGR attributes +- [Origin mode (DEC Mode 6)](/vt/modes/origin) + +Only one cursor can be saved at any time. If save cursor is repeated, the +previously save cursor is overwritten. + +Primary and alternate screens have separate saved cursor state. A cursor +saved on the primary screen is inaccessible from the alternate screen +and vice versa. + +## Validation + +### SC V-1: Cursor Position + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "\033[1;5H" +printf "A" +printf "\0337" # Save Cursor +printf "\033[1;1H" +printf "B" +printf "\0338" # Restore Cursor +printf "X" +``` + +``` +|B___AX____| +``` + +### SC V-2: Pending Wrap State + +```bash +cols=$(tput cols) +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "\033[${cols}G" +printf "A" +printf "\0337" # Save Cursor +printf "\033[1;1H" +printf "B" +printf "\0338" # Restore Cursor +printf "X" +``` + +``` +|B________A| +|X_________| +``` + +### SC V-3: SGR Attributes + +```bash +printf "\033[1;1H" # move to top-left +printf "\033[0J" # clear screen +printf "\033[1;4;33;44m" +printf "A" +printf "\0337" # Save Cursor +printf "\033[0m" +printf "B" +printf "\0338" # Restore Cursor +printf "X" +``` + +``` +|AX________| +``` + +The "A" and "X" should have identical styling.