repos / zmx

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

commit
acfa1ec
parent
8560c39
author
Eric Bower
date
2025-10-13 11:31:13 -0400 EDT
fix: respond to Device Attribute queries to prevent fish shell warning

Fish shell sends a Primary Device Attribute (DA1) query (ESC [ c) when
it starts to detect terminal capabilities. Previously, zmx forwarded
this query to the PTY but never sent a response back to the client,
causing fish to wait 2 seconds and display a terminal compatibility
warning.

This commit intercepts DA queries in handlePtyInput() before they reach
the PTY and responds directly to the client with VT220 capabilities:
- Primary DA (ESC [ c): VT220 with color (22) and clipboard (52)
- Secondary DA (ESC [ > c): Terminal version info

The response format matches ghostty's deviceAttributes implementation.

Fixes the "fish could not read response to Primary Device Attribute
query" warning on first attach.
1 files changed,  +32, -0
M src/daemon.zig
+32, -0
 1@@ -613,6 +613,25 @@ fn handlePtyInput(client: *Client, text: []const u8) !void {
 2 
 3     std.debug.print("Writing {d} bytes to PTY fd={d}\n", .{ text.len, session.pty_master_fd });
 4 
 5+    // Intercept Device Attribute queries and respond directly
 6+    if (respondToDeviceAttributes(client, text)) |response| {
 7+        // Send response back to client instead of PTY
 8+        const header = protocol.FrameHeader{
 9+            .length = @intCast(response.len),
10+            .frame_type = @intFromEnum(protocol.FrameType.pty_binary),
11+        };
12+
13+        var frame_buf = std.ArrayList(u8).initCapacity(session.allocator, @sizeOf(protocol.FrameHeader) + response.len) catch return;
14+        defer frame_buf.deinit(session.allocator);
15+
16+        const header_bytes = std.mem.asBytes(&header);
17+        frame_buf.appendSlice(session.allocator, header_bytes) catch return;
18+        frame_buf.appendSlice(session.allocator, response) catch return;
19+
20+        _ = posix.write(client.fd, frame_buf.items) catch {};
21+        return; // Don't forward to PTY
22+    }
23+
24     // Write input to PTY master fd
25     const written = posix.write(session.pty_master_fd, text) catch |err| {
26         std.debug.print("Error writing to PTY: {s}\n", .{@errorName(err)});
27@@ -621,6 +640,19 @@ fn handlePtyInput(client: *Client, text: []const u8) !void {
28     _ = written;
29 }
30 
31+fn respondToDeviceAttributes(_: *Client, text: []const u8) ?[]const u8 {
32+    // Primary Device Attributes: CSI c or ESC [ c
33+    if (std.mem.eql(u8, text, "\x1b[c")) {
34+        // VT220 with color and clipboard support
35+        return "\x1b[?62;22;52c";
36+    }
37+    // Secondary Device Attributes: CSI > c or ESC [ > c
38+    if (std.mem.eql(u8, text, "\x1b[>c")) {
39+        return "\x1b[>1;10;0c";
40+    }
41+    return null;
42+}
43+
44 fn handleWindowResize(client: *Client, rows: u16, cols: u16) !void {
45     const session_name = client.attached_session orelse {
46         std.debug.print("Client fd={d} not attached to any session\n", .{client.fd});