repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
4b729ab
parent
5ef4207
author
Eric Bower
date
2025-10-14 10:12:35 -0400 EDT
refactor: renderTerminalSnapshot
1 files changed,  +77, -5
M src/daemon.zig
+77, -5
 1@@ -736,16 +736,88 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
 2     // Get the active screen from the terminal
 3     const screen = &session.vt.screen;
 4     var it = screen.pages.pageIterator(.right_down, .{ .screen = .{} }, null);
 5-    var page_index: usize = 0;
 6 
 7-    while (it.next()) |chunk| : (page_index += 1) {
 8+    var blank_rows: usize = 0;
 9+    var blank_cells: usize = 0;
10+
11+    while (it.next()) |chunk| {
12         const page: *const ghostty.Page = &chunk.node.data;
13         const start_y = chunk.start;
14         const end_y = chunk.end;
15-    }
16 
17-    // Add final newline if needed
18-    try output.appendSlice(allocator, "\r\n");
19+        for (start_y..end_y) |y_usize| {
20+            const y: u16 = @intCast(y_usize);
21+            const row = page.getRow(y);
22+            const cells = page.getCells(row);
23+
24+            // Check if row has any text content
25+            const has_text = blk: {
26+                for (cells) |cell| {
27+                    if (cell.hasText()) break :blk true;
28+                }
29+                break :blk false;
30+            };
31+
32+            // If row is blank, accumulate blank rows
33+            if (!has_text) {
34+                blank_rows += 1;
35+                continue;
36+            }
37+
38+            // Dump accumulated blank rows as newlines
39+            for (0..blank_rows) |_| {
40+                try output.appendSlice(allocator, "\r\n");
41+            }
42+            blank_rows = 0;
43+
44+            // If row doesn't continue a wrap, reset blank cells
45+            if (!row.wrap_continuation) blank_cells = 0;
46+
47+            // Write each cell in the row
48+            for (cells) |*cell| {
49+                // Skip spacer cells (wide character continuations)
50+                switch (cell.wide) {
51+                    .narrow, .wide => {},
52+                    .spacer_head, .spacer_tail => continue,
53+                }
54+
55+                // Accumulate blank cells
56+                if (!cell.hasText()) {
57+                    blank_cells += 1;
58+                    continue;
59+                }
60+
61+                // Dump accumulated blank cells as spaces
62+                if (blank_cells > 0) {
63+                    for (0..blank_cells) |_| {
64+                        try output.append(allocator, ' ');
65+                    }
66+                    blank_cells = 0;
67+                }
68+
69+                // Write the actual character
70+                const cp = cell.content.codepoint;
71+                var buf: [4]u8 = undefined;
72+                const len = std.unicode.utf8Encode(cp, &buf) catch continue;
73+                try output.appendSlice(allocator, buf[0..len]);
74+
75+                // Handle grapheme clusters (combining characters)
76+                if (cell.content_tag == .codepoint_grapheme) {
77+                    if (page.lookupGrapheme(cell)) |grapheme| {
78+                        for (grapheme) |gcp| {
79+                            const glen = std.unicode.utf8Encode(gcp, &buf) catch continue;
80+                            try output.appendSlice(allocator, buf[0..glen]);
81+                        }
82+                    }
83+                }
84+            }
85+
86+            // Add newline after each row (unless it wraps to the next)
87+            if (!row.wrap) {
88+                try output.appendSlice(allocator, "\r\n");
89+            }
90+        }
91+    }
92 
93     // Position cursor at correct location (ANSI is 1-based)
94     const cursor = screen.cursor;