repos / zmx

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

commit
23203f8
parent
659cf8e
author
Eric Bower
date
2025-10-12 21:07:15 -0400 EDT
refactor: resize ghostty term on attach
4 files changed,  +33, -3
M specs/protocol.md
+2, -0
1@@ -69,6 +69,8 @@ Responses are sent from the daemon to the client in response to a request. Every
2 - **Request Type**: `attach_session_request`
3 - **Request Payload**:
4     - `session_name`: string
5+    - `rows`: u16 (terminal height in rows)
6+    - `cols`: u16 (terminal width in columns)
7 
8 - **Direction**: Daemon -> Client
9 - **Response Type**: `attach_session_response`
M src/attach.zig
+12, -1
 1@@ -8,6 +8,7 @@ const protocol = @import("protocol.zig");
 2 
 3 const c = @cImport({
 4     @cInclude("termios.h");
 5+    @cInclude("sys/ioctl.h");
 6 });
 7 
 8 const Context = struct {
 9@@ -98,7 +99,17 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
10         .config = config,
11     };
12 
13-    const request_payload = protocol.AttachSessionRequest{ .session_name = session_name };
14+    // Get terminal size
15+    var ws: c.struct_winsize = undefined;
16+    const result = c.ioctl(posix.STDOUT_FILENO, c.TIOCGWINSZ, &ws);
17+    const rows: u16 = if (result == 0) ws.ws_row else 24;
18+    const cols: u16 = if (result == 0) ws.ws_col else 80;
19+
20+    const request_payload = protocol.AttachSessionRequest{
21+        .session_name = session_name,
22+        .rows = rows,
23+        .cols = cols,
24+    };
25     var out: std.io.Writer.Allocating = .init(allocator);
26     defer out.deinit();
27 
M src/daemon.zig
+17, -2
 1@@ -305,7 +305,7 @@ fn handleMessage(client: *Client, data: []const u8) !void {
 2             const parsed = try protocol.parseMessage(protocol.AttachSessionRequest, client.allocator, data);
 3             defer parsed.deinit();
 4             std.debug.print("Handling attach request for session: {s}\n", .{parsed.value.payload.session_name});
 5-            try handleAttachSession(client.server_ctx, client, parsed.value.payload.session_name);
 6+            try handleAttachSession(client.server_ctx, client, parsed.value.payload.session_name, parsed.value.payload.rows, parsed.value.payload.cols);
 7         },
 8         .detach_session_request => {
 9             const parsed = try protocol.parseMessage(protocol.DetachSessionRequest, client.allocator, data);
10@@ -537,7 +537,7 @@ fn handleListSessions(ctx: *ServerContext, client: *Client) !void {
11     _ = written;
12 }
13 
14-fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []const u8) !void {
15+fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []const u8, rows: u16, cols: u16) !void {
16     // Check if session already exists
17     const is_reattach = ctx.sessions.contains(session_name);
18     const session = if (is_reattach) blk: {
19@@ -551,6 +551,21 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
20         break :blk new_session;
21     };
22 
23+    // Update libghostty-vt terminal size
24+    try session.vt.resize(session.allocator, cols, rows);
25+
26+    // Update PTY window size
27+    var ws = c.struct_winsize{
28+        .ws_row = rows,
29+        .ws_col = cols,
30+        .ws_xpixel = 0,
31+        .ws_ypixel = 0,
32+    };
33+    const result = c.ioctl(session.pty_master_fd, c.TIOCSWINSZ, &ws);
34+    if (result < 0) {
35+        return error.IoctlFailed;
36+    }
37+
38     // Mark client as attached
39     client.attached_session = session.name;
40     try session.attached_clients.put(client.fd, {});
M src/protocol.zig
+2, -0
1@@ -63,6 +63,8 @@ pub const MessageType = enum {
2 // Typed payload structs for requests
3 pub const AttachSessionRequest = struct {
4     session_name: []const u8,
5+    rows: u16,
6+    cols: u16,
7 };
8 
9 pub const DetachSessionRequest = struct {