repos / zmx

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

commit
66c8e0e
parent
bdbf795
author
Eric Bower
date
2026-04-08 20:22:43 -0400 EDT
feat: change pty window size when new leader is set

When we set a new client leader we send a Resize event request from the daemon
to the client.  So now whenever a user types into their stdin we set the new leader
and then resize the window.
2 files changed,  +31, -26
M src/ipc.zig
+1, -0
1@@ -80,6 +80,7 @@ pub fn appendMessage(
2     tag: Tag,
3     data: []const u8,
4 ) !void {
5+    std.log.info("sending ipc message tag={s}", .{@tagName(tag)});
6     const header = Header{
7         .tag = tag,
8         .len = @intCast(data.len),
M src/main.zig
+30, -26
  1@@ -319,11 +319,6 @@ const Cfg = struct {
  2     }
  3 };
  4 
  5-const EnsureSessionResult = struct {
  6-    created: bool,
  7-    is_daemon: bool,
  8-};
  9-
 10 /// Daemon is responsible for managing a zmx session.
 11 ///
 12 /// It holds all the state for a running session.  Instead of a single daemon for all sessions, we
 13@@ -354,6 +349,11 @@ const Daemon = struct {
 14     task_command: ?[]const []const u8 = null,
 15     pty_write_buf: std.ArrayList(u8) = .empty,
 16 
 17+    const EnsureSessionResult = struct {
 18+        created: bool,
 19+        is_daemon: bool,
 20+    };
 21+
 22     pub fn deinit(self: *Daemon) void {
 23         self.clients.deinit(self.alloc);
 24         self.pty_write_buf.deinit(self.alloc);
 25@@ -384,6 +384,15 @@ const Daemon = struct {
 26         return false;
 27     }
 28 
 29+    fn setLeader(self: *Daemon, client: *Client) !void {
 30+        std.log.info("setting new leader client_fd={d}", .{client.socket_fd});
 31+        self.leader_client_fd = client.socket_fd;
 32+        // Send a resize message to the client so it can send us back their window size
 33+        // so we can resize the pty and ghostty state.
 34+        try ipc.appendMessage(self.alloc, &client.write_buf, .Resize, "");
 35+        client.has_pending_output = true;
 36+    }
 37+
 38     /// Runs in the forked child. Either execs or returns an error (caller
 39     /// must exit on error -- returning would fall through to parent code).
 40     fn execChild(self: *Daemon) !noreturn {
 41@@ -598,11 +607,7 @@ const Daemon = struct {
 42         const isNewline = std.mem.indexOfScalar(u8, payload, '\r') != null;
 43         const isUpArrow = std.mem.eql(u8, payload, "\x1b[A") or util.isUpArrow(payload);
 44         if (isNewline or isUpArrow) {
 45-            std.log.info(
 46-                "setting new leader session={s} client_fd={d}",
 47-                .{ self.session_name, client.socket_fd },
 48-            );
 49-            self.leader_client_fd = client.socket_fd;
 50+            try self.setLeader(client);
 51             self.queuePtyInput(payload);
 52             return;
 53         }
 54@@ -623,11 +628,7 @@ const Daemon = struct {
 55             defer client.alloc.free(output);
 56             // if there's no text output then this client is effectively read-only until they type
 57             if (output.len > 0) {
 58-                std.log.info(
 59-                    "setting new leader session={s} client_fd={d}",
 60-                    .{ self.session_name, client.socket_fd },
 61-                );
 62-                self.leader_client_fd = client.socket_fd;
 63+                try self.setLeader(client);
 64                 // new leader is set to this client so send *entire* payload
 65                 self.queuePtyInput(payload);
 66             }
 67@@ -669,11 +670,7 @@ const Daemon = struct {
 68 
 69         // no leader is set so set one
 70         if (self.leader_client_fd == null) {
 71-            std.log.info(
 72-                "setting new leader session={s} client_fd={d}",
 73-                .{ self.session_name, client.socket_fd },
 74-            );
 75-            self.leader_client_fd = client.socket_fd;
 76+            try self.setLeader(client);
 77         }
 78 
 79         // only resize if leader
 80@@ -704,11 +701,7 @@ const Daemon = struct {
 81     ) !void {
 82         if (payload.len != @sizeOf(ipc.Resize)) return;
 83         if (self.leader_client_fd == null) {
 84-            std.log.info(
 85-                "setting new leader session={s} client_fd={d}",
 86-                .{ self.session_name, client.socket_fd },
 87-            );
 88-            self.leader_client_fd = client.socket_fd;
 89+            try self.setLeader(client);
 90         }
 91         // only leader can resize
 92         if (self.leader_client_fd != client.socket_fd) return;
 93@@ -1020,7 +1013,7 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
 94             }
 95         }
 96 
 97-        std.Thread.sleep(3000 * std.time.ns_per_ms);
 98+        std.Thread.sleep(1000 * std.time.ns_per_ms);
 99     }
100 }
101 
102@@ -1529,6 +1522,17 @@ fn clientLoop(client_sock_fd: i32) !void {
103                             try stdout_buf.appendSlice(alloc, msg.payload);
104                         }
105                     },
106+                    .Resize => {
107+                        // daemon is asking for the client's window size usually in response
108+                        // to this client being set as leader.
109+                        const next_size = ipc.getTerminalSize(posix.STDOUT_FILENO);
110+                        try ipc.appendMessage(
111+                            alloc,
112+                            &sock_write_buf,
113+                            .Resize,
114+                            std.mem.asBytes(&next_size),
115+                        );
116+                    },
117                     else => {},
118                 }
119             }