repos / zmx

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

commit
0639e71
parent
abfa487
author
Eric Bower
date
2025-10-10 21:21:20 -0400 EDT
feat: terminal restore works!
2 files changed,  +55, -17
M src/attach.zig
+1, -1
1@@ -123,7 +123,7 @@ fn writeCallback(
2 
3 const ReadContext = struct {
4     ctx: *Context,
5-    buffer: [4096]u8,
6+    buffer: [128 * 1024]u8, // 128KB to handle large scrollback messages
7 };
8 
9 fn readCallback(
M src/daemon.zig
+54, -16
 1@@ -529,30 +529,50 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 2         _ = try posix.write(client.fd, response);
 3     }
 4 
 5-    // If reattaching, send the current terminal state after attach response
 6-    if (is_reattach) {
 7-        const snapshot = try renderTerminalSnapshot(session, ctx.allocator);
 8-        defer ctx.allocator.free(snapshot);
 9+    // If reattaching, send the scrollback buffer (raw PTY output with colors)
10+    // Limit to last 64KB to avoid huge JSON messages
11+    if (is_reattach and session.buffer.items.len > 0) {
12+        std.debug.print("Sending scrollback buffer: {d} bytes total\n", .{session.buffer.items.len});
13 
14-        // Use Zig's JSON formatting to properly escape the snapshot
15+        const max_buffer_size = 64 * 1024;
16+        const buffer_start = if (session.buffer.items.len > max_buffer_size)
17+            session.buffer.items.len - max_buffer_size
18+        else
19+            0;
20+        const buffer_slice = session.buffer.items[buffer_start..];
21+
22+        std.debug.print("Sending slice: {d} bytes (from offset {d})\n", .{ buffer_slice.len, buffer_start });
23+
24+        // Use Zig's JSON formatting to properly escape the buffer
25         var out: std.io.Writer.Allocating = .init(ctx.allocator);
26         defer out.deinit();
27 
28         var json_writer: std.json.Stringify = .{ .writer = &out.writer };
29 
30-        try json_writer.beginObject();
31-        try json_writer.objectField("type");
32-        try json_writer.write("pty_out");
33-        try json_writer.objectField("payload");
34-        try json_writer.beginObject();
35-        try json_writer.objectField("text");
36-        try json_writer.write(snapshot);
37-        try json_writer.endObject();
38-        try json_writer.endObject();
39-
40-        const response = try std.fmt.allocPrint(ctx.allocator, "{s}\n", .{out.written()});
41+        json_writer.beginObject() catch |err| {
42+            std.debug.print("JSON beginObject error: {s}\n", .{@errorName(err)});
43+            return;
44+        };
45+        json_writer.objectField("type") catch return;
46+        json_writer.write("pty_out") catch return;
47+        json_writer.objectField("payload") catch return;
48+        json_writer.beginObject() catch return;
49+        json_writer.objectField("text") catch return;
50+        json_writer.write(buffer_slice) catch |err| {
51+            std.debug.print("JSON write buffer error: {s}\n", .{@errorName(err)});
52+            return;
53+        };
54+        json_writer.endObject() catch return;
55+        json_writer.endObject() catch return;
56+
57+        const json_output = out.written();
58+        std.debug.print("JSON output length: {d} bytes\n", .{json_output.len});
59+        std.debug.print("JSON first 100 bytes: {s}\n", .{json_output[0..@min(100, json_output.len)]});
60+
61+        const response = try std.fmt.allocPrint(ctx.allocator, "{s}\n", .{json_output});
62         defer ctx.allocator.free(response);
63         _ = try posix.write(client.fd, response);
64+        std.debug.print("Sent scrollback buffer to client fd={d}\n", .{client.fd});
65     }
66 }
67 
68@@ -718,6 +738,11 @@ fn readPtyCallback(
69         const data = read_buffer.slice[0..bytes_read];
70         std.debug.print("PTY output ({d} bytes)\n", .{bytes_read});
71 
72+        // Store PTY output in buffer for session restore
73+        session.buffer.appendSlice(session.allocator, data) catch |err| {
74+            std.debug.print("Buffer append error: {s}\n", .{@errorName(err)});
75+        };
76+
77         // ALWAYS parse through libghostty-vt to maintain state
78         session.vt_stream.nextSlice(data) catch |err| {
79             std.debug.print("VT parse error: {s}\n", .{@errorName(err)});
80@@ -775,6 +800,19 @@ fn readPtyCallback(
81         );
82         return .disarm;
83     } else |err| {
84+        // WouldBlock is expected for non-blocking I/O
85+        if (err == error.WouldBlock) {
86+            stream.read(
87+                loop,
88+                completion,
89+                .{ .slice = &session.pty_read_buffer },
90+                PtyReadContext,
91+                pty_ctx,
92+                readPtyCallback,
93+            );
94+            return .disarm;
95+        }
96+
97         std.debug.print("PTY read error: {s}\n", .{@errorName(err)});
98         return .disarm;
99     }