- commit
- e0ea9a3
- parent
- e2c3da5
- author
- Eric Bower
- date
- 2025-10-15 14:21:21 -0400 EDT
chore(reattach): row iteration
2 files changed,
+281,
-4
+48,
-4
1@@ -8,6 +8,7 @@ const protocol = @import("protocol.zig");
2 const builtin = @import("builtin");
3
4 const ghostty = @import("ghostty-vt");
5+const sgr = @import("sgr.zig");
6
7 const c = switch (builtin.os.tag) {
8 .macos => @cImport({
9@@ -888,16 +889,59 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
10 var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
11 errdefer output.deinit(allocator);
12
13- _ = session;
14+ // Get the terminal's page list
15+ const pages = &session.vt.screen.pages;
16
17- // Clear screen when reattaching - let shell repaint naturally
18- // TODO: Properly restore scrollback once we figure out why ghostty-vt
19- // buffer contains corrupted escape sequence remnants
20+ // Create row iterator for active viewport
21+ var row_it = pages.rowIterator(.right_down, .{ .active = .{} }, null);
22+
23+ // Iterate through viewport rows
24+ var row_idx: usize = 0;
25+ while (row_it.next()) |pin| : (row_idx += 1) {
26+ // Get row and cell data from pin
27+ const rac = pin.rowAndCell();
28+ const row = rac.row;
29+ const page = &pin.node.data;
30+ const cells = page.getCells(row);
31+
32+ // TODO: Process cells for this row (row_idx available for positioning)
33+ _ = cells;
34+ }
35+
36+ // TODO: Properly restore scrollback
37 try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear screen and home cursor
38
39 return output.toOwnedSlice(allocator);
40 }
41
42+test "renderTerminalSnapshot: rowIterator viewport iteration" {
43+ const testing = std.testing;
44+ const allocator = testing.allocator;
45+
46+ // Create a simple terminal
47+ var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
48+ defer vt.deinit(allocator);
49+
50+ // Write some content
51+ try vt.print('H');
52+ try vt.print('e');
53+ try vt.print('l');
54+ try vt.print('l');
55+ try vt.print('o');
56+
57+ // Test that we can iterate through viewport using rowIterator
58+ const pages = &vt.screen.pages;
59+ var row_it = pages.rowIterator(.right_down, .{ .active = .{} }, null);
60+
61+ var row_count: usize = 0;
62+ while (row_it.next()) |pin| : (row_count += 1) {
63+ const rac = pin.rowAndCell();
64+ _ = rac; // Just verify we can access row and cell
65+ }
66+
67+ try testing.expectEqual(pages.rows, row_count);
68+}
69+
70 fn notifyAttachedClientsAndCleanup(session: *Session, ctx: *ServerContext, reason: []const u8) void {
71 // Copy the session name FIRST before doing anything else, including printing
72 // This protects against any potential memory corruption
+233,
-0
1@@ -0,0 +1,233 @@
2+const std = @import("std");
3+const ghostty = @import("ghostty-vt");
4+
5+/// Helper functions for generating ANSI SGR (Select Graphic Rendition) escape sequences
6+/// from ghostty-vt Style objects. Used to restore terminal styling when reattaching to sessions.
7+/// Generate SGR sequence to change from old_style to new_style.
8+/// Emits minimal SGR codes to reduce output size.
9+/// Returns owned slice that caller must free.
10+pub fn emitStyleChange(
11+ allocator: std.mem.Allocator,
12+ old_style: ghostty.Style,
13+ new_style: ghostty.Style,
14+) ![]u8 {
15+ var buf = try std.ArrayList(u8).initCapacity(allocator, 64);
16+
17+ // If new style is default, emit reset
18+ if (new_style.default()) {
19+ try buf.appendSlice(allocator, "\x1b[0m");
20+ return buf.toOwnedSlice(allocator);
21+ }
22+
23+ // Start escape sequence
24+ try buf.appendSlice(allocator, "\x1b[");
25+ var first = true;
26+
27+ // Helper to add separator
28+ const addSep = struct {
29+ fn call(b: *std.ArrayList(u8), alloc: std.mem.Allocator, is_first: *bool) !void {
30+ if (!is_first.*) {
31+ try b.append(alloc, ';');
32+ }
33+ is_first.* = false;
34+ }
35+ }.call;
36+
37+ // Bold
38+ if (new_style.flags.bold != old_style.flags.bold) {
39+ try addSep(&buf, allocator, &first);
40+ if (new_style.flags.bold) {
41+ try buf.append(allocator, '1');
42+ } else {
43+ try buf.appendSlice(allocator, "22");
44+ }
45+ }
46+
47+ // Faint
48+ if (new_style.flags.faint != old_style.flags.faint) {
49+ try addSep(&buf, allocator, &first);
50+ if (new_style.flags.faint) {
51+ try buf.append('2');
52+ } else {
53+ try buf.appendSlice("22");
54+ }
55+ }
56+
57+ // Italic
58+ if (new_style.flags.italic != old_style.flags.italic) {
59+ try addSep(&buf, allocator, &first);
60+ if (new_style.flags.italic) {
61+ try buf.append('3');
62+ } else {
63+ try buf.appendSlice("23");
64+ }
65+ }
66+
67+ // Underline
68+ if (!std.meta.eql(new_style.flags.underline, old_style.flags.underline)) {
69+ try addSep(&buf, allocator, &first);
70+ switch (new_style.flags.underline) {
71+ .none => try buf.appendSlice("24"),
72+ .single => try buf.append('4'),
73+ .double => try buf.appendSlice("21"),
74+ .curly => try buf.appendSlice("4:3"),
75+ .dotted => try buf.appendSlice("4:4"),
76+ .dashed => try buf.appendSlice("4:5"),
77+ }
78+ }
79+
80+ // Blink
81+ if (new_style.flags.blink != old_style.flags.blink) {
82+ try addSep(&buf, allocator, &first);
83+ if (new_style.flags.blink) {
84+ try buf.append('5');
85+ } else {
86+ try buf.appendSlice("25");
87+ }
88+ }
89+
90+ // Inverse
91+ if (new_style.flags.inverse != old_style.flags.inverse) {
92+ try addSep(&buf, allocator, &first);
93+ if (new_style.flags.inverse) {
94+ try buf.append('7');
95+ } else {
96+ try buf.appendSlice("27");
97+ }
98+ }
99+
100+ // Invisible
101+ if (new_style.flags.invisible != old_style.flags.invisible) {
102+ try addSep(&buf, allocator, &first);
103+ if (new_style.flags.invisible) {
104+ try buf.append('8');
105+ } else {
106+ try buf.appendSlice("28");
107+ }
108+ }
109+
110+ // Strikethrough
111+ if (new_style.flags.strikethrough != old_style.flags.strikethrough) {
112+ try addSep(&buf, allocator, &first);
113+ if (new_style.flags.strikethrough) {
114+ try buf.append('9');
115+ } else {
116+ try buf.appendSlice("29");
117+ }
118+ }
119+
120+ // Foreground color
121+ if (!std.meta.eql(new_style.fg_color, old_style.fg_color)) {
122+ try addSep(&buf, allocator, &first);
123+ switch (new_style.fg_color) {
124+ .none => try buf.appendSlice("39"),
125+ .palette => |idx| {
126+ try buf.appendSlice("38;5;");
127+ try buf.writer().print("{d}", .{idx});
128+ },
129+ .rgb => |rgb| {
130+ try buf.appendSlice("38;2;");
131+ try buf.writer().print("{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
132+ },
133+ }
134+ }
135+
136+ // Background color
137+ if (!std.meta.eql(new_style.bg_color, old_style.bg_color)) {
138+ try addSep(&buf, allocator, &first);
139+ switch (new_style.bg_color) {
140+ .none => try buf.appendSlice("49"),
141+ .palette => |idx| {
142+ try buf.appendSlice("48;5;");
143+ try buf.writer().print("{d}", .{idx});
144+ },
145+ .rgb => |rgb| {
146+ try buf.appendSlice("48;2;");
147+ try buf.writer().print("{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
148+ },
149+ }
150+ }
151+
152+ // Underline color (not all terminals support this, but emit it anyway)
153+ if (!std.meta.eql(new_style.underline_color, old_style.underline_color)) {
154+ try addSep(&buf, allocator, &first);
155+ switch (new_style.underline_color) {
156+ .none => try buf.appendSlice("59"),
157+ .palette => |idx| {
158+ try buf.appendSlice("58;5;");
159+ try buf.writer().print("{d}", .{idx});
160+ },
161+ .rgb => |rgb| {
162+ try buf.appendSlice("58;2;");
163+ try buf.writer().print("{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
164+ },
165+ }
166+ }
167+
168+ // End escape sequence
169+ try buf.append('m');
170+
171+ // If we only added the escape opener and closer with nothing in between,
172+ // return empty string (no change needed)
173+ if (first) {
174+ buf.deinit();
175+ return allocator.dupe(u8, "");
176+ }
177+
178+ return buf.toOwnedSlice();
179+}
180+
181+test "emitStyleChange: default to default" {
182+ const allocator = std.testing.allocator;
183+ const result = try emitStyleChange(allocator, .{}, .{});
184+ defer allocator.free(result);
185+ try std.testing.expectEqualStrings("", result);
186+}
187+
188+test "emitStyleChange: bold" {
189+ const allocator = std.testing.allocator;
190+ var new_style = ghostty.Style{};
191+ new_style.flags.bold = true;
192+ const result = try emitStyleChange(allocator, .{}, new_style);
193+ defer allocator.free(result);
194+ try std.testing.expectEqualStrings("\x1b[1m", result);
195+}
196+
197+test "emitStyleChange: reset to default" {
198+ const allocator = std.testing.allocator;
199+ var old_style = ghostty.Style{};
200+ old_style.flags.bold = true;
201+ old_style.flags.italic = true;
202+ const result = try emitStyleChange(allocator, old_style, .{});
203+ defer allocator.free(result);
204+ try std.testing.expectEqualStrings("\x1b[0m", result);
205+}
206+
207+test "emitStyleChange: palette color" {
208+ const allocator = std.testing.allocator;
209+ var new_style = ghostty.Style{};
210+ new_style.fg_color = .{ .palette = 196 }; // red
211+ const result = try emitStyleChange(allocator, .{}, new_style);
212+ defer allocator.free(result);
213+ try std.testing.expectEqualStrings("\x1b[38;5;196m", result);
214+}
215+
216+test "emitStyleChange: rgb color" {
217+ const allocator = std.testing.allocator;
218+ var new_style = ghostty.Style{};
219+ new_style.bg_color = .{ .rgb = .{ .r = 255, .g = 128, .b = 64 } };
220+ const result = try emitStyleChange(allocator, .{}, new_style);
221+ defer allocator.free(result);
222+ try std.testing.expectEqualStrings("\x1b[48;2;255;128;64m", result);
223+}
224+
225+test "emitStyleChange: multiple attributes" {
226+ const allocator = std.testing.allocator;
227+ var new_style = ghostty.Style{};
228+ new_style.flags.bold = true;
229+ new_style.flags.italic = true;
230+ new_style.fg_color = .{ .palette = 10 };
231+ const result = try emitStyleChange(allocator, .{}, new_style);
232+ defer allocator.free(result);
233+ try std.testing.expectEqualStrings("\x1b[1;3;38;5;10m", result);
234+}