mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal: report size when mode 2048 is enabled
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user