repos / zmx

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

commit
f15dee4
parent
24420e4
author
Eric Bower
date
2025-12-19 14:03:23 -0500 EST
refactor: daemonLoop and terminal serialization

Code cleanup to make the daemonLoop and ghostty interaction easier to
understand
1 files changed,  +124, -85
M src/main.zig
+124, -85
  1@@ -157,6 +157,92 @@ const Daemon = struct {
  2         }
  3         return false;
  4     }
  5+
  6+    pub fn handleInput(self: *Daemon, pty_fd: i32, payload: []const u8) !void {
  7+        _ = self;
  8+        if (payload.len > 0) {
  9+            _ = try posix.write(pty_fd, payload);
 10+        }
 11+    }
 12+
 13+    pub fn handleInit(
 14+        self: *Daemon,
 15+        client: *Client,
 16+        pty_fd: i32,
 17+        term: *ghostty_vt.Terminal,
 18+        payload: []const u8,
 19+    ) !void {
 20+        if (payload.len != @sizeOf(ipc.Resize)) return;
 21+
 22+        const resize = std.mem.bytesToValue(ipc.Resize, payload);
 23+        var ws: c.struct_winsize = .{
 24+            .ws_row = resize.rows,
 25+            .ws_col = resize.cols,
 26+            .ws_xpixel = 0,
 27+            .ws_ypixel = 0,
 28+        };
 29+        _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
 30+        try term.resize(self.alloc, resize.cols, resize.rows);
 31+
 32+        std.log.debug("init resize rows={d} cols={d}", .{ resize.rows, resize.cols });
 33+
 34+        if (self.has_pty_output) {
 35+            if (serializeTerminalState(self.alloc, term)) |term_output| {
 36+                defer self.alloc.free(term_output);
 37+                ipc.appendMessage(self.alloc, &client.write_buf, .Output, term_output) catch |err| {
 38+                    std.log.warn("failed to buffer terminal state for client err={s}", .{@errorName(err)});
 39+                };
 40+                client.has_pending_output = true;
 41+            }
 42+        }
 43+    }
 44+
 45+    pub fn handleResize(self: *Daemon, pty_fd: i32, term: *ghostty_vt.Terminal, payload: []const u8) !void {
 46+        if (payload.len != @sizeOf(ipc.Resize)) return;
 47+
 48+        const resize = std.mem.bytesToValue(ipc.Resize, payload);
 49+        var ws: c.struct_winsize = .{
 50+            .ws_row = resize.rows,
 51+            .ws_col = resize.cols,
 52+            .ws_xpixel = 0,
 53+            .ws_ypixel = 0,
 54+        };
 55+        _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
 56+        try term.resize(self.alloc, resize.cols, resize.rows);
 57+        std.log.debug("resize rows={d} cols={d}", .{ resize.rows, resize.cols });
 58+    }
 59+
 60+    pub fn handleDetach(self: *Daemon, client: *Client, i: usize) void {
 61+        std.log.info("client detach fd={d}", .{client.socket_fd});
 62+        _ = self.closeClient(client, i, false);
 63+    }
 64+
 65+    pub fn handleDetachAll(self: *Daemon) void {
 66+        std.log.info("detach all clients={d}", .{self.clients.items.len});
 67+        for (self.clients.items) |client_to_close| {
 68+            client_to_close.deinit();
 69+            self.alloc.destroy(client_to_close);
 70+        }
 71+        self.clients.clearRetainingCapacity();
 72+    }
 73+
 74+    pub fn handleKill(self: *Daemon) void {
 75+        std.log.info("kill received session={s}", .{self.session_name});
 76+        posix.kill(self.pid, posix.SIG.TERM) catch |err| {
 77+            std.log.warn("failed to send SIGTERM to pty child err={s}", .{@errorName(err)});
 78+        };
 79+        self.shutdown();
 80+    }
 81+
 82+    pub fn handleInfo(self: *Daemon, client: *Client) !void {
 83+        const clients_len = self.clients.items.len - 1;
 84+        const info = ipc.Info{
 85+            .clients_len = clients_len,
 86+            .pid = self.pid,
 87+        };
 88+        try ipc.appendMessage(self.alloc, &client.write_buf, .Info, std.mem.asBytes(&info));
 89+        client.has_pending_output = true;
 90+    }
 91 };
 92 
 93 pub fn main() !void {
 94@@ -832,101 +918,24 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 95 
 96                 while (client.read_buf.next()) |msg| {
 97                     switch (msg.header.tag) {
 98-                        .Input => {
 99-                            if (msg.payload.len > 0) {
100-                                _ = try posix.write(pty_fd, msg.payload);
101-                            }
102-                        },
103-                        .Init => {
104-                            if (msg.payload.len == @sizeOf(ipc.Resize)) {
105-                                const resize = std.mem.bytesToValue(ipc.Resize, msg.payload);
106-                                var ws: c.struct_winsize = .{
107-                                    .ws_row = resize.rows,
108-                                    .ws_col = resize.cols,
109-                                    .ws_xpixel = 0,
110-                                    .ws_ypixel = 0,
111-                                };
112-                                _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
113-                                try term.resize(daemon.alloc, resize.cols, resize.rows);
114-
115-                                std.log.debug("init resize rows={d} cols={d}", .{ resize.rows, resize.cols });
116-
117-                                // Only send terminal state if there's been PTY output (skip on first attach)
118-                                if (daemon.has_pty_output) {
119-                                    var builder: std.Io.Writer.Allocating = .init(daemon.alloc);
120-                                    defer builder.deinit();
121-                                    var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(&term, .vt);
122-                                    term_formatter.content = .{ .selection = null };
123-                                    term_formatter.extra = .{
124-                                        .palette = false, // Don't override host terminal's palette
125-                                        .modes = true,
126-                                        .scrolling_region = true,
127-                                        .tabstops = true,
128-                                        .pwd = true,
129-                                        .keyboard = true,
130-                                        .screen = .all,
131-                                    };
132-                                    term_formatter.format(&builder.writer) catch |err| {
133-                                        std.log.warn("failed to format terminal state err={s}", .{@errorName(err)});
134-                                    };
135-                                    const term_output = builder.writer.buffered();
136-                                    if (term_output.len > 0) {
137-                                        ipc.appendMessage(daemon.alloc, &client.write_buf, .Output, term_output) catch |err| {
138-                                            std.log.warn("failed to buffer terminal state for client err={s}", .{@errorName(err)});
139-                                        };
140-                                        client.has_pending_output = true;
141-                                    }
142-                                }
143-                            }
144-                        },
145-                        .Resize => {
146-                            if (msg.payload.len == @sizeOf(ipc.Resize)) {
147-                                const resize = std.mem.bytesToValue(ipc.Resize, msg.payload);
148-                                var ws: c.struct_winsize = .{
149-                                    .ws_row = resize.rows,
150-                                    .ws_col = resize.cols,
151-                                    .ws_xpixel = 0,
152-                                    .ws_ypixel = 0,
153-                                };
154-                                _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
155-                                try term.resize(daemon.alloc, resize.cols, resize.rows);
156-                                std.log.debug("resize rows={d} cols={d}", .{ resize.rows, resize.cols });
157-                            }
158-                        },
159+                        .Input => try daemon.handleInput(pty_fd, msg.payload),
160+                        .Init => try daemon.handleInit(client, pty_fd, &term, msg.payload),
161+                        .Resize => try daemon.handleResize(pty_fd, &term, msg.payload),
162                         .Detach => {
163-                            std.log.info("client detach fd={d}", .{client.socket_fd});
164-                            _ = daemon.closeClient(client, i, false);
165+                            daemon.handleDetach(client, i);
166                             break :clients_loop;
167                         },
168                         .DetachAll => {
169-                            std.log.info("detach all clients={d}", .{daemon.clients.items.len});
170-                            for (daemon.clients.items) |client_to_close| {
171-                                client_to_close.deinit();
172-                                daemon.alloc.destroy(client_to_close);
173-                            }
174-                            daemon.clients.clearRetainingCapacity();
175+                            daemon.handleDetachAll();
176                             break :clients_loop;
177                         },
178                         .Kill => {
179-                            std.log.info("kill received session={s}", .{daemon.session_name});
180-                            posix.kill(daemon.pid, posix.SIG.TERM) catch |err| {
181-                                std.log.warn("failed to send SIGTERM to pty child err={s}", .{@errorName(err)});
182-                            };
183-                            daemon.shutdown();
184+                            daemon.handleKill();
185                             should_exit = true;
186                             break :clients_loop;
187                         },
188-                        .Info => {
189-                            // subtract current client since it's just fetching info
190-                            const clients_len = daemon.clients.items.len - 1;
191-                            const info = ipc.Info{
192-                                .clients_len = clients_len,
193-                                .pid = daemon.pid,
194-                            };
195-                            try ipc.appendMessage(daemon.alloc, &client.write_buf, .Info, std.mem.asBytes(&info));
196-                            client.has_pending_output = true;
197-                        },
198-                        .Output => {}, // Clients shouldn't send output
199+                        .Info => try daemon.handleInfo(client),
200+                        .Output => {},
201                     }
202                 }
203             }
204@@ -1138,6 +1147,36 @@ test "isKittyCtrlBackslash" {
205     try std.testing.expect(!isKittyCtrlBackslash("garbage"));
206 }
207 
208+fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal) ?[]const u8 {
209+    var builder: std.Io.Writer.Allocating = .init(alloc);
210+    defer builder.deinit();
211+
212+    var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, .vt);
213+    term_formatter.content = .{ .selection = null };
214+    term_formatter.extra = .{
215+        .palette = false,
216+        .modes = true,
217+        .scrolling_region = true,
218+        .tabstops = true,
219+        .pwd = true,
220+        .keyboard = true,
221+        .screen = .all,
222+    };
223+
224+    term_formatter.format(&builder.writer) catch |err| {
225+        std.log.warn("failed to format terminal state err={s}", .{@errorName(err)});
226+        return null;
227+    };
228+
229+    const output = builder.writer.buffered();
230+    if (output.len == 0) return null;
231+
232+    return alloc.dupe(u8, output) catch |err| {
233+        std.log.warn("failed to allocate terminal state err={s}", .{@errorName(err)});
234+        return null;
235+    };
236+}
237+
238 fn isKittyCtrlB(buf: []const u8) bool {
239     return std.mem.indexOf(u8, buf, "\x1b[98;5u") != null or
240         std.mem.indexOf(u8, buf, "\x1b[98;133u") != null;