terminal: report size when mode 2048 is enabled

This commit is contained in:
Elias Andualem
2026-08-18 14:26:37 +03:00
parent 12967b68f7
commit 07af4612ba
2 changed files with 218 additions and 7 deletions

View File

@@ -272,9 +272,10 @@ const Effects = struct {
/// and its content are borrowed for the callback duration.
pub const UnknownSequenceFn = *const fn (Terminal, ?*anyopaque, *const UnknownSequence.C) callconv(lib.calling_conv) void;
/// C function pointer type for the size callback.
/// Returns true and fills out_size if size is available,
/// or returns false to silently ignore the query.
/// C function pointer type for the size callback. Used by XTWINOPS queries
/// and VT-driven mode 2048 enable reports. Returns true and fills out_size
/// if size is available, or false to suppress the XTWINOPS response or
/// mode 2048 report.
pub const SizeFn = *const fn (Terminal, ?*anyopaque, *size_report.Size) callconv(lib.calling_conv) bool;
/// C function pointer type for the device_attributes callback.
@@ -4673,6 +4674,109 @@ test "size without callback is silent" {
vt_write(t, "\x1B[18t", 5);
}
test "mode 2048 enable and disable use C callbacks" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
80,
24,
));
defer free(t);
const S = struct {
var data: [128]u8 = undefined;
var len: usize = 0;
var calls: usize = 0;
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, length: usize) callconv(lib.calling_conv) void {
@memcpy(data[0..length], ptr[0..length]);
len = length;
calls += 1;
}
fn sizeCb(_: Terminal, _: ?*anyopaque, out_size: *size_report.Size) callconv(lib.calling_conv) bool {
out_size.* = .{
.rows = 24,
.columns = 80,
.cell_width = 8,
.cell_height = 16,
};
return true;
}
};
S.len = 0;
S.calls = 0;
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
try testing.expectEqual(Result.success, set(t, .size_cb, @ptrCast(&S.sizeCb)));
const enable = "\x1B[?2048h";
const disable = "\x1B[?2048l";
vt_write(t, enable, enable.len);
vt_write(t, enable, enable.len);
try testing.expectEqual(@as(usize, 2), S.calls);
try testing.expectEqualStrings("\x1B[48;24;80;384;640t", S.data[0..S.len]);
vt_write(t, disable, disable.len);
try testing.expectEqual(@as(usize, 2), S.calls);
try testing.expect(!t.?.terminal.modes.get(.in_band_size_reports));
}
test "mode 2048 enable tolerates missing C callbacks" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
80,
24,
));
defer free(t);
const S = struct {
var writes: usize = 0;
var sizes: usize = 0;
fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(lib.calling_conv) void {
writes += 1;
}
fn sizeCb(_: Terminal, _: ?*anyopaque, out_size: *size_report.Size) callconv(lib.calling_conv) bool {
sizes += 1;
out_size.* = .{
.rows = 24,
.columns = 80,
.cell_width = 8,
.cell_height = 16,
};
return true;
}
};
S.writes = 0;
S.sizes = 0;
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
const sequence = "\x1B[?2048h";
vt_write(t, sequence, sequence.len);
try testing.expectEqual(@as(usize, 0), S.writes);
try testing.expect(t.?.terminal.modes.get(.in_band_size_reports));
var no_write: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&no_write,
80,
24,
));
defer free(no_write);
try testing.expectEqual(Result.success, set(no_write, .size_cb, @ptrCast(&S.sizeCb)));
vt_write(no_write, sequence, sequence.len);
try testing.expectEqual(@as(usize, 1), S.sizes);
try testing.expect(no_write.?.terminal.modes.get(.in_band_size_reports));
}
test "set device_attributes callback primary" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(

View File

@@ -118,9 +118,10 @@ pub const Handler = struct {
/// valid for the lifetime of the call.
enquiry: ?*const fn (*Handler) []const u8,
/// Called in response to XTWINOPS size queries (CSI 14/16/18 t).
/// Returns the current terminal geometry used for encoding.
/// Return null to silently ignore the query.
/// Called for XTWINOPS size queries (CSI 14/16/18 t) and when VT input
/// enables in-band size reports (mode 2048). Returns the current
/// terminal geometry used for encoding. Return null to suppress the
/// XTWINOPS response or mode 2048 report.
size: ?*const fn (*Handler) ?size_report.Size,
/// Called when the terminal title changes via escape sequences
@@ -674,6 +675,17 @@ pub const Handler = struct {
self.writePty(resp);
}
fn reportMode2048(self: *Handler) void {
const get_size = self.effects.size orelse return;
const current = get_size(self) orelse return;
var buf: [128]u8 = undefined;
var writer: std.Io.Writer = .fixed(buf[0 .. buf.len - 1]);
size_report.encode(&writer, .mode_2048, current) catch return;
buf[writer.end] = 0;
self.writePty(buf[0..writer.end :0]);
}
fn windowTitle(self: *Handler, title_raw: []const u8) !void {
// Prevent DoS attacks by limiting title length.
const max_title_len = 1024;
@@ -797,10 +809,14 @@ pub const Handler = struct {
.synchronized_output,
.linefeed,
.in_band_size_reports,
.focus_event,
=> {},
// Enabling mode 2048 reports already-committed pixel geometry.
// Waiting for the next resize leaves late-enabling clients without
// the dimensions they need for their first image frame.
.in_band_size_reports => if (enabled) self.reportMode2048(),
.report_visibility => if (enabled) self.sendVisibilityReport(),
.mouse_event_x10 => {
@@ -2909,6 +2925,97 @@ test "size report csi_14_t with effect" {
try testing.expectEqualStrings("\x1b[4;432;720t", S.written.?);
}
test "mode 2048 enable reports current geometry and disable is silent" {
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);
const S = struct {
var response: [128]u8 = undefined;
var response_len: usize = 0;
var calls: usize = 0;
fn writePty(_: *Handler, data: [:0]const u8) void {
@memcpy(response[0..data.len], data);
response_len = data.len;
calls += 1;
}
fn getSize(_: *Handler) ?size_report.Size {
return .{ .rows = 24, .columns = 80, .cell_width = 8, .cell_height = 16 };
}
};
S.response_len = 0;
S.calls = 0;
var handler: Handler = .init(&t);
handler.effects.write_pty = &S.writePty;
handler.effects.size = &S.getSize;
var s: Stream = .init(.{ .allocator = testing.allocator, .handler = handler });
defer s.deinit();
s.nextSlice("\x1b[?2048h");
s.nextSlice("\x1b[?2048h");
try testing.expectEqual(@as(usize, 2), S.calls);
try testing.expectEqualStrings("\x1b[48;24;80;384;640t", S.response[0..S.response_len]);
s.nextSlice("\x1b[?2048l");
try testing.expectEqual(@as(usize, 2), S.calls);
try testing.expect(!t.modes.get(.in_band_size_reports));
}
test "mode 2048 enable tolerates missing effects" {
const S = struct {
var calls: usize = 0;
fn writePty(_: *Handler, _: [:0]const u8) void {
calls += 1;
}
fn getSize(_: *Handler) ?size_report.Size {
return .{ .rows = 24, .columns = 80, .cell_width = 8, .cell_height = 16 };
}
};
S.calls = 0;
var no_size_terminal: Terminal = try .init(
testing.io,
testing.allocator,
.{ .cols = 80, .rows = 24 },
);
defer no_size_terminal.deinit(testing.allocator);
var no_size_handler: Handler = .init(&no_size_terminal);
no_size_handler.effects.write_pty = &S.writePty;
var no_size_stream: Stream = .init(.{
.allocator = testing.allocator,
.handler = no_size_handler,
});
defer no_size_stream.deinit();
no_size_stream.nextSlice("\x1b[?2048h");
try testing.expect(no_size_terminal.modes.get(.in_band_size_reports));
try testing.expectEqual(@as(usize, 0), S.calls);
var no_write_terminal: Terminal = try .init(
testing.io,
testing.allocator,
.{ .cols = 80, .rows = 24 },
);
defer no_write_terminal.deinit(testing.allocator);
var no_write_handler: Handler = .init(&no_write_terminal);
no_write_handler.effects.size = &S.getSize;
var no_write_stream: Stream = .init(.{
.allocator = testing.allocator,
.handler = no_write_handler,
});
defer no_write_stream.deinit();
no_write_stream.nextSlice("\x1b[?2048h");
try testing.expect(no_write_terminal.modes.get(.in_band_size_reports));
try testing.expectEqual(@as(usize, 0), S.calls);
}
test "size report csi_16_t with effect" {
var t: Terminal = try .init(testing.io, testing.allocator, .{ .cols = 80, .rows = 24 });
defer t.deinit(testing.allocator);