terminal: decrqm don't truncate 16-bit mode requests (#14237)

DECRQM requests can use a full 16-bit number but we truncated to 15-bits
for the reply (cause thats all we know about). Preserve, the full
16-bits for requests so we give the proper response.
This commit is contained in:
Mitchell Hashimoto
2026-09-14 12:58:39 -07:00
committed by GitHub
4 changed files with 47 additions and 6 deletions

View File

@@ -24,7 +24,7 @@ pub fn report_encode(
) callconv(lib.calling_conv) Result {
const mode_tag: modes.ModeTag = @bitCast(tag);
const report: modes.Report = .{
.tag = mode_tag,
.tag = .{ .value = mode_tag.value, .ansi = mode_tag.ansi },
.state = state.toZig() orelse return .invalid_value,
};

View File

@@ -76,7 +76,7 @@ pub const ModeState = struct {
/// Return a DECRPM report for the given mode tag. If the tag does
/// not correspond to a known mode, the report state is .not_recognized.
pub fn getReport(self: *const ModeState, tag: ModeTag) Report {
pub fn getReport(self: *const ModeState, tag: Report.Tag) Report {
// DECECM (Erase Color Mode, DEC private mode 117) controls whether erasing
// and scrolling use the default background or the active background color.
// Ghostty's behavior is fixed equivalent to DECECM reset, and DECRQM has a
@@ -191,14 +191,27 @@ pub fn modeFromInt(v: u16, ansi: bool) ?Mode {
/// A DECRPM mode report response.
pub const Report = struct {
tag: ModeTag,
tag: Tag,
state: State,
/// A query identifier can use the full parser parameter range. Keep it
/// separate from ModeTag, which packs supported modes and the ANSI flag
/// into a u16 and is also used by the public C API.
pub const Tag = struct {
value: u16,
ansi: bool = false,
pub fn fromMode(mode: Mode) Tag {
const tag = ModeTag.fromMode(mode);
return .{ .value = tag.value, .ansi = tag.ansi };
}
};
pub const max_size = max_size: {
// Construct the largest possible report in terms of values.
const report: Report = .{
.tag = .{
.value = std.math.maxInt(u15),
.value = std.math.maxInt(u16),
.ansi = false,
},
.state = .permanently_reset,
@@ -460,3 +473,25 @@ test "Report.encode not recognized" {
try report.encode(&writer);
try testing.expectEqualStrings("\x1B[?9999;0$y", writer.buffered());
}
test "getReport large unknown modes" {
const state: ModeState = .{};
// These would alias ANSI insert mode and fixed-status DEC mode 117.
for ([_]Report.Tag{
.{ .value = 32772, .ansi = true },
.{ .value = 32885 },
}) |tag| {
const report = state.getReport(tag);
try testing.expectEqual(Report.State.not_recognized, report.state);
try testing.expectEqualDeep(tag, report.tag);
}
}
test "Report.encode maximum size" {
var buf: [Report.max_size]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
const report: Report = .{ .tag = .{ .value = 65535 }, .state = .permanently_reset };
try report.encode(&writer);
try testing.expectEqualStrings("\x1b[?65535;4$y", writer.buffered());
try testing.expectEqual(buf.len, writer.buffered().len);
}

View File

@@ -1517,7 +1517,7 @@ pub const Handler = struct {
fn requestModeUnknown(self: *Handler, mode_raw: u16, ansi: bool) void {
const report = self.terminal.modes.getReport(.{
.value = @truncate(mode_raw),
.value = mode_raw,
.ansi = ansi,
});
self.sendModeReport(report);
@@ -4606,6 +4606,12 @@ test "request mode DECRQM with write_pty callback" {
s.nextSlice("\x1B[?7$p");
try testing.expectEqualStrings("\x1B[?7;2$y", S.last_response.?);
// A large unknown mode must not alias wraparound mode 7.
const before = t.modes;
s.nextSlice("\x1B[?32775$p");
try testing.expectEqualStrings("\x1B[?32775;0$y", S.last_response.?);
try testing.expectEqualDeep(before, t.modes);
// Query an unknown mode
s.nextSlice("\x1B[?9999$p");
try testing.expectEqualStrings("\x1B[?9999;0$y", S.last_response.?);

View File

@@ -583,7 +583,7 @@ pub const StreamHandler = struct {
}
fn requestModeUnknown(self: *StreamHandler, mode_raw: u16, ansi: bool) !void {
self.sendModeReport(self.terminal.modes.getReport(.{ .value = @truncate(mode_raw), .ansi = ansi }));
self.sendModeReport(self.terminal.modes.getReport(.{ .value = mode_raw, .ansi = ansi }));
}
fn sendModeReport(self: *StreamHandler, report: terminal.modes.Report) void {