- commit
- cca1a8a
- parent
- cedbe7f
- author
- Eric Bower
- date
- 2025-10-15 22:47:50 -0400 EDT
fix: filter our client input queries from echoing to stdout
2 files changed,
+119,
-8
+1,
-1
1@@ -258,7 +258,7 @@ fn readCallback(
2 const msg_line = remaining_data[0..newline_idx];
3
4 const msg_type_parsed = protocol.parseMessageType(ctx.allocator, msg_line) catch |err| {
5- std.debug.print("JSON parse error: {s}\r\n", .{@errorName(err)});
6+ std.debug.print("JSON parse error: {s}, data: {s}\r\n", .{ @errorName(err), msg_line });
7 return .rearm;
8 };
9 defer msg_type_parsed.deinit();
+118,
-7
1@@ -203,11 +203,7 @@ const VTHandler = struct {
2 ) !void {
3 _ = da_params;
4
5- const response = switch (req) {
6- .primary => "\x1b[?1;2c", // VT100 with AVO (matches screen/tmux)
7- .secondary => "\x1b[>0;0;0c", // Conservative secondary DA
8- .tertiary => return, // Ignore tertiary DA
9- };
10+ const response = getDeviceAttributeResponse(req) orelse return;
11
12 _ = posix.write(self.pty_master_fd, response) catch |err| {
13 std.debug.print("Error writing DA response to PTY: {s}\n", .{@errorName(err)});
14@@ -830,6 +826,112 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
15 }
16 }
17
18+/// Returns the device attribute response zmx should send (matching tmux/screen)
19+/// Returns null for tertiary DA (ignored)
20+fn getDeviceAttributeResponse(req: ghostty.DeviceAttributeReq) ?[]const u8 {
21+ return switch (req) {
22+ .primary => "\x1b[?1;2c", // VT100 with AVO (matches screen/tmux)
23+ .secondary => "\x1b[>0;0;0c", // Conservative secondary DA
24+ .tertiary => null, // Ignore tertiary DA
25+ };
26+}
27+
28+/// Filter out terminal response sequences that the client's terminal sends
29+/// These should not be written to the PTY since the daemon handles queries itself
30+///
31+/// Architecture: When apps send queries (e.g., ESC[c), the client's terminal
32+/// auto-responds. We must drop those responses because:
33+/// 1. VTHandler already responds with correct zmx terminal capabilities
34+/// 2. Client responses describe the client's terminal, not zmx's virtual terminal
35+/// 3. Without filtering, responses get echoed by PTY and appear as literal text
36+///
37+/// This matches tmux/screen behavior: intercept queries, respond ourselves, drop client responses
38+fn filterTerminalResponses(input: []const u8, output_buf: []u8) usize {
39+ var out_idx: usize = 0;
40+ var i: usize = 0;
41+
42+ while (i < input.len) {
43+ // Look for ESC sequences
44+ if (input[i] == 0x1b and i + 1 < input.len and input[i + 1] == '[') {
45+ // CSI sequence - parse it
46+ const seq_start = i;
47+ i += 2; // Skip ESC [
48+
49+ // Collect parameter bytes (0x30-0x3F)
50+ const param_start = i;
51+ while (i < input.len and input[i] >= 0x30 and input[i] <= 0x3F) : (i += 1) {}
52+ const seq_params = input[param_start..i];
53+
54+ // Collect intermediate bytes (0x20-0x2F)
55+ while (i < input.len and input[i] >= 0x20 and input[i] <= 0x2F) : (i += 1) {}
56+
57+ // Final byte (0x40-0x7E)
58+ if (i < input.len and input[i] >= 0x40 and input[i] <= 0x7E) {
59+ const final = input[i];
60+ const should_drop = blk: {
61+ // Device Attributes responses: ESC[?...c, ESC[>...c, ESC[=...c
62+ // These match the responses defined in getDeviceAttributeResponse()
63+ if (final == 'c' and seq_params.len > 0) {
64+ if (seq_params[0] == '?' or seq_params[0] == '>' or seq_params[0] == '=') {
65+ std.debug.print("Filtered DA response: ESC[{s}c\n", .{seq_params});
66+ break :blk true;
67+ }
68+ }
69+ // Cursor Position Report: ESC[<row>;<col>R
70+ if (final == 'R' and seq_params.len > 0) {
71+ // Simple heuristic: if params look like digits/semicolon, it's likely CPR
72+ var is_cpr = true;
73+ for (seq_params) |byte| {
74+ if (byte != ';' and (byte < '0' or byte > '9')) {
75+ is_cpr = false;
76+ break;
77+ }
78+ }
79+ if (is_cpr) {
80+ std.debug.print("Filtered CPR: ESC[{s}R\n", .{seq_params});
81+ break :blk true;
82+ }
83+ }
84+ // DSR responses: ESC[0n, ESC[3n, ESC[?...n
85+ if (final == 'n' and seq_params.len > 0) {
86+ if ((seq_params.len == 1 and (seq_params[0] == '0' or seq_params[0] == '3')) or
87+ seq_params[0] == '?')
88+ {
89+ std.debug.print("Filtered DSR response: ESC[{s}n\n", .{seq_params});
90+ break :blk true;
91+ }
92+ }
93+ break :blk false;
94+ };
95+
96+ if (should_drop) {
97+ // Skip this entire sequence, continue to next character
98+ i += 1;
99+ } else {
100+ // Copy the entire sequence to output
101+ const seq_len = (i + 1) - seq_start;
102+ @memcpy(output_buf[out_idx .. out_idx + seq_len], input[seq_start .. i + 1]);
103+ out_idx += seq_len;
104+ i += 1;
105+ }
106+ } else {
107+ // Incomplete sequence, copy what we have
108+ const seq_len = i - seq_start;
109+ @memcpy(output_buf[out_idx .. out_idx + seq_len], input[seq_start..i]);
110+ out_idx += seq_len;
111+ // i is already positioned at the next byte
112+ }
113+ } else {
114+ // Not a CSI sequence, copy the byte
115+ output_buf[out_idx] = input[i];
116+ out_idx += 1;
117+ i += 1;
118+ }
119+ }
120+
121+ return out_idx;
122+}
123+
124 fn handlePtyInput(client: *Client, text: []const u8) !void {
125 const session_name = client.attached_session orelse {
126 std.debug.print("Client fd={d} not attached to any session\n", .{client.fd});
127@@ -841,10 +943,19 @@ fn handlePtyInput(client: *Client, text: []const u8) !void {
128 return error.SessionNotFound;
129 };
130
131- std.debug.print("Client fd={d}: Writing {d} bytes to PTY fd={d} (first byte: {d})\n", .{ client.fd, text.len, session.pty_master_fd, if (text.len > 0) text[0] else 0 });
132+ // Filter out terminal response sequences before writing to PTY
133+ var filtered_buf: [128 * 1024]u8 = undefined;
134+ const filtered_len = filterTerminalResponses(text, &filtered_buf);
135+
136+ if (filtered_len == 0) {
137+ return; // All input was filtered, nothing to write
138+ }
139+
140+ const filtered_text = filtered_buf[0..filtered_len];
141+ std.debug.print("Client fd={d}: Writing {d} bytes to PTY fd={d} (filtered from {d} bytes)\n", .{ client.fd, filtered_len, session.pty_master_fd, text.len });
142
143 // Write input to PTY master fd
144- const written = posix.write(session.pty_master_fd, text) catch |err| {
145+ const written = posix.write(session.pty_master_fd, filtered_text) catch |err| {
146 std.debug.print("Error writing to PTY: {s}\n", .{@errorName(err)});
147 return err;
148 };