- commit
- e3a2063
- parent
- 85cda06
- author
- Eric Bower
- date
- 2025-10-14 13:13:41 -0400 EDT
feat: restore terminal state
1 files changed,
+155,
-10
+155,
-10
1@@ -95,6 +95,105 @@ const VTHandler = struct {
2 self.terminal.tabReset();
3 }
4
5+ // Cursor movement (relative)
6+ pub fn cursorUp(self: *VTHandler, count: usize) !void {
7+ self.terminal.cursorUp(count);
8+ }
9+
10+ pub fn cursorDown(self: *VTHandler, count: usize) !void {
11+ self.terminal.cursorDown(count);
12+ }
13+
14+ pub fn cursorForward(self: *VTHandler, count: usize) !void {
15+ self.terminal.cursorRight(count);
16+ }
17+
18+ pub fn cursorBack(self: *VTHandler, count: usize) !void {
19+ self.terminal.cursorLeft(count);
20+ }
21+
22+ pub fn setCursorColRelative(self: *VTHandler, count: usize) !void {
23+ const new_col = self.terminal.screen.cursor.x + count;
24+ self.terminal.setCursorPos(self.terminal.screen.cursor.y, new_col);
25+ }
26+
27+ pub fn setCursorRowRelative(self: *VTHandler, count: usize) !void {
28+ const new_row = self.terminal.screen.cursor.y + count;
29+ self.terminal.setCursorPos(new_row, self.terminal.screen.cursor.x);
30+ }
31+
32+ // Special movement (ESC sequences)
33+ pub fn index(self: *VTHandler) !void {
34+ try self.terminal.index();
35+ }
36+
37+ pub fn reverseIndex(self: *VTHandler) !void {
38+ self.terminal.reverseIndex();
39+ }
40+
41+ pub fn nextLine(self: *VTHandler) !void {
42+ try self.terminal.linefeed();
43+ self.terminal.carriageReturn();
44+ }
45+
46+ pub fn prevLine(self: *VTHandler) !void {
47+ self.terminal.reverseIndex();
48+ self.terminal.carriageReturn();
49+ }
50+
51+ // Line/char editing
52+ pub fn insertLines(self: *VTHandler, count: usize) !void {
53+ self.terminal.insertLines(count);
54+ }
55+
56+ pub fn deleteLines(self: *VTHandler, count: usize) !void {
57+ self.terminal.deleteLines(count);
58+ }
59+
60+ pub fn deleteChars(self: *VTHandler, count: usize) !void {
61+ self.terminal.deleteChars(count);
62+ }
63+
64+ pub fn eraseChars(self: *VTHandler, count: usize) !void {
65+ self.terminal.eraseChars(count);
66+ }
67+
68+ pub fn scrollUp(self: *VTHandler, count: usize) !void {
69+ self.terminal.scrollUp(count);
70+ }
71+
72+ pub fn scrollDown(self: *VTHandler, count: usize) !void {
73+ self.terminal.scrollDown(count);
74+ }
75+
76+ // Basic control characters
77+ pub fn carriageReturn(self: *VTHandler) !void {
78+ self.terminal.carriageReturn();
79+ }
80+
81+ pub fn linefeed(self: *VTHandler) !void {
82+ try self.terminal.linefeed();
83+ }
84+
85+ pub fn backspace(self: *VTHandler) !void {
86+ self.terminal.backspace();
87+ }
88+
89+ pub fn horizontalTab(self: *VTHandler, count: usize) !void {
90+ _ = count; // stream always passes 1
91+ try self.terminal.horizontalTab();
92+ }
93+
94+ pub fn horizontalTabBack(self: *VTHandler, count: usize) !void {
95+ _ = count; // stream always passes 1
96+ try self.terminal.horizontalTabBack();
97+ }
98+
99+ pub fn bell(self: *VTHandler) !void {
100+ _ = self;
101+ // Ignore bell in daemon context - no UI to notify
102+ }
103+
104 pub fn deviceAttributes(
105 self: *VTHandler,
106 req: ghostty.DeviceAttributeReq,
107@@ -789,11 +888,9 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
108 var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
109 errdefer output.deinit(allocator);
110
111- // Clear screen and move to home
112- try output.appendSlice(allocator, "\x1b[2J\x1b[H");
113-
114 const vt = &session.vt;
115 const screen = &session.vt.screen;
116+ const scroll_region = &vt.scrolling_region;
117
118 // Debug: Print all enabled modes
119 std.debug.print("Terminal modes for session {s}:\n", .{session.name});
120@@ -813,11 +910,48 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
121 }
122 }
123
124- // Restore terminal modes by sending appropriate escape sequences
125- // Iterate over all modes and generate the correct escape sequence for each
126+ // Step 1: Neutralize constraints for predictable snapshot printing
127+ try output.appendSlice(allocator, "\x1b[?69l"); // Disable left/right margins
128+ try output.appendSlice(allocator, "\x1b[r"); // Reset scroll region to full screen
129+ try output.appendSlice(allocator, "\x1b[?6l"); // Disable origin mode temporarily
130+ try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear and home
131+
132+ // Step 2: Print terminal content
133+ const content = try session.vt.plainStringUnwrapped(allocator);
134+ defer allocator.free(content);
135+ try output.appendSlice(allocator, content);
136+
137+ // Step 3: Restore scroll regions (if non-default)
138+ const default_top: u16 = 0;
139+ const default_bottom: u16 = vt.rows - 1;
140+ const default_left: u16 = 0;
141+ const default_right: u16 = vt.cols - 1;
142+
143+ if (scroll_region.top != default_top or scroll_region.bottom != default_bottom) {
144+ // DECSTBM - set top/bottom margins (1-based)
145+ try output.writer(allocator).print("\x1b[{d};{d}r", .{
146+ scroll_region.top + 1,
147+ scroll_region.bottom + 1,
148+ });
149+ }
150+
151+ if (scroll_region.left != default_left or scroll_region.right != default_right) {
152+ // Enable LRMM first, then set left/right margins (1-based)
153+ try output.appendSlice(allocator, "\x1b[?69h"); // Enable left/right margin mode
154+ try output.writer(allocator).print("\x1b[{d};{d}s", .{
155+ scroll_region.left + 1,
156+ scroll_region.right + 1,
157+ });
158+ }
159+
160+ // Step 4: Restore terminal modes (except origin, which we do later)
161 inline for (@typeInfo(ghostty.Mode).@"enum".fields) |field| {
162 @setEvalBranchQuota(6000);
163 const mode: ghostty.Mode = @field(ghostty.Mode, field.name);
164+
165+ // Skip origin mode - we'll restore it after cursor positioning
166+ if (mode == .origin) continue;
167+
168 const value = vt.modes.get(mode);
169 const tag: ghostty.modes.ModeTag = @bitCast(@as(ghostty.modes.ModeTag.Backing, field.value));
170 const mode_default = @field(vt.modes.default, field.name);
171@@ -835,13 +969,24 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
172 }
173 }
174
175- const content = try session.vt.plainStringUnwrapped(allocator);
176- defer allocator.free(content);
177- try output.appendSlice(allocator, content);
178+ // Step 5: Restore origin mode if it was enabled
179+ const origin_mode = vt.modes.get(.origin);
180+ if (origin_mode) {
181+ try output.appendSlice(allocator, "\x1b[?6h");
182+ }
183
184- // Position cursor at correct location (ANSI is 1-based)
185+ // Step 6: Position cursor (origin-aware, 1-based)
186 const cursor = screen.cursor;
187- try output.writer(allocator).print("\x1b[{d};{d}H", .{ cursor.y + 1, cursor.x + 1 });
188+ const cursor_row: u16 = if (origin_mode)
189+ (cursor.y -| scroll_region.top) + 1
190+ else
191+ cursor.y + 1;
192+ const cursor_col: u16 = if (origin_mode)
193+ (cursor.x -| scroll_region.left) + 1
194+ else
195+ cursor.x + 1;
196+
197+ try output.writer(allocator).print("\x1b[{d};{d}H", .{ cursor_row, cursor_col });
198
199 return output.toOwnedSlice(allocator);
200 }