repos / zmx

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

commit
d12b7cc
parent
954f8db
author
Eric Bower
date
2026-03-03 20:22:16 -0500 EST
fix(run): when no client is attached, send DA response query from daemon
1 files changed,  +49, -0
M src/main.zig
+49, -0
 1@@ -1322,6 +1322,46 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
 2     }
 3 }
 4 
 5+const DA1_QUERY = "\x1b[c";
 6+const DA1_QUERY_EXPLICIT = "\x1b[0c";
 7+const DA2_QUERY = "\x1b[>c";
 8+const DA2_QUERY_EXPLICIT = "\x1b[>0c";
 9+const DA1_RESPONSE = "\x1b[?62;22c";
10+const DA2_RESPONSE = "\x1b[>1;10;0c";
11+
12+fn respondToDeviceAttributes(pty_fd: i32, data: []const u8) void {
13+    // Scan for DA queries in PTY output and respond on behalf of the terminal.
14+    // This handles the case where no client is attached (e.g. zmx run)
15+    // and the shell (e.g. fish) sends a DA query that would otherwise go unanswered.
16+    //
17+    // DA1 query: ESC [ c  or  ESC [ 0 c
18+    // DA2 query: ESC [ > c  or  ESC [ > 0 c
19+    // DA1 response (from terminal): ESC [ ? ... c  (has '?' after '[')
20+    //
21+    // We must NOT match DA responses (which contain '?') as queries.
22+    var i: usize = 0;
23+    while (i < data.len) {
24+        if (data[i] == '\x1b' and i + 1 < data.len and data[i + 1] == '[') {
25+            // Skip DA responses which have '?' after CSI
26+            if (i + 2 < data.len and data[i + 2] == '?') {
27+                i += 3;
28+                continue;
29+            }
30+            if (matchSeq(data[i..], DA2_QUERY) or matchSeq(data[i..], DA2_QUERY_EXPLICIT)) {
31+                _ = posix.write(pty_fd, DA2_RESPONSE) catch {};
32+            } else if (matchSeq(data[i..], DA1_QUERY) or matchSeq(data[i..], DA1_QUERY_EXPLICIT)) {
33+                _ = posix.write(pty_fd, DA1_RESPONSE) catch {};
34+            }
35+        }
36+        i += 1;
37+    }
38+}
39+
40+fn matchSeq(data: []const u8, seq: []const u8) bool {
41+    if (data.len < seq.len) return false;
42+    return std.mem.eql(u8, data[0..seq.len], seq);
43+}
44+
45 fn findTaskExitMarker(output: []const u8) ?u8 {
46     const marker = "ZMX_TASK_COMPLETED:";
47 
48@@ -1436,6 +1476,15 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
49                     try vt_stream.nextSlice(buf[0..n]);
50                     daemon.has_pty_output = true;
51 
52+                    // When no clients are attached, respond to terminal
53+                    // queries (e.g. DA1/DA2) on behalf of the terminal.
54+                    // This prevents shells like from fish from waiting 2s
55+                    // and then sending a no DA query response warning because
56+                    // there's no client terminal to respond to the query.
57+                    if (daemon.clients.items.len == 0) {
58+                        respondToDeviceAttributes(pty_fd, buf[0..n]);
59+                    }
60+
61                     // In run mode, scan output for exit code marker
62                     if (daemon.is_task_mode and daemon.task_exit_code == null) {
63                         if (findTaskExitMarker(buf[0..n])) |exit_code| {