repos / zmx

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

commit
eaf6c5a
parent
7740eb9
author
Eric Bower
date
2025-10-14 09:04:34 -0400 EDT
refactor: renderTerminalSnapshot
3 files changed,  +36, -35
M specs/protocol.md
+1, -1
1@@ -34,7 +34,7 @@ The protocol uses a hybrid approach: JSON for control messages and binary frames
2 **Current Usage:**
3 - Control messages (attach, detach, kill, etc.): NDJSON format
4 - PTY output from daemon to client: Binary frames (type 2)
5-- PTY input from client to daemon: JSON `pty_in` messages (may be optimized to binary frames in future)
6+- PTY input from client to daemon: Binary frames (type 2)
7 
8 ## Message Structure
9 
M src/daemon.zig
+31, -30
 1@@ -638,7 +638,7 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 2 
 3     // If reattaching, send the scrollback buffer as binary frame
 4     if (is_reattach) {
 5-        const buffer_slice = try session.vt.plainStringUnwrapped(client.allocator);
 6+        const buffer_slice = try renderTerminalSnapshot(session, client.allocator);
 7         defer client.allocator.free(buffer_slice);
 8 
 9         try protocol.writeBinaryFrame(client.fd, .pty_binary, buffer_slice);
10@@ -738,40 +738,41 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
11     const rows = screen.pages.rows;
12     const cols = screen.pages.cols;
13 
14-    var row: usize = 0;
15-    while (row < rows) : (row += 1) {
16-        var col: usize = 0;
17-        while (col < cols) : (col += 1) {
18-            // Build a point.Point referring to the active (visible) page
19-            const pt: ghostty.point.Point = .{ .active = .{
20-                .x = @as(u16, @intCast(col)),
21-                .y = @as(u16, @intCast(row)),
22-            } };
23-
24-            if (screen.pages.getCell(pt)) |cell_ref| {
25-                const cp = cell_ref.cell.content.codepoint;
26-                if (cp == 0) {
27-                    try output.append(allocator, ' ');
28-                } else {
29-                    var buf: [4]u8 = undefined;
30-                    const len = std.unicode.utf8Encode(cp, &buf) catch 0;
31-                    if (len == 0) {
32-                        try output.append(allocator, ' ');
33-                    } else {
34-                        try output.appendSlice(allocator, buf[0..len]);
35-                    }
36-                }
37-            } else {
38-                // Outside bounds or no cell => space to preserve width
39-                try output.append(allocator, ' ');
40-            }
41-        }
42+    // Use cellIterator to walk through the visible viewport
43+    const tl_pt: ghostty.point.Point = screen.pages.getTopLeft(tl);
44+    const br_pt: ghostty.point.Point = screen.pages.getBottomRight(tl);
45+
46+    var it = screen.pages.cellIterator(.right_down, tl_pt, br_pt);
47+    var current_row: u16 = 0;
48 
49-        if (row < rows - 1) {
50+    while (it.next()) |pin| {
51+        const cell_info = pin.rowAndCell();
52+        const cell = cell_info.cell;
53+
54+        // Check if we've moved to a new row
55+        if (pin.y > current_row) {
56             try output.appendSlice(allocator, "\r\n");
57+            current_row = pin.y;
58+        }
59+
60+        // Write the cell content
61+        const cp = cell.content.codepoint;
62+        if (cp == 0) {
63+            try output.append(allocator, ' ');
64+        } else {
65+            var buf: [4]u8 = undefined;
66+            const len = std.unicode.utf8Encode(cp, &buf) catch 0;
67+            if (len == 0) {
68+                try output.append(allocator, ' ');
69+            } else {
70+                try output.appendSlice(allocator, buf[0..len]);
71+            }
72         }
73     }
74 
75+    // Add final newline if needed
76+    try output.appendSlice(allocator, "\r\n");
77+
78     // Position cursor at correct location (ANSI is 1-based)
79     const cursor = screen.cursor;
80     try output.writer(allocator).print("\x1b[{d};{d}H", .{ cursor.y + 1, cursor.x + 1 });
M src/protocol.zig
+4, -4
 1@@ -210,11 +210,11 @@ pub const LineBuffer = struct {
 2     };
 3 };
 4 
 5-// Future: Binary frame support for PTY data
 6+// Binary frame support for PTY data
 7 // This infrastructure allows us to add binary framing later without breaking existing code
 8 pub const FrameType = enum(u16) {
 9     json_control = 1, // JSON-encoded control messages (current protocol)
10-    pty_binary = 2, // Raw PTY bytes (future optimization)
11+    pty_binary = 2, // Raw PTY bytes
12 };
13 
14 pub const FrameHeader = packed struct {
15@@ -222,7 +222,7 @@ pub const FrameHeader = packed struct {
16     frame_type: u16, // little-endian, FrameType value
17 };
18 
19-// Future: Helper to write a binary frame (not used yet)
20+// Helper to write a binary frame
21 pub fn writeBinaryFrame(fd: posix.fd_t, frame_type: FrameType, payload: []const u8) !void {
22     const header = FrameHeader{
23         .length = @intCast(payload.len),
24@@ -234,7 +234,7 @@ pub fn writeBinaryFrame(fd: posix.fd_t, frame_type: FrameType, payload: []const
25     _ = try posix.write(fd, payload);
26 }
27 
28-// Future: Helper to read a binary frame (not used yet)
29+// Helper to read a binary frame (not used yet)
30 pub fn readBinaryFrame(allocator: std.mem.Allocator, fd: posix.fd_t) !struct { frame_type: FrameType, payload: []u8 } {
31     var header_bytes: [@sizeOf(FrameHeader)]u8 = undefined;
32     const read_len = try posix.read(fd, &header_bytes);