mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
vt: handle pixel sizes and size reports in ghostty_terminal_resize (#11818)
The resize function now requires cell_width_px and cell_height_px parameters and handles the full resize sequence: computing and setting width_px/height_px on the terminal, clearing synchronized output mode so changes display immediately, and encoding a mode 2048 in-band size report via the write_pty callback when that mode is enabled. A valid width/height px is critical for some applications and protocols and some applications rely directly on in-band size reports, so this change is necessary to support those use cases. I do wonder if for the Zig API we should be doing this in `terminal.resize` or somewhere else, because as it stands this has to all be manually done on the Zig side.
This commit is contained in:
@@ -570,6 +570,24 @@ typedef enum {
|
||||
* Output type: size_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_SCROLLBACK_ROWS = 15,
|
||||
|
||||
/**
|
||||
* The total width of the terminal in pixels.
|
||||
*
|
||||
* This is cols * cell_width_px as set by ghostty_terminal_resize().
|
||||
*
|
||||
* Output type: uint32_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_WIDTH_PX = 16,
|
||||
|
||||
/**
|
||||
* The total height of the terminal in pixels.
|
||||
*
|
||||
* This is rows * cell_height_px as set by ghostty_terminal_resize().
|
||||
*
|
||||
* Output type: uint32_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_HEIGHT_PX = 17,
|
||||
} GhosttyTerminalData;
|
||||
|
||||
/**
|
||||
@@ -618,16 +636,25 @@ void ghostty_terminal_reset(GhosttyTerminal terminal);
|
||||
* screen will reflow content if wraparound mode is enabled; the alternate
|
||||
* screen does not reflow. If the dimensions are unchanged, this is a no-op.
|
||||
*
|
||||
* This also updates the terminal's pixel dimensions (used for image
|
||||
* protocols and size reports), disables synchronized output mode (allowed
|
||||
* by the spec so that resize results are shown immediately), and sends an
|
||||
* in-band size report if mode 2048 is enabled.
|
||||
*
|
||||
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param cols New width in cells (must be greater than zero)
|
||||
* @param rows New height in cells (must be greater than zero)
|
||||
* @param cell_width_px Width of a single cell in pixels
|
||||
* @param cell_height_px Height of a single cell in pixels
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal,
|
||||
uint16_t cols,
|
||||
uint16_t rows);
|
||||
uint16_t rows,
|
||||
uint32_t cell_width_px,
|
||||
uint32_t cell_height_px);
|
||||
|
||||
/**
|
||||
* Set an option on the terminal.
|
||||
|
||||
@@ -387,10 +387,39 @@ pub fn resize(
|
||||
terminal_: Terminal,
|
||||
cols: size.CellCountInt,
|
||||
rows: size.CellCountInt,
|
||||
cell_width_px: u32,
|
||||
cell_height_px: u32,
|
||||
) callconv(.c) Result {
|
||||
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
|
||||
const wrapper = terminal_ orelse return .invalid_value;
|
||||
const t = wrapper.terminal;
|
||||
if (cols == 0 or rows == 0) return .invalid_value;
|
||||
t.resize(t.gpa(), cols, rows) catch return .out_of_memory;
|
||||
|
||||
// Update pixel sizes
|
||||
t.width_px = std.math.mul(u32, cols, cell_width_px) catch std.math.maxInt(u32);
|
||||
t.height_px = std.math.mul(u32, rows, cell_height_px) catch std.math.maxInt(u32);
|
||||
|
||||
// Disable synchronized output mode so that we show changes
|
||||
// immediately for a resize. This is allowed by the spec.
|
||||
t.modes.set(.synchronized_output, false);
|
||||
|
||||
// If we have in-band size reporting enabled, send a report.
|
||||
if (t.modes.get(.in_band_size_reports)) in_band: {
|
||||
const func = wrapper.effects.write_pty orelse break :in_band;
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
size_report.encode(&writer, .mode_2048, .{
|
||||
.rows = rows,
|
||||
.columns = cols,
|
||||
.cell_width = cell_width_px,
|
||||
.cell_height = cell_height_px,
|
||||
}) catch break :in_band;
|
||||
|
||||
const data = writer.buffered();
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata, data.ptr, data.len);
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
@@ -447,6 +476,8 @@ pub const TerminalData = enum(c_int) {
|
||||
pwd = 13,
|
||||
total_rows = 14,
|
||||
scrollback_rows = 15,
|
||||
width_px = 16,
|
||||
height_px = 17,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: TerminalData) type {
|
||||
@@ -460,6 +491,7 @@ pub const TerminalData = enum(c_int) {
|
||||
.cursor_style => style_c.Style,
|
||||
.title, .pwd => lib.String,
|
||||
.total_rows, .scrollback_rows => usize,
|
||||
.width_px, .height_px => u32,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -518,6 +550,8 @@ fn getTyped(
|
||||
},
|
||||
.total_rows => out.* = t.screens.active.pages.total_rows,
|
||||
.scrollback_rows => out.* = t.screens.active.pages.total_rows - t.rows,
|
||||
.width_px => out.* = t.width_px,
|
||||
.height_px => out.* = t.height_px,
|
||||
}
|
||||
|
||||
return .success;
|
||||
@@ -692,13 +726,13 @@ test "resize" {
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
try testing.expectEqual(Result.success, resize(t, 40, 12));
|
||||
try testing.expectEqual(Result.success, resize(t, 40, 12, 9, 18));
|
||||
try testing.expectEqual(40, t.?.terminal.cols);
|
||||
try testing.expectEqual(12, t.?.terminal.rows);
|
||||
}
|
||||
|
||||
test "resize null" {
|
||||
try testing.expectEqual(Result.invalid_value, resize(null, 80, 24));
|
||||
try testing.expectEqual(Result.invalid_value, resize(null, 80, 24, 9, 18));
|
||||
}
|
||||
|
||||
test "resize invalid value" {
|
||||
@@ -714,8 +748,8 @@ test "resize invalid value" {
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, resize(t, 0, 24));
|
||||
try testing.expectEqual(Result.invalid_value, resize(t, 80, 0));
|
||||
try testing.expectEqual(Result.invalid_value, resize(t, 0, 24, 9, 18));
|
||||
try testing.expectEqual(Result.invalid_value, resize(t, 80, 0, 9, 18));
|
||||
}
|
||||
|
||||
test "mode_get and mode_set" {
|
||||
@@ -1840,6 +1874,189 @@ test "get title set via vt_write" {
|
||||
try testing.expectEqualStrings("VT Title", title.ptr[0..title.len]);
|
||||
}
|
||||
|
||||
test "resize updates pixel dimensions" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
||||
|
||||
const zt = t.?.terminal;
|
||||
try testing.expectEqual(@as(u32, 100 * 9), zt.width_px);
|
||||
try testing.expectEqual(@as(u32, 40 * 18), zt.height_px);
|
||||
}
|
||||
|
||||
test "resize pixel overflow saturates" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
try testing.expectEqual(Result.success, resize(t, 100, 40, std.math.maxInt(u32), std.math.maxInt(u32)));
|
||||
|
||||
const zt = t.?.terminal;
|
||||
try testing.expectEqual(std.math.maxInt(u32), zt.width_px);
|
||||
try testing.expectEqual(std.math.maxInt(u32), zt.height_px);
|
||||
}
|
||||
|
||||
test "resize disables synchronized output" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
const zt = t.?.terminal;
|
||||
zt.modes.set(.synchronized_output, true);
|
||||
|
||||
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
||||
try testing.expect(!zt.modes.get(.synchronized_output));
|
||||
}
|
||||
|
||||
test "resize sends in-band size report" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
const S = struct {
|
||||
var last_data: ?[]u8 = null;
|
||||
|
||||
fn deinit() void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = null;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(.c) void {
|
||||
if (last_data) |d| testing.allocator.free(d);
|
||||
last_data = testing.allocator.dupe(u8, ptr[0..len]) catch @panic("OOM");
|
||||
}
|
||||
};
|
||||
defer S.deinit();
|
||||
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
|
||||
// Enable in-band size reports (mode 2048)
|
||||
t.?.terminal.modes.set(.in_band_size_reports, true);
|
||||
|
||||
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
||||
|
||||
// Expected: \x1B[48;rows;cols;height_px;width_pxt
|
||||
// height_px = 40*18 = 720, width_px = 100*9 = 900
|
||||
try testing.expect(S.last_data != null);
|
||||
try testing.expectEqualStrings("\x1B[48;40;100;720;900t", S.last_data.?);
|
||||
}
|
||||
|
||||
test "resize no size report without mode 2048" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
const S = struct {
|
||||
var called: bool = false;
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, _: [*]const u8, _: usize) callconv(.c) void {
|
||||
called = true;
|
||||
}
|
||||
};
|
||||
S.called = false;
|
||||
|
||||
try testing.expectEqual(Result.success, set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
|
||||
// in_band_size_reports is off by default
|
||||
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
||||
try testing.expect(!S.called);
|
||||
}
|
||||
|
||||
test "resize in-band report without write_pty callback" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
// Enable mode 2048 but don't set a write_pty callback — should not crash
|
||||
t.?.terminal.modes.set(.in_band_size_reports, true);
|
||||
try testing.expectEqual(Result.success, resize(t, 100, 40, 9, 18));
|
||||
}
|
||||
|
||||
test "resize null terminal" {
|
||||
try testing.expectEqual(Result.invalid_value, resize(null, 100, 40, 9, 18));
|
||||
}
|
||||
|
||||
test "resize zero cols" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, resize(t, 0, 40, 9, 18));
|
||||
}
|
||||
|
||||
test "resize zero rows" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 0,
|
||||
},
|
||||
));
|
||||
defer free(t);
|
||||
|
||||
try testing.expectEqual(Result.invalid_value, resize(t, 100, 0, 9, 18));
|
||||
}
|
||||
|
||||
test "grid_ref out of bounds" {
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
||||
Reference in New Issue
Block a user