terminal: add xtversion effect to stream_terminal

Add an xtversion callback to the Effects struct so that
stream_terminal can respond to XTVERSION queries. The callback
returns the version string to embed in the DCS response. If the
callback is unset or returns an empty string, the response defaults
to "libghostty". The response is formatted and written back via the
existing write_pty effect.
pull/11787/head
Mitchell Hashimoto 2026-03-22 20:48:08 -07:00
parent 134516310d
commit 26c81b4b0e
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 95 additions and 1 deletions

View File

@ -45,6 +45,12 @@ pub const Handler = struct {
/// handler.terminal.getTitle().
title_changed: ?*const fn (*Handler) void,
/// Called in response to an XTVERSION query. Returns the version
/// string to report (e.g. "ghostty 1.2.3"). The returned memory
/// must be valid for the lifetime of the call. The maximum length
/// is 256 bytes; longer strings will be silently ignored.
xtversion: ?*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.
@ -52,6 +58,7 @@ pub const Handler = struct {
.bell = null,
.write_pty = null,
.title_changed = null,
.xtversion = null,
};
};
@ -200,6 +207,7 @@ pub const Handler = struct {
.request_mode => self.requestMode(value.mode),
.request_mode_unknown => self.requestModeUnknown(value.mode, value.ansi),
.window_title => self.windowTitle(value.title),
.xtversion => self.reportXtversion(),
// No supported DCS commands have any terminal-modifying effects,
// but they may in the future. For now we just ignore it.
@ -218,7 +226,6 @@ pub const Handler = struct {
// Have no terminal-modifying effect
.enquiry,
.size_report,
.xtversion,
.device_attributes,
.device_status,
.report_pwd,
@ -241,6 +248,17 @@ pub const Handler = struct {
func(self);
}
fn reportXtversion(self: *Handler) void {
const version = if (self.effects.xtversion) |func| func(self) else "";
var buf: [288]u8 = undefined;
const resp = std.fmt.bufPrintZ(
&buf,
"\x1BP>|{s}\x1B\\",
.{if (version.len > 0) version else "libghostty"},
) catch return;
self.writePty(resp);
}
fn windowTitle(self: *Handler, title_raw: []const u8) void {
// Prevent DoS attacks by limiting title length.
const max_title_len = 1024;
@ -1295,3 +1313,79 @@ test "kitty_keyboard_query" {
s.nextSlice("\x1b[?u");
try testing.expectEqualStrings("\x1b[?1u", S.written.?);
}
test "xtversion default" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
const S = struct {
var written: ?[:0]const u8 = null;
fn writePty(_: *Handler, data: [:0]const u8) void {
written = data;
}
};
S.written = null;
var handler: Handler = .init(&t);
handler.effects.write_pty = &S.writePty;
var s: Stream = .initAlloc(testing.allocator, handler);
defer s.deinit();
// Without xtversion effect set, should report "libghostty"
s.nextSlice("\x1b[>0q");
try testing.expectEqualStrings("\x1bP>|libghostty\x1b\\", S.written.?);
}
test "xtversion with effect" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
const S = struct {
var written: ?[:0]const u8 = null;
fn writePty(_: *Handler, data: [:0]const u8) void {
written = data;
}
fn xtversion(_: *Handler) []const u8 {
return "ghostty 1.2.3";
}
};
S.written = null;
var handler: Handler = .init(&t);
handler.effects.write_pty = &S.writePty;
handler.effects.xtversion = &S.xtversion;
var s: Stream = .initAlloc(testing.allocator, handler);
defer s.deinit();
s.nextSlice("\x1b[>0q");
try testing.expectEqualStrings("\x1bP>|ghostty 1.2.3\x1b\\", S.written.?);
}
test "xtversion with empty string effect" {
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
const S = struct {
var written: ?[:0]const u8 = null;
fn writePty(_: *Handler, data: [:0]const u8) void {
written = data;
}
fn xtversion(_: *Handler) []const u8 {
return "";
}
};
S.written = null;
var handler: Handler = .init(&t);
handler.effects.write_pty = &S.writePty;
handler.effects.xtversion = &S.xtversion;
var s: Stream = .initAlloc(testing.allocator, handler);
defer s.deinit();
// Empty string from effect should fall back to "libghostty"
s.nextSlice("\x1b[>0q");
try testing.expectEqualStrings("\x1bP>|libghostty\x1b\\", S.written.?);
}