repos / zmx

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

commit
abfa487
parent
89393db
author
Eric Bower
date
2025-10-10 21:10:50 -0400 EDT
fix: restore session
1 files changed,  +32, -40
M src/daemon.zig
+32, -40
  1@@ -361,7 +361,7 @@ fn handleDetachSession(client: *Client, session_name: []const u8, target_client_
  2 
  3         client.attached_session = null;
  4         _ = session.attached_clients.remove(client.fd);
  5-        
  6+
  7         const response = "{\"type\":\"detach_session_response\",\"payload\":{\"status\":\"ok\"}}\n";
  8         std.debug.print("Sending detach response to client fd={d}: {s}", .{ client.fd, response });
  9 
 10@@ -515,40 +515,6 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 11     client.attached_session = session.name;
 12     try session.attached_clients.put(client.fd, {});
 13 
 14-    // If reattaching, send the current terminal state
 15-    if (is_reattach and session.attached_clients.count() > 1) {
 16-        const snapshot = try renderTerminalSnapshot(session, ctx.allocator);
 17-        defer ctx.allocator.free(snapshot);
 18-
 19-        // Build JSON response with escaped snapshot
 20-        var response_buf = try std.ArrayList(u8).initCapacity(ctx.allocator, 4096);
 21-        defer response_buf.deinit(ctx.allocator);
 22-
 23-        try response_buf.appendSlice(ctx.allocator, "{\"type\":\"pty_out\",\"payload\":{\"text\":\"");
 24-
 25-        // Escape the snapshot for JSON
 26-        for (snapshot) |byte| {
 27-            switch (byte) {
 28-                '"' => try response_buf.appendSlice(ctx.allocator, "\\\""),
 29-                '\\' => try response_buf.appendSlice(ctx.allocator, "\\\\"),
 30-                '\n' => try response_buf.appendSlice(ctx.allocator, "\\n"),
 31-                '\r' => try response_buf.appendSlice(ctx.allocator, "\\r"),
 32-                '\t' => try response_buf.appendSlice(ctx.allocator, "\\t"),
 33-                0x08 => try response_buf.appendSlice(ctx.allocator, "\\b"),
 34-                0x0C => try response_buf.appendSlice(ctx.allocator, "\\f"),
 35-                0x00...0x07, 0x0B, 0x0E...0x1F, 0x7F...0xFF => {
 36-                    const escaped = try std.fmt.allocPrint(ctx.allocator, "\\u{x:0>4}", .{byte});
 37-                    defer ctx.allocator.free(escaped);
 38-                    try response_buf.appendSlice(ctx.allocator, escaped);
 39-                },
 40-                else => try response_buf.append(ctx.allocator, byte),
 41-            }
 42-        }
 43-
 44-        try response_buf.appendSlice(ctx.allocator, "\"}}\n");
 45-        _ = try posix.write(client.fd, response_buf.items);
 46-    }
 47-
 48     // Start reading from PTY if not already started (first client)
 49     if (session.attached_clients.count() == 1) {
 50         try readFromPty(ctx, client, session);
 51@@ -562,6 +528,32 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
 52         defer ctx.allocator.free(response);
 53         _ = try posix.write(client.fd, response);
 54     }
 55+
 56+    // If reattaching, send the current terminal state after attach response
 57+    if (is_reattach) {
 58+        const snapshot = try renderTerminalSnapshot(session, ctx.allocator);
 59+        defer ctx.allocator.free(snapshot);
 60+
 61+        // Use Zig's JSON formatting to properly escape the snapshot
 62+        var out: std.io.Writer.Allocating = .init(ctx.allocator);
 63+        defer out.deinit();
 64+
 65+        var json_writer: std.json.Stringify = .{ .writer = &out.writer };
 66+
 67+        try json_writer.beginObject();
 68+        try json_writer.objectField("type");
 69+        try json_writer.write("pty_out");
 70+        try json_writer.objectField("payload");
 71+        try json_writer.beginObject();
 72+        try json_writer.objectField("text");
 73+        try json_writer.write(snapshot);
 74+        try json_writer.endObject();
 75+        try json_writer.endObject();
 76+
 77+        const response = try std.fmt.allocPrint(ctx.allocator, "{s}\n", .{out.written()});
 78+        defer ctx.allocator.free(response);
 79+        _ = try posix.write(client.fd, response);
 80+    }
 81 }
 82 
 83 fn handlePtyInput(client: *Client, text: []const u8) !void {
 84@@ -663,17 +655,17 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
 85     const screen = &session.vt.screen;
 86     const rows = screen.pages.rows;
 87     const cols = screen.pages.cols;
 88-    
 89+
 90     var row: usize = 0;
 91     while (row < rows) : (row += 1) {
 92         var col: usize = 0;
 93         while (col < cols) : (col += 1) {
 94             // Build a point.Point referring to the active (visible) page
 95-            const pt: ghostty.point.Point = .{ .active = .{ 
 96-                .x = @as(u16, @intCast(col)), 
 97+            const pt: ghostty.point.Point = .{ .active = .{
 98+                .x = @as(u16, @intCast(col)),
 99                 .y = @as(u16, @intCast(row)),
100             } };
101-            
102+
103             if (screen.pages.getCell(pt)) |cell_ref| {
104                 const cp = cell_ref.cell.content.codepoint;
105                 if (cp == 0) {
106@@ -692,7 +684,7 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
107                 try output.append(allocator, ' ');
108             }
109         }
110-        
111+
112         if (row < rows - 1) {
113             try output.appendSlice(allocator, "\r\n");
114         }