repos / zmx

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

commit
24420e4
parent
a166953
author
Eric Bower
date
2025-12-19 13:47:25 -0500 EST
feat: ctrl+b + d to detach

With support to add more commands
1 files changed,  +77, -3
M src/main.zig
+77, -3
  1@@ -532,6 +532,9 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
  2 
  3     const stdin_fd = posix.STDIN_FILENO;
  4 
  5+    // Prefix key state for ctrl+b + <key> bindings
  6+    var prefix_active = false;
  7+
  8     // Make stdin non-blocking
  9     const flags = try posix.fcntl(stdin_fd, posix.F.GETFL, 0);
 10     _ = try posix.fcntl(stdin_fd, posix.F.SETFL, flags | posix.SOCK.NONBLOCK);
 11@@ -583,14 +586,28 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
 12 
 13             if (n_opt) |n| {
 14                 if (n > 0) {
 15-                    // Check for Kitty keyboard protocol escape sequence for Ctrl+\
 16-                    // Format: CSI 92 ; <modifiers> u  where modifiers has Ctrl bit (bit 2) set
 17-                    // Examples: \e[92;5u (basic), \e[92;133u (with event flags)
 18+                    // Check for Kitty keyboard protocol escape sequences
 19                     if (isKittyCtrlBackslash(buf[0..n])) {
 20                         ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
 21                             error.BrokenPipe, error.ConnectionResetByPeer => return,
 22                             else => return err,
 23                         };
 24+                        prefix_active = false;
 25+                        continue;
 26+                    }
 27+
 28+                    if (isKittyCtrlB(buf[0..n])) {
 29+                        prefix_active = true;
 30+                        continue;
 31+                    }
 32+
 33+                    // Handle prefix mode for Kitty 'd' key
 34+                    if (prefix_active and isKittyKey(buf[0..n], 'd')) {
 35+                        ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
 36+                            error.BrokenPipe, error.ConnectionResetByPeer => return,
 37+                            else => return err,
 38+                        };
 39+                        prefix_active = false;
 40                         continue;
 41                     }
 42 
 43@@ -601,6 +618,36 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
 44                                 error.BrokenPipe, error.ConnectionResetByPeer => return,
 45                                 else => return err,
 46                             };
 47+                            prefix_active = false;
 48+                        } else if (buf[i] == 0x02) { // Ctrl+B
 49+                            if (prefix_active) {
 50+                                // Double ctrl+b sends literal ctrl+b
 51+                                ipc.send(client_sock_fd, .Input, &[_]u8{0x02}) catch |err| switch (err) {
 52+                                    error.BrokenPipe, error.ConnectionResetByPeer => return,
 53+                                    else => return err,
 54+                                };
 55+                                prefix_active = false;
 56+                            } else {
 57+                                prefix_active = true;
 58+                            }
 59+                        } else if (prefix_active) {
 60+                            if (buf[i] == 'd') {
 61+                                ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
 62+                                    error.BrokenPipe, error.ConnectionResetByPeer => return,
 63+                                    else => return err,
 64+                                };
 65+                            } else {
 66+                                // Unknown prefix command, forward both ctrl+b and this key
 67+                                ipc.send(client_sock_fd, .Input, &[_]u8{0x02}) catch |err| switch (err) {
 68+                                    error.BrokenPipe, error.ConnectionResetByPeer => return,
 69+                                    else => return err,
 70+                                };
 71+                                ipc.send(client_sock_fd, .Input, buf[i .. i + 1]) catch |err| switch (err) {
 72+                                    error.BrokenPipe, error.ConnectionResetByPeer => return,
 73+                                    else => return err,
 74+                                };
 75+                            }
 76+                            prefix_active = false;
 77                         } else {
 78                             const payload = buf[i .. i + 1];
 79                             ipc.send(client_sock_fd, .Input, payload) catch |err| switch (err) {
 80@@ -1090,3 +1137,30 @@ test "isKittyCtrlBackslash" {
 81     try std.testing.expect(!isKittyCtrlBackslash("\x1b[93;5u"));
 82     try std.testing.expect(!isKittyCtrlBackslash("garbage"));
 83 }
 84+
 85+fn isKittyCtrlB(buf: []const u8) bool {
 86+    return std.mem.indexOf(u8, buf, "\x1b[98;5u") != null or
 87+        std.mem.indexOf(u8, buf, "\x1b[98;133u") != null;
 88+}
 89+
 90+test "isKittyCtrlB" {
 91+    try std.testing.expect(isKittyCtrlB("\x1b[98;5u"));
 92+    try std.testing.expect(isKittyCtrlB("\x1b[98;133u"));
 93+    try std.testing.expect(!isKittyCtrlB("\x1b[98;1u"));
 94+    try std.testing.expect(!isKittyCtrlB("\x1b[99;5u"));
 95+    try std.testing.expect(!isKittyCtrlB("garbage"));
 96+}
 97+
 98+fn isKittyKey(buf: []const u8, key: u8) bool {
 99+    var expected: [16]u8 = undefined;
100+    const seq = std.fmt.bufPrint(&expected, "\x1b[{d}u", .{key}) catch return false;
101+
102+    return std.mem.indexOf(u8, buf, seq) != null;
103+}
104+
105+test "isKittyKey" {
106+    try std.testing.expect(isKittyKey("\x1b[100u", 'd'));
107+    try std.testing.expect(!isKittyKey("\x1b[100;5u", 'd'));
108+    try std.testing.expect(!isKittyKey("\x1b[101u", 'd'));
109+    try std.testing.expect(!isKittyKey("d", 'd'));
110+}