From 165e03669ce53263b1763e42e9beaf39b0911fd6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Mar 2026 14:23:44 -0700 Subject: [PATCH] terminal: port enquiry to Effects Previously the ENQ (0x05) action was ignored in stream_terminal, listed in the no-op group alongside other unhandled queries. The real implementation in termio/stream_handler writes a configurable response string back to the pty. Add an enquiry callback to Effects following the same query-style pattern as xtversion: the callback returns the raw response bytes and the handler owns writing them to the pty via writePty. When no callback is set (readonly mode), ENQ is silently ignored. Empty responses are also ignored. The response is capped at 256 bytes using a stack buffer with sentinel conversion for writePty. --- src/terminal/stream_terminal.zig | 99 +++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/terminal/stream_terminal.zig b/src/terminal/stream_terminal.zig index addd6bf83..8dbb3f2ae 100644 --- a/src/terminal/stream_terminal.zig +++ b/src/terminal/stream_terminal.zig @@ -39,7 +39,8 @@ pub const Handler = struct { /// Called when the terminal needs to write data back to the pty, /// e.g. in response to a DECRQM query. The data is only valid - /// during the lifetime of the call. + /// during the lifetime of the call so callers must copy it + /// if it needs to be stored or used after the call returns. write_pty: ?*const fn (*Handler, [:0]const u8) void, /// Called when the terminal title changes via escape sequences @@ -58,6 +59,11 @@ pub const Handler = struct { /// Return null to silently ignore the query. size: ?*const fn (*Handler) ?size_report.Size, + /// Called in response to ENQ (0x05). Returns the raw response + /// bytes to write back to the pty. The returned memory must be + /// valid for the lifetime of the call. + enquiry: ?*const fn (*Handler) []const u8, + /// No effects means that the stream effectively becomes readonly /// that only affects pure terminal state and ignores all side /// effects beyond that. @@ -67,6 +73,7 @@ pub const Handler = struct { .title_changed = null, .xtversion = null, .size = null, + .enquiry = null, }; }; @@ -217,6 +224,7 @@ pub const Handler = struct { .size_report => self.reportSize(value), .window_title => self.windowTitle(value.title), .xtversion => self.reportXtversion(), + .enquiry => self.reportEnquiry(), // No supported DCS commands have any terminal-modifying effects, // but they may in the future. For now we just ignore it. @@ -233,7 +241,6 @@ pub const Handler = struct { => {}, // Have no terminal-modifying effect - .enquiry, .device_attributes, .device_status, .report_pwd, @@ -256,6 +263,17 @@ pub const Handler = struct { func(self); } + fn reportEnquiry(self: *Handler) void { + const func = self.effects.enquiry orelse return; + const response = func(self); + if (response.len == 0) return; + var buf: [256]u8 = undefined; + if (response.len >= buf.len) return; + @memcpy(buf[0..response.len], response); + buf[response.len] = 0; + self.writePty(buf[0..response.len :0]); + } + fn reportXtversion(self: *Handler) void { const version = if (self.effects.xtversion) |func| func(self) else ""; var buf: [288]u8 = undefined; @@ -1576,3 +1594,80 @@ test "size report csi_21_t title" { defer testing.allocator.free(S.written.?); try testing.expectEqualStrings("\x1b]lMy Title\x1b\\", S.written.?); } + +test "enquiry no effect" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); + defer t.deinit(testing.allocator); + + const S = struct { + var written: ?[]const u8 = null; + fn writePty(_: *Handler, data: [:0]const u8) void { + written = testing.allocator.dupe(u8, data) catch @panic("OOM"); + } + }; + S.written = null; + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + // ENQ without enquiry effect should not write anything + s.nextSlice("\x05"); + try testing.expect(S.written == null); +} + +test "enquiry with effect" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); + defer t.deinit(testing.allocator); + + const S = struct { + var written: ?[]const u8 = null; + fn writePty(_: *Handler, data: [:0]const u8) void { + written = testing.allocator.dupe(u8, data) catch @panic("OOM"); + } + fn enquiry(_: *Handler) []const u8 { + return "ghostty"; + } + }; + S.written = null; + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + handler.effects.enquiry = &S.enquiry; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + s.nextSlice("\x05"); + defer testing.allocator.free(S.written.?); + try testing.expectEqualStrings("ghostty", S.written.?); +} + +test "enquiry with empty response" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 }); + defer t.deinit(testing.allocator); + + const S = struct { + var written: ?[]const u8 = null; + fn writePty(_: *Handler, data: [:0]const u8) void { + written = testing.allocator.dupe(u8, data) catch @panic("OOM"); + } + fn enquiry(_: *Handler) []const u8 { + return ""; + } + }; + S.written = null; + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + handler.effects.enquiry = &S.enquiry; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + // Empty enquiry response should not write anything + s.nextSlice("\x05"); + try testing.expect(S.written == null); +}