Commit d12b7cc

Eric Bower  ·  2026-03-03 20:22:16 -0500 EST
parent 954f8db
fix(run): when no client is attached, send DA response query from daemon
1 files changed,  +49, -0
+49, -0
......@@ -1322,6 +1322,46 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
13221322 }
13231323 }
13241324
1325+const DA1_QUERY = "\x1b[c";
1326+const DA1_QUERY_EXPLICIT = "\x1b[0c";
1327+const DA2_QUERY = "\x1b[>c";
1328+const DA2_QUERY_EXPLICIT = "\x1b[>0c";
1329+const DA1_RESPONSE = "\x1b[?62;22c";
1330+const DA2_RESPONSE = "\x1b[>1;10;0c";
1331+
1332+fn respondToDeviceAttributes(pty_fd: i32, data: []const u8) void {
1333+ // Scan for DA queries in PTY output and respond on behalf of the terminal.
1334+ // This handles the case where no client is attached (e.g. zmx run)
1335+ // and the shell (e.g. fish) sends a DA query that would otherwise go unanswered.
1336+ //
1337+ // DA1 query: ESC [ c or ESC [ 0 c
1338+ // DA2 query: ESC [ > c or ESC [ > 0 c
1339+ // DA1 response (from terminal): ESC [ ? ... c (has '?' after '[')
1340+ //
1341+ // We must NOT match DA responses (which contain '?') as queries.
1342+ var i: usize = 0;
1343+ while (i < data.len) {
1344+ if (data[i] == '\x1b' and i + 1 < data.len and data[i + 1] == '[') {
1345+ // Skip DA responses which have '?' after CSI
1346+ if (i + 2 < data.len and data[i + 2] == '?') {
1347+ i += 3;
1348+ continue;
1349+ }
1350+ if (matchSeq(data[i..], DA2_QUERY) or matchSeq(data[i..], DA2_QUERY_EXPLICIT)) {
1351+ _ = posix.write(pty_fd, DA2_RESPONSE) catch {};
1352+ } else if (matchSeq(data[i..], DA1_QUERY) or matchSeq(data[i..], DA1_QUERY_EXPLICIT)) {
1353+ _ = posix.write(pty_fd, DA1_RESPONSE) catch {};
1354+ }
1355+ }
1356+ i += 1;
1357+ }
1358+}
1359+
1360+fn matchSeq(data: []const u8, seq: []const u8) bool {
1361+ if (data.len < seq.len) return false;
1362+ return std.mem.eql(u8, data[0..seq.len], seq);
1363+}
1364+
13251365 fn findTaskExitMarker(output: []const u8) ?u8 {
13261366 const marker = "ZMX_TASK_COMPLETED:";
13271367
......@@ -1436,6 +1476,15 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
14361476 try vt_stream.nextSlice(buf[0..n]);
14371477 daemon.has_pty_output = true;
14381478
1479+ // When no clients are attached, respond to terminal
1480+ // queries (e.g. DA1/DA2) on behalf of the terminal.
1481+ // This prevents shells like from fish from waiting 2s
1482+ // and then sending a no DA query response warning because
1483+ // there's no client terminal to respond to the query.
1484+ if (daemon.clients.items.len == 0) {
1485+ respondToDeviceAttributes(pty_fd, buf[0..n]);
1486+ }
1487+
14391488 // In run mode, scan output for exit code marker
14401489 if (daemon.is_task_mode and daemon.task_exit_code == null) {
14411490 if (findTaskExitMarker(buf[0..n])) |exit_code| {