repos / zmx

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

commit
3dcce3d
parent
f15dee4
author
Eric Bower
date
2025-12-19 14:09:29 -0500 EST
refactor: input shortcut handling

Another code clarity change that abstracts handling the shortcuts for
detach
1 files changed,  +97, -57
M src/main.zig
+97, -57
  1@@ -672,74 +672,67 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
  2 
  3             if (n_opt) |n| {
  4                 if (n > 0) {
  5-                    // Check for Kitty keyboard protocol escape sequences
  6-                    if (isKittyCtrlBackslash(buf[0..n])) {
  7-                        ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
  8-                            error.BrokenPipe, error.ConnectionResetByPeer => return,
  9-                            else => return err,
 10-                        };
 11-                        prefix_active = false;
 12-                        continue;
 13-                    }
 14-
 15-                    if (isKittyCtrlB(buf[0..n])) {
 16-                        prefix_active = true;
 17-                        continue;
 18-                    }
 19-
 20-                    // Handle prefix mode for Kitty 'd' key
 21-                    if (prefix_active and isKittyKey(buf[0..n], 'd')) {
 22-                        ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
 23-                            error.BrokenPipe, error.ConnectionResetByPeer => return,
 24-                            else => return err,
 25-                        };
 26-                        prefix_active = false;
 27-                        continue;
 28-                    }
 29-
 30-                    var i: usize = 0;
 31-                    while (i < n) : (i += 1) {
 32-                        if (buf[i] == 0x1C) { // Ctrl+\ (File Separator)
 33+                    // Check for Kitty keyboard protocol sequences first
 34+                    switch (parseStdinInput(buf[0..n], &prefix_active)) {
 35+                        .detach => {
 36                             ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
 37                                 error.BrokenPipe, error.ConnectionResetByPeer => return,
 38                                 else => return err,
 39                             };
 40-                            prefix_active = false;
 41-                        } else if (buf[i] == 0x02) { // Ctrl+B
 42-                            if (prefix_active) {
 43-                                // Double ctrl+b sends literal ctrl+b
 44-                                ipc.send(client_sock_fd, .Input, &[_]u8{0x02}) catch |err| switch (err) {
 45-                                    error.BrokenPipe, error.ConnectionResetByPeer => return,
 46-                                    else => return err,
 47-                                };
 48-                                prefix_active = false;
 49-                            } else {
 50-                                prefix_active = true;
 51-                            }
 52-                        } else if (prefix_active) {
 53-                            if (buf[i] == 'd') {
 54+                            continue;
 55+                        },
 56+                        .send => |data| {
 57+                            ipc.send(client_sock_fd, .Input, data) catch |err| switch (err) {
 58+                                error.BrokenPipe, error.ConnectionResetByPeer => return,
 59+                                else => return err,
 60+                            };
 61+                            continue;
 62+                        },
 63+                        .activate_prefix => continue,
 64+                        .none => {},
 65+                    }
 66+
 67+                    // Process byte-by-byte for non-Kitty input
 68+                    var i: usize = 0;
 69+                    while (i < n) : (i += 1) {
 70+                        const action = parseStdinByte(buf[i], prefix_active);
 71+                        switch (action) {
 72+                            .detach => {
 73                                 ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
 74                                     error.BrokenPipe, error.ConnectionResetByPeer => return,
 75                                     else => return err,
 76                                 };
 77-                            } else {
 78-                                // Unknown prefix command, forward both ctrl+b and this key
 79-                                ipc.send(client_sock_fd, .Input, &[_]u8{0x02}) catch |err| switch (err) {
 80-                                    error.BrokenPipe, error.ConnectionResetByPeer => return,
 81-                                    else => return err,
 82-                                };
 83-                                ipc.send(client_sock_fd, .Input, buf[i .. i + 1]) catch |err| switch (err) {
 84+                                prefix_active = false;
 85+                            },
 86+                            .send => |data| {
 87+                                ipc.send(client_sock_fd, .Input, data) catch |err| switch (err) {
 88                                     error.BrokenPipe, error.ConnectionResetByPeer => return,
 89                                     else => return err,
 90                                 };
 91-                            }
 92-                            prefix_active = false;
 93-                        } else {
 94-                            const payload = buf[i .. i + 1];
 95-                            ipc.send(client_sock_fd, .Input, payload) catch |err| switch (err) {
 96-                                error.BrokenPipe, error.ConnectionResetByPeer => return,
 97-                                else => return err,
 98-                            };
 99+                                prefix_active = false;
100+                            },
101+                            .activate_prefix => {
102+                                prefix_active = true;
103+                            },
104+                            .none => {
105+                                if (prefix_active) {
106+                                    // Unknown prefix command, forward both ctrl+b and this key
107+                                    ipc.send(client_sock_fd, .Input, &[_]u8{0x02}) catch |err| switch (err) {
108+                                        error.BrokenPipe, error.ConnectionResetByPeer => return,
109+                                        else => return err,
110+                                    };
111+                                    ipc.send(client_sock_fd, .Input, buf[i .. i + 1]) catch |err| switch (err) {
112+                                        error.BrokenPipe, error.ConnectionResetByPeer => return,
113+                                        else => return err,
114+                                    };
115+                                    prefix_active = false;
116+                                } else {
117+                                    ipc.send(client_sock_fd, .Input, buf[i .. i + 1]) catch |err| switch (err) {
118+                                        error.BrokenPipe, error.ConnectionResetByPeer => return,
119+                                        else => return err,
120+                                    };
121+                                }
122+                            },
123                         }
124                     }
125                 } else {
126@@ -1071,6 +1064,53 @@ fn probeSession(alloc: std.mem.Allocator, socket_path: []const u8) SessionProbeE
127     return error.Unexpected;
128 }
129 
130+const InputAction = union(enum) {
131+    send: []const u8,
132+    detach,
133+    activate_prefix,
134+    none,
135+};
136+
137+fn parseStdinByte(byte: u8, prefix_active: bool) InputAction {
138+    if (byte == 0x1C) { // Ctrl+\ (File Separator)
139+        return .detach;
140+    } else if (byte == 0x02) { // Ctrl+B
141+        if (prefix_active) {
142+            return .{ .send = &[_]u8{0x02} };
143+        } else {
144+            return .activate_prefix;
145+        }
146+    } else if (prefix_active) {
147+        if (byte == 'd') {
148+            return .detach;
149+        } else {
150+            return .none; // Unknown prefix command - caller handles forwarding
151+        }
152+    }
153+    return .none; // Regular byte - caller handles forwarding
154+}
155+
156+fn parseStdinInput(buf: []const u8, prefix_active: *bool) InputAction {
157+    // Check for Kitty keyboard protocol escape sequences first
158+    if (isKittyCtrlBackslash(buf)) {
159+        prefix_active.* = false;
160+        return .detach;
161+    }
162+
163+    if (isKittyCtrlB(buf)) {
164+        prefix_active.* = true;
165+        return .activate_prefix;
166+    }
167+
168+    // Handle prefix mode for Kitty 'd' key
169+    if (prefix_active.* and isKittyKey(buf, 'd')) {
170+        prefix_active.* = false;
171+        return .detach;
172+    }
173+
174+    return .none; // Not a special sequence
175+}
176+
177 fn cleanupStaleSocket(dir: std.fs.Dir, session_name: []const u8) void {
178     std.log.warn("stale socket found, cleaning up session={s}", .{session_name});
179     dir.deleteFile(session_name) catch |err| {