- commit
- fe21350
- parent
- 1136218
- author
- Eric Bower
- date
- 2025-11-24 22:44:58 -0500 EST
feat: ctrl+\ detach client
2 files changed,
+31,
-9
+1,
-1
1@@ -6,6 +6,7 @@ session persistence for terminal processes
2
3 - Persist terminal shell sessions (pty processes)
4 - Ability to attach and detach from a shell session without killing it
5+- Supports all the terminal features that the client's terminal emulator supports
6 - Native terminal scrollback
7 - Manage shell sessions
8 - Multiple clients can connect to the same session
9@@ -13,7 +14,6 @@ session persistence for terminal processes
10 - Re-attaching to a session restores previous terminal state and output
11 - The `daemon` and client processes communicate via a unix socket
12 - This project does **NOT** provide windows, tabs, or window splits
13-- It supports all the terminal features that the client's terminal emulator supports
14 - Works on mac and linux
15
16 ## usage
+30,
-8
1@@ -44,8 +44,6 @@ const Client = struct {
2
3 const Cfg = struct {
4 socket_dir: []const u8 = "/tmp/zmx",
5- prefix_key: []const u8 = "^", // control key
6- detach_key: []const u8 = "\\", // backslash key
7
8 pub fn mkdir(self: *Cfg) !void {
9 std.log.info("creating socket dir: socket_dir={s}", .{self.socket_dir});
10@@ -323,6 +321,8 @@ fn attach(daemon: *Daemon) !void {
11 // Additional granular raw mode settings for precise control
12 // (matches what abduco and shpool do)
13 raw_termios.c_cc[c.VLNEXT] = c._POSIX_VDISABLE; // Disable literal-next (Ctrl-V)
14+ // We want to intercept Ctrl+\ (SIGQUIT) so we can use it as a detach key
15+ raw_termios.c_cc[c.VQUIT] = c._POSIX_VDISABLE; // Disable SIGQUIT (Ctrl+\)
16 raw_termios.c_cc[c.VMIN] = 1; // Minimum chars to read: return after 1 byte
17 raw_termios.c_cc[c.VTIME] = 0; // Read timeout: no timeout, return immediately
18
19@@ -333,10 +333,10 @@ fn attach(daemon: *Daemon) !void {
20 const alt_buffer_seq = "\x1b[?1049h\x1b[H";
21 _ = try posix.write(posix.STDOUT_FILENO, alt_buffer_seq);
22
23- try clientLoop(client_sock);
24+ try clientLoop(daemon.cfg, client_sock);
25 }
26
27-fn clientLoop(client_sock_fd: i32) !void {
28+fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
29 std.log.info("starting client loop client_sock_fd={d}", .{client_sock_fd});
30 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
31 const alloc = std.heap.c_allocator;
32@@ -394,10 +394,32 @@ fn clientLoop(client_sock_fd: i32) !void {
33
34 if (n_opt) |n| {
35 if (n > 0) {
36- ipc.send(client_sock_fd, .Input, buf[0..n]) catch |err| switch (err) {
37- error.BrokenPipe, error.ConnectionResetByPeer => return,
38- else => return err,
39- };
40+ // Check for extended escape sequence for Ctrl+\ (ESC [ 92 ; 5 u)
41+ // This is sent by some terminals (like ghostty/kitty) in CSI u mode
42+ const esc_seq = "\x1b[92;5u";
43+ if (std.mem.indexOf(u8, buf[0..n], esc_seq) != null) {
44+ ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
45+ error.BrokenPipe, error.ConnectionResetByPeer => return,
46+ else => return err,
47+ };
48+ continue;
49+ }
50+
51+ var i: usize = 0;
52+ while (i < n) : (i += 1) {
53+ if (buf[i] == 0x1C) { // Ctrl+\ (File Separator)
54+ ipc.send(client_sock_fd, .Detach, "") catch |err| switch (err) {
55+ error.BrokenPipe, error.ConnectionResetByPeer => return,
56+ else => return err,
57+ };
58+ } else {
59+ const payload = buf[i .. i + 1];
60+ ipc.send(client_sock_fd, .Input, payload) catch |err| switch (err) {
61+ error.BrokenPipe, error.ConnectionResetByPeer => return,
62+ else => return err,
63+ };
64+ }
65+ }
66 } else {
67 // EOF on stdin
68 return;