mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-19 20:18:07 +00:00
terminal: answer ANSI DECRQM queries
#14225 Handle the ANSI form of DECRQM (`CSI Ps $ p`), as specified by the VT510 reference [1] and xterm [2]. Previously, only the DEC private form reached the mode query handler. [1] https://vt100.net/docs/vt510-rm/DECRQM.html [2] https://invisible-island.net/xterm/ctlseqs/ctlseqs.txt
This commit is contained in:
@@ -3882,7 +3882,7 @@ test "point_from_grid_ref null node" {
|
||||
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null));
|
||||
}
|
||||
|
||||
test "set write_pty callback" {
|
||||
test "set write_pty callback DECRQM" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
@@ -3895,17 +3895,20 @@ test "set write_pty callback" {
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
var last_userdata: ?*anyopaque = null;
|
||||
var calls: usize = 0;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
last_userdata = null;
|
||||
calls = 0;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, ud: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
last_userdata = ud;
|
||||
calls += 1;
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
@@ -3920,6 +3923,13 @@ test "set write_pty callback" {
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1B[?7;1$y", S.last_data.?);
|
||||
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
||||
try testing.expectEqual(1, S.calls);
|
||||
|
||||
const query = "\x1B[4h\x1B[4$p";
|
||||
vt_write(t, query, query.len);
|
||||
try testing.expectEqual(2, S.calls);
|
||||
try testing.expectEqualStrings("\x1B[4;1$y", S.last_data.?);
|
||||
try testing.expectEqual(@as(?*anyopaque, @ptrCast(&sentinel)), S.last_userdata);
|
||||
}
|
||||
|
||||
test "write_pty receives DECRQSS response" {
|
||||
@@ -4003,6 +4013,7 @@ test "set write_pty without callback ignores queries" {
|
||||
|
||||
// Without setting a callback, DECRQM should be silently ignored (no crash)
|
||||
vt_write(t, "\x1B[?7$p", 6);
|
||||
vt_write(t, "\x1B[4$p", 5);
|
||||
}
|
||||
|
||||
test "set write_pty null clears callback" {
|
||||
|
||||
@@ -2167,7 +2167,7 @@ pub fn Stream(comptime H: type) type {
|
||||
|
||||
// DECRQM - Request Mode
|
||||
'p' => switch (input.intermediates.len) {
|
||||
2 => decrqm: {
|
||||
1, 2 => decrqm: {
|
||||
const ansi_mode = ansi: {
|
||||
switch (input.intermediates.len) {
|
||||
1 => if (input.intermediates[0] == '$') break :ansi true,
|
||||
@@ -3401,6 +3401,61 @@ test "stream: ansi set mode (SM) and reset mode (RM)" {
|
||||
try testing.expect(s.handler.mode == null);
|
||||
}
|
||||
|
||||
test "stream: DECRQM dispatch" {
|
||||
const H = struct {
|
||||
calls: usize = 0,
|
||||
mode: ?modes.Mode = null,
|
||||
raw: ?Action.RawMode = null,
|
||||
|
||||
pub fn vt(self: *@This(), comptime action: Action.Tag, value: Action.Value(action)) void {
|
||||
switch (action) {
|
||||
.request_mode => {
|
||||
self.calls += 1;
|
||||
self.mode = value.mode;
|
||||
},
|
||||
.request_mode_unknown => {
|
||||
self.calls += 1;
|
||||
self.raw = value;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cases = [_]struct {
|
||||
input: []const u8,
|
||||
mode: ?modes.Mode = null,
|
||||
raw: ?Action.RawMode = null,
|
||||
}{
|
||||
.{ .input = "\x1b[4$p", .mode = .insert },
|
||||
.{ .input = "\x1b[?4$p", .mode = .slow_scroll },
|
||||
.{ .input = "\x1b[9999$p", .raw = .{ .mode = 9999, .ansi = true } },
|
||||
.{ .input = "\x1b[?9999$p", .raw = .{ .mode = 9999, .ansi = false } },
|
||||
.{ .input = "\x1b[4p" },
|
||||
.{ .input = "\x1b[?4p" },
|
||||
.{ .input = "\x1b[4!p" },
|
||||
.{ .input = "\x1b[4 p" },
|
||||
.{ .input = "\x1b[>4$p" },
|
||||
.{ .input = "\x1b[?4!p" },
|
||||
.{ .input = "\x1b[$p" },
|
||||
.{ .input = "\x1b[?$p" },
|
||||
.{ .input = "\x1b[4;20$p" },
|
||||
.{ .input = "\x1b[?4;7$p" },
|
||||
.{ .input = "\x1b[4:20$p" },
|
||||
};
|
||||
for (cases) |case| {
|
||||
for (0..case.input.len + 1) |split| {
|
||||
var s: Stream(H) = .init(.{ .handler = .{} });
|
||||
s.nextSlice(case.input[0..split]);
|
||||
if (split < case.input.len) try testing.expectEqual(0, s.handler.calls);
|
||||
s.nextSlice(case.input[split..]);
|
||||
try testing.expectEqual(@as(usize, if (case.mode != null or case.raw != null) 1 else 0), s.handler.calls);
|
||||
try testing.expectEqual(case.mode, s.handler.mode);
|
||||
try testing.expectEqualDeep(case.raw, s.handler.raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" {
|
||||
const H = struct {
|
||||
mode: ?modes.Mode = null,
|
||||
|
||||
@@ -4574,6 +4574,7 @@ test "request mode DECRQM with write_pty callback" {
|
||||
|
||||
// DECRQM for mode 7 (wraparound) — should be silently ignored
|
||||
s.nextSlice("\x1B[?7$p");
|
||||
s.nextSlice("\x1B[4$p");
|
||||
}
|
||||
|
||||
t.fullReset();
|
||||
@@ -4615,6 +4616,65 @@ test "request mode DECRQM with write_pty callback" {
|
||||
}
|
||||
}
|
||||
|
||||
test "request mode DECRQM ANSI responses" {
|
||||
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
const S = struct {
|
||||
var response: [32]u8 = undefined;
|
||||
var len: usize = 0;
|
||||
var calls: usize = 0;
|
||||
|
||||
fn writePty(_: *Handler, data: []const u8) void {
|
||||
@memcpy(response[0..data.len], data);
|
||||
len = data.len;
|
||||
calls += 1;
|
||||
}
|
||||
};
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
|
||||
defer s.deinit();
|
||||
|
||||
inline for (.{
|
||||
.{ "2", modes.Mode.disable_keyboard },
|
||||
.{ "4", modes.Mode.insert },
|
||||
.{ "12", modes.Mode.send_receive_mode },
|
||||
.{ "20", modes.Mode.linefeed },
|
||||
}) |mode| {
|
||||
inline for (.{ false, true, false }) |enabled| {
|
||||
s.nextSlice("\x1b[" ++ mode[0] ++ (if (enabled) "h" else "l"));
|
||||
try testing.expectEqual(enabled, t.modes.get(mode[1]));
|
||||
const query = "\x1b[" ++ mode[0] ++ "$p";
|
||||
for (0..query.len + 1) |split| {
|
||||
S.calls = 0;
|
||||
S.len = 0;
|
||||
s.nextSlice(query[0..split]);
|
||||
if (split < query.len) try testing.expectEqual(0, S.calls);
|
||||
s.nextSlice(query[split..]);
|
||||
try testing.expectEqual(1, S.calls);
|
||||
try testing.expectEqualStrings("\x1b[" ++ mode[0] ++ (if (enabled) ";1$y" else ";2$y"), S.response[0..S.len]);
|
||||
try testing.expectEqual(enabled, t.modes.get(mode[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The two namespaces must report independent states for mode 4.
|
||||
s.nextSlice("\x1b[4h\x1b[?4l");
|
||||
const cases = .{
|
||||
.{ "\x1b[4$p", "\x1b[4;1$y" },
|
||||
.{ "\x1b[?4$p", "\x1b[?4;2$y" },
|
||||
.{ "\x1b[9999$p", "\x1b[9999;0$y" },
|
||||
};
|
||||
inline for (cases) |case| {
|
||||
S.calls = 0;
|
||||
S.len = 0;
|
||||
s.nextSlice(case[0]);
|
||||
try testing.expectEqual(1, S.calls);
|
||||
try testing.expectEqualStrings(case[1], S.response[0..S.len]);
|
||||
}
|
||||
}
|
||||
|
||||
test "stream: CSI W with intermediate but no params" {
|
||||
// Regression test from AFL++ crash. CSI ? W without
|
||||
// parameters caused an out-of-bounds access on input.params[0].
|
||||
|
||||
Reference in New Issue
Block a user