- commit
- 927bc64
- parent
- dce3f04
- author
- Eric Bower
- date
- 2025-12-27 19:12:56 -0500 EST
refactor: remove ctrl+b + d detach shortcut This feature was annoying to implement because we can't simply check for a single byte, we first have to detect if ctrl+b was pressed then check for d. In the end I don't like the complexity it introduces and it seems pretty low value since we should recommend users simply close their terminal window.
3 files changed,
+16,
-126
+0,
-1
1@@ -11,7 +11,6 @@ Use spec: https://common-changelog.org/
2
3 ### Changed
4
5-- Use `ctrl+b + d` to detach from session instead of `ctrl+\` (deprecated)
6 - Updated `ghostty-vt` to latest HEAD
7
8 ### Fixed
+7,
-3
1@@ -70,9 +70,13 @@ zmx attach build make -j8 # run a build, reattach to check progress
2 zmx attach mux dvtm # run a multiplexer inside zmx
3 ```
4
5+### detach
6+
7+While we try our best to support detaching from the terminal session using multiple mechanisms (e.g. shortcut, detach command) we find ourselves simply closing terminal windows when we want to detach from a `zmx` session. Whenever you attach to a `zmx` session we make sure that it stays alive so explicitly detaching feels unnecessary unless for some reason you need to reuse the same exact terminal window.
8+
9 ## shell prompt
10
11-When you attach to a zmx session, we don't provide any indication that you are inside `zmx`. We do provide an environment variable `ZMX_SESSION` which contains the session name.
12+When you attach to a `zmx` session, we don't provide any indication that you are inside `zmx`. We do provide an environment variable `ZMX_SESSION` which contains the session name.
13
14 We recommend checking for that env var inside your prompt and displaying some indication there.
15
16@@ -106,13 +110,13 @@ PROMPT="${ZMX_SESSION:+[$ZMX_SESSION]} $BASE_PROMPT"
17
18 ## philosophy
19
20-The entire argument for `zmx` instead of something like `tmux` that has windows, panes, splits, etc. is that job should be handled by your os window manager. By using something like `tmux` you now have redundent functionality in your dev stack: a window manager for your os and a window manager for your terminal. Further, in order to use modern terminal features, your terminal emulator **and** `tmux` need to have support for them. This holds back the terminal enthusiast community and feature development.
21+The entire argument for `zmx` instead of something like `tmux` that has windows, panes, splits, etc. is that job should be handled by your os window manager. By using something like `tmux` you now have redundant functionality in your dev stack: a window manager for your os and a window manager for your terminal. Further, in order to use modern terminal features, your terminal emulator **and** `tmux` need to have support for them. This holds back the terminal enthusiast community and feature development.
22
23 Instead, this tool specifically focuses on session persistence and defers window management to your os wm.
24
25 ## ssh workflow
26
27-Using `zmx` with `ssh` is a first-class citizen. Instead of `ssh`ing into your remote system with a single terminal and `n` tmux panes, you open `n` terminals and run `ssh` for all of them. This might sound tedious, but there are tools to make this a delightful workflow.
28+Using `zmx` with `ssh` is a first-class citizen. Instead of using `ssh` to remote into your system with a single terminal and `n` tmux panes, you open `n` terminals and run `ssh` for all of them. This might sound tedious, but there are tools to make this a delightful workflow.
29
30 First, create an `ssh` config entry for your remote dev server:
31
+9,
-122
1@@ -707,9 +707,6 @@ 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@@ -763,47 +760,11 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
12
13 if (n_opt) |n| {
14 if (n > 0) {
15- // Check for Kitty keyboard protocol sequences first
16- switch (parseStdinInput(buf[0..n], &prefix_active)) {
17- .detach => {
18- try ipc.appendMessage(alloc, &sock_write_buf, .Detach, "");
19- continue;
20- },
21- .send => |data| {
22- try ipc.appendMessage(alloc, &sock_write_buf, .Input, data);
23- continue;
24- },
25- .activate_prefix => continue,
26- .none => {},
27- }
28-
29- // Process byte-by-byte for non-Kitty input
30- var i: usize = 0;
31- while (i < n) : (i += 1) {
32- const action = parseStdinByte(buf[i], prefix_active);
33- switch (action) {
34- .detach => {
35- try ipc.appendMessage(alloc, &sock_write_buf, .Detach, "");
36- prefix_active = false;
37- },
38- .send => |data| {
39- try ipc.appendMessage(alloc, &sock_write_buf, .Input, data);
40- prefix_active = false;
41- },
42- .activate_prefix => {
43- prefix_active = true;
44- },
45- .none => {
46- if (prefix_active) {
47- // Unknown prefix command, forward both ctrl+b and this key
48- try ipc.appendMessage(alloc, &sock_write_buf, .Input, &[_]u8{0x02});
49- try ipc.appendMessage(alloc, &sock_write_buf, .Input, buf[i .. i + 1]);
50- prefix_active = false;
51- } else {
52- try ipc.appendMessage(alloc, &sock_write_buf, .Input, buf[i .. i + 1]);
53- }
54- },
55- }
56+ // Check for detach sequences (ctrl+\ as first byte or Kitty escape sequence)
57+ if (buf[0] == 0x1C or isKittyCtrlBackslash(buf[0..n])) {
58+ try ipc.appendMessage(alloc, &sock_write_buf, .Detach, "");
59+ } else {
60+ try ipc.appendMessage(alloc, &sock_write_buf, .Input, buf[0..n]);
61 }
62 } else {
63 // EOF on stdin
64@@ -1151,53 +1112,6 @@ fn probeSession(alloc: std.mem.Allocator, socket_path: []const u8) SessionProbeE
65 return error.Unexpected;
66 }
67
68-const InputAction = union(enum) {
69- send: []const u8,
70- detach,
71- activate_prefix,
72- none,
73-};
74-
75-fn parseStdinByte(byte: u8, prefix_active: bool) InputAction {
76- if (byte == 0x1C) { // Ctrl+\ (File Separator)
77- return .detach;
78- } else if (byte == 0x02) { // Ctrl+B
79- if (prefix_active) {
80- return .{ .send = &[_]u8{0x02} };
81- } else {
82- return .activate_prefix;
83- }
84- } else if (prefix_active) {
85- if (byte == 'd') {
86- return .detach;
87- } else {
88- return .none; // Unknown prefix command - caller handles forwarding
89- }
90- }
91- return .none; // Regular byte - caller handles forwarding
92-}
93-
94-fn parseStdinInput(buf: []const u8, prefix_active: *bool) InputAction {
95- // Check for Kitty keyboard protocol escape sequences first
96- if (isKittyCtrlBackslash(buf)) {
97- prefix_active.* = false;
98- return .detach;
99- }
100-
101- if (isKittyCtrlB(buf)) {
102- prefix_active.* = true;
103- return .activate_prefix;
104- }
105-
106- // Handle prefix mode for Kitty 'd' key
107- if (prefix_active.* and isKittyKey(buf, 'd')) {
108- prefix_active.* = false;
109- return .detach;
110- }
111-
112- return .none; // Not a special sequence
113-}
114-
115 fn cleanupStaleSocket(dir: std.fs.Dir, session_name: []const u8) void {
116 std.log.warn("stale socket found, cleaning up session={s}", .{session_name});
117 dir.deleteFile(session_name) catch |err| {
118@@ -1260,17 +1174,17 @@ fn getTerminalSize(fd: i32) ipc.Resize {
119 }
120
121 /// Detects Kitty keyboard protocol escape sequence for Ctrl+\
122-/// Common sequences: \e[92;5u (basic), \e[92;133u (with event flags)
123+/// 92 = backslash, 5 = ctrl modifier, :1 = key press event
124 fn isKittyCtrlBackslash(buf: []const u8) bool {
125 return std.mem.indexOf(u8, buf, "\x1b[92;5u") != null or
126- std.mem.indexOf(u8, buf, "\x1b[92;133u") != null;
127+ std.mem.indexOf(u8, buf, "\x1b[92;5:1u") != null;
128 }
129
130 test "isKittyCtrlBackslash" {
131 try std.testing.expect(isKittyCtrlBackslash("\x1b[92;5u"));
132- try std.testing.expect(isKittyCtrlBackslash("\x1b[92;133u"));
133+ try std.testing.expect(isKittyCtrlBackslash("\x1b[92;5:1u"));
134+ try std.testing.expect(!isKittyCtrlBackslash("\x1b[92;5:3u"));
135 try std.testing.expect(!isKittyCtrlBackslash("\x1b[92;1u"));
136- try std.testing.expect(!isKittyCtrlBackslash("\x1b[93;5u"));
137 try std.testing.expect(!isKittyCtrlBackslash("garbage"));
138 }
139
140@@ -1325,30 +1239,3 @@ fn serializeTerminalPlainText(alloc: std.mem.Allocator, term: *ghostty_vt.Termin
141 return null;
142 };
143 }
144-
145-fn isKittyCtrlB(buf: []const u8) bool {
146- return std.mem.indexOf(u8, buf, "\x1b[98;5u") != null or
147- std.mem.indexOf(u8, buf, "\x1b[98;133u") != null;
148-}
149-
150-test "isKittyCtrlB" {
151- try std.testing.expect(isKittyCtrlB("\x1b[98;5u"));
152- try std.testing.expect(isKittyCtrlB("\x1b[98;133u"));
153- try std.testing.expect(!isKittyCtrlB("\x1b[98;1u"));
154- try std.testing.expect(!isKittyCtrlB("\x1b[99;5u"));
155- try std.testing.expect(!isKittyCtrlB("garbage"));
156-}
157-
158-fn isKittyKey(buf: []const u8, key: u8) bool {
159- var expected: [16]u8 = undefined;
160- const seq = std.fmt.bufPrint(&expected, "\x1b[{d}u", .{key}) catch return false;
161-
162- return std.mem.indexOf(u8, buf, seq) != null;
163-}
164-
165-test "isKittyKey" {
166- try std.testing.expect(isKittyKey("\x1b[100u", 'd'));
167- try std.testing.expect(!isKittyKey("\x1b[100;5u", 'd'));
168- try std.testing.expect(!isKittyKey("\x1b[101u", 'd'));
169- try std.testing.expect(!isKittyKey("d", 'd'));
170-}