- commit
- 280e122
- parent
- e0ea9a3
- author
- Eric Bower
- date
- 2025-10-15 14:37:32 -0400 EDT
refactor: move render terminal snapshot to separate module
2 files changed,
+164,
-58
+2,
-58
1@@ -9,6 +9,7 @@ const builtin = @import("builtin");
2
3 const ghostty = @import("ghostty-vt");
4 const sgr = @import("sgr.zig");
5+const terminal_snapshot = @import("terminal_snapshot.zig");
6
7 const c = switch (builtin.os.tag) {
8 .macos => @cImport({
9@@ -797,7 +798,7 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
10
11 // If reattaching, send the scrollback buffer as binary frame
12 if (is_reattach) {
13- const buffer_slice = try renderTerminalSnapshot(session, client.allocator);
14+ const buffer_slice = try terminal_snapshot.render(&session.vt, client.allocator);
15 defer client.allocator.free(buffer_slice);
16
17 try protocol.writeBinaryFrame(client.fd, .pty_binary, buffer_slice);
18@@ -885,63 +886,6 @@ fn getSessionForClient(ctx: *ServerContext, client: *Client) ?*Session {
19 return ctx.sessions.get(session_name);
20 }
21
22-fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8 {
23- var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
24- errdefer output.deinit(allocator);
25-
26- // Get the terminal's page list
27- const pages = &session.vt.screen.pages;
28-
29- // Create row iterator for active viewport
30- var row_it = pages.rowIterator(.right_down, .{ .active = .{} }, null);
31-
32- // Iterate through viewport rows
33- var row_idx: usize = 0;
34- while (row_it.next()) |pin| : (row_idx += 1) {
35- // Get row and cell data from pin
36- const rac = pin.rowAndCell();
37- const row = rac.row;
38- const page = &pin.node.data;
39- const cells = page.getCells(row);
40-
41- // TODO: Process cells for this row (row_idx available for positioning)
42- _ = cells;
43- }
44-
45- // TODO: Properly restore scrollback
46- try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear screen and home cursor
47-
48- return output.toOwnedSlice(allocator);
49-}
50-
51-test "renderTerminalSnapshot: rowIterator viewport iteration" {
52- const testing = std.testing;
53- const allocator = testing.allocator;
54-
55- // Create a simple terminal
56- var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
57- defer vt.deinit(allocator);
58-
59- // Write some content
60- try vt.print('H');
61- try vt.print('e');
62- try vt.print('l');
63- try vt.print('l');
64- try vt.print('o');
65-
66- // Test that we can iterate through viewport using rowIterator
67- const pages = &vt.screen.pages;
68- var row_it = pages.rowIterator(.right_down, .{ .active = .{} }, null);
69-
70- var row_count: usize = 0;
71- while (row_it.next()) |pin| : (row_count += 1) {
72- const rac = pin.rowAndCell();
73- _ = rac; // Just verify we can access row and cell
74- }
75-
76- try testing.expectEqual(pages.rows, row_count);
77-}
78-
79 fn notifyAttachedClientsAndCleanup(session: *Session, ctx: *ServerContext, reason: []const u8) void {
80 // Copy the session name FIRST before doing anything else, including printing
81 // This protects against any potential memory corruption
+162,
-0
1@@ -0,0 +1,162 @@
2+const std = @import("std");
3+const ghostty = @import("ghostty-vt");
4+
5+/// Extract UTF-8 text content from a cell, including multi-codepoint graphemes
6+fn extractCellText(pin: ghostty.Pin, cell: *const ghostty.Cell, buf: *std.ArrayList(u8), allocator: std.mem.Allocator) !void {
7+ // Skip empty cells and spacer cells
8+ if (cell.isEmpty()) return;
9+ if (cell.wide == .spacer_tail or cell.wide == .spacer_head) return;
10+
11+ // Get the first codepoint
12+ const cp = cell.codepoint();
13+ if (cp == 0) return; // Empty cell
14+
15+ // Encode first codepoint to UTF-8
16+ var utf8_buf: [4]u8 = undefined;
17+ const len = std.unicode.utf8Encode(cp, &utf8_buf) catch return;
18+ try buf.appendSlice(allocator, utf8_buf[0..len]);
19+
20+ // If this is a multi-codepoint grapheme, encode the rest
21+ if (cell.hasGrapheme()) {
22+ if (pin.grapheme(cell)) |codepoints| {
23+ for (codepoints) |extra_cp| {
24+ const extra_len = std.unicode.utf8Encode(extra_cp, &utf8_buf) catch continue;
25+ try buf.appendSlice(allocator, utf8_buf[0..extra_len]);
26+ }
27+ }
28+ }
29+}
30+
31+/// Render the current terminal viewport state as text (with escape sequences to come)
32+/// Returns owned slice that must be freed by caller
33+pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
34+ var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
35+ errdefer output.deinit(allocator);
36+
37+ // Get the terminal's page list
38+ const pages = &vt.screen.pages;
39+
40+ // Create row iterator for active viewport
41+ var row_it = pages.rowIterator(.right_down, .{ .active = .{} }, null);
42+
43+ // Iterate through viewport rows
44+ var row_idx: usize = 0;
45+ while (row_it.next()) |pin| : (row_idx += 1) {
46+ // Get row and cell data from pin
47+ const rac = pin.rowAndCell();
48+ const row = rac.row;
49+ const page = &pin.node.data;
50+ const cells = page.getCells(row);
51+
52+ // Extract text from each cell in the row
53+ for (cells, 0..) |*cell, col_idx| {
54+ // Create a pin for this specific cell to access graphemes
55+ const cell_pin = ghostty.Pin{
56+ .node = pin.node,
57+ .y = pin.y,
58+ .x = @intCast(col_idx),
59+ };
60+
61+ try extractCellText(cell_pin, cell, &output, allocator);
62+ }
63+
64+ // Add newline after each row
65+ try output.appendSlice(allocator, "\n");
66+ }
67+
68+ // TODO: Properly restore scrollback with colors and cursor position
69+ // For now, just return the text content
70+ // try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear screen and home cursor
71+
72+ return output.toOwnedSlice(allocator);
73+}
74+
75+test "render: rowIterator viewport iteration" {
76+ const testing = std.testing;
77+ const allocator = testing.allocator;
78+
79+ // Create a simple terminal
80+ var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
81+ defer vt.deinit(allocator);
82+
83+ // Write some content
84+ try vt.print('H');
85+ try vt.print('e');
86+ try vt.print('l');
87+ try vt.print('l');
88+ try vt.print('o');
89+
90+ // Test that we can iterate through viewport using rowIterator
91+ const pages = &vt.screen.pages;
92+ var row_it = pages.rowIterator(.right_down, .{ .active = .{} }, null);
93+
94+ var row_count: usize = 0;
95+ while (row_it.next()) |pin| : (row_count += 1) {
96+ const rac = pin.rowAndCell();
97+ _ = rac; // Just verify we can access row and cell
98+ }
99+
100+ try testing.expectEqual(pages.rows, row_count);
101+}
102+
103+test "extractCellText: single codepoint" {
104+ const testing = std.testing;
105+ const allocator = testing.allocator;
106+
107+ var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
108+ defer vt.deinit(allocator);
109+
110+ // Write ASCII text
111+ try vt.print('A');
112+ try vt.print('B');
113+ try vt.print('C');
114+
115+ // Get the first cell
116+ const pages = &vt.screen.pages;
117+ const pin = pages.pin(.{ .active = .{} }).?;
118+ const rac = pin.rowAndCell();
119+ const page = &pin.node.data;
120+ const cells = page.getCells(rac.row);
121+
122+ // Extract text from first 3 cells
123+ var buf = try std.ArrayList(u8).initCapacity(allocator, 64);
124+ defer buf.deinit(allocator);
125+
126+ for (cells[0..3], 0..) |*cell, col_idx| {
127+ const cell_pin = ghostty.Pin{
128+ .node = pin.node,
129+ .y = pin.y,
130+ .x = @intCast(col_idx),
131+ };
132+ try extractCellText(cell_pin, cell, &buf, allocator);
133+ }
134+
135+ try testing.expectEqualStrings("ABC", buf.items);
136+}
137+
138+test "extractCellText: multi-codepoint grapheme (emoji)" {
139+ const testing = std.testing;
140+ const allocator = testing.allocator;
141+
142+ var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
143+ defer vt.deinit(allocator);
144+
145+ // Write an emoji with skin tone modifier (multi-codepoint grapheme)
146+ // 👋 (waving hand) + skin tone modifier
147+ try vt.print(0x1F44B); // 👋
148+ try vt.print(0x1F3FB); // light skin tone
149+
150+ const pages = &vt.screen.pages;
151+ const pin = pages.pin(.{ .active = .{} }).?;
152+ const rac = pin.rowAndCell();
153+ const page = &pin.node.data;
154+ const cells = page.getCells(rac.row);
155+
156+ var buf = try std.ArrayList(u8).initCapacity(allocator, 64);
157+ defer buf.deinit(allocator);
158+
159+ try extractCellText(pin, &cells[0], &buf, allocator);
160+
161+ // Should have both codepoints encoded as UTF-8
162+ try testing.expect(buf.items.len > 4); // At least 2 multi-byte UTF-8 sequences
163+}