From a1d7ad92434a6c1c5b5a2b436a15e0573790b6cc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Mar 2026 14:33:01 -0700 Subject: [PATCH] terminal: extract size report encoder Size report escape sequences were previously formatted inline in Termio.sizeReportLocked, and termio.Message carried a duplicate enum for report styles. That made the encoding logic harder to reuse and kept the style type scoped to termio. Move the encoding into terminal.size_report and export it through terminal.main. The encoder now takes renderer.Size directly and derives grid and pixel dimensions from one source of truth. termio.Message now aliases terminal.size_report.Style, and Termio writes reports via the shared encoder. --- src/renderer/size.zig | 5 +- src/terminal/main.zig | 1 + src/terminal/size_report.zig | 162 +++++++++++++++++++++++++++++++++++ src/termio/Termio.zig | 50 +++-------- src/termio/message.zig | 9 +- 5 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 src/terminal/size_report.zig diff --git a/src/renderer/size.zig b/src/renderer/size.zig index 565e95e2c..7a022cdb4 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -1,8 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const configpkg = @import("../config.zig"); -const font = @import("../font/main.zig"); -const terminal = @import("../terminal/main.zig"); +const terminal_size = @import("../terminal/size.zig"); const log = std.log.scoped(.renderer_size); @@ -225,7 +224,7 @@ pub const ScreenSize = extern struct { /// The dimensions of the grid itself, in rows/columns units. pub const GridSize = extern struct { - pub const Unit = terminal.size.CellCountInt; + pub const Unit = terminal_size.CellCountInt; columns: Unit = 0, rows: Unit = 0, diff --git a/src/terminal/main.zig b/src/terminal/main.zig index bbdea1542..17cc3a81d 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -21,6 +21,7 @@ pub const parse_table = @import("parse_table.zig"); pub const search = @import("search.zig"); pub const sgr = @import("sgr.zig"); pub const size = @import("size.zig"); +pub const size_report = @import("size_report.zig"); pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {}; pub const x11_color = @import("x11_color.zig"); diff --git a/src/terminal/size_report.zig b/src/terminal/size_report.zig new file mode 100644 index 000000000..97fa8d277 --- /dev/null +++ b/src/terminal/size_report.zig @@ -0,0 +1,162 @@ +const std = @import("std"); +const CellCountInt = @import("size.zig").CellCountInt; + +/// Output formats for terminal size reports written to the PTY. +pub const Style = enum { + /// In-band size reports (mode 2048) + mode_2048, + + /// XTWINOPS: report text area size in pixels + csi_14_t, + + /// XTWINOPS: report cell size in pixels + csi_16_t, + + /// XTWINOPS: report text area size in characters + csi_18_t, +}; + +/// Runtime size values used to encode terminal size reports. +pub const Size = struct { + /// Terminal row count in cells. + rows: CellCountInt, + + /// Terminal column count in cells. + columns: CellCountInt, + + /// Width of a single terminal cell in pixels. + cell_width: u32, + + /// Height of a single terminal cell in pixels. + cell_height: u32, + + pub fn widthPixels(self: Size) u64 { + return @as(u64, self.columns) * @as(u64, self.cell_width); + } + + pub fn heightPixels(self: Size) u64 { + return @as(u64, self.rows) * @as(u64, self.cell_height); + } +}; + +/// Encode a terminal size report sequence. +pub fn encode( + writer: *std.Io.Writer, + style: Style, + size: Size, +) std.Io.Writer.Error!void { + switch (style) { + .mode_2048 => try writer.print( + "\x1B[48;{};{};{};{}t", + .{ + size.rows, + size.columns, + size.heightPixels(), + size.widthPixels(), + }, + ), + + .csi_14_t => try writer.print( + "\x1b[4;{};{}t", + .{ + size.heightPixels(), + size.widthPixels(), + }, + ), + + .csi_16_t => try writer.print( + "\x1b[6;{};{}t", + .{ + size.cell_height, + size.cell_width, + }, + ), + + .csi_18_t => try writer.print( + "\x1b[8;{};{}t", + .{ + size.rows, + size.columns, + }, + ), + } +} + +fn testSize() Size { + return .{ + .rows = 24, + .columns = 80, + .cell_width = 9, + .cell_height = 18, + }; +} + +test "encode mode 2048" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encode(&writer, .mode_2048, testSize()); + + try std.testing.expectEqualStrings("\x1B[48;24;80;432;720t", writer.buffered()); +} + +test "encode csi 14 t" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encode(&writer, .csi_14_t, testSize()); + + try std.testing.expectEqualStrings("\x1b[4;432;720t", writer.buffered()); +} + +test "encode csi 16 t" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encode(&writer, .csi_16_t, testSize()); + + try std.testing.expectEqualStrings("\x1b[6;18;9t", writer.buffered()); +} + +test "encode csi 18 t" { + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encode(&writer, .csi_18_t, testSize()); + + try std.testing.expectEqualStrings("\x1b[8;24;80t", writer.buffered()); +} + +test "encode max values for all fields" { + const max_size: Size = .{ + .rows = std.math.maxInt(@FieldType(Size, "rows")), + .columns = std.math.maxInt(@FieldType(Size, "columns")), + .cell_width = std.math.maxInt(@FieldType(Size, "cell_width")), + .cell_height = std.math.maxInt(@FieldType(Size, "cell_height")), + }; + + const Case = struct { + style: Style, + expected: []const u8, + }; + + inline for ([_]Case{ + .{ + .style = .mode_2048, + .expected = "\x1B[48;65535;65535;281470681677825;281470681677825t", + }, + .{ + .style = .csi_14_t, + .expected = "\x1b[4;281470681677825;281470681677825t", + }, + .{ + .style = .csi_16_t, + .expected = "\x1b[6;4294967295;4294967295t", + }, + .{ + .style = .csi_18_t, + .expected = "\x1b[8;65535;65535t", + }, + }) |case| { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encode(&writer, case.style, max_size); + try std.testing.expectEqualStrings(case.expected, writer.buffered()); + } +} diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 73e1c46d5..4a99e8221 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -526,48 +526,24 @@ pub fn sizeReport(self: *Termio, td: *ThreadData, style: termio.Message.SizeRepo fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeReport) !void { const grid_size = self.size.grid(); + const report_size: terminalpkg.size_report.Size = .{ + .rows = grid_size.rows, + .columns = grid_size.columns, + .cell_width = self.size.cell.width, + .cell_height = self.size.cell.height, + }; // 1024 bytes should be enough for size report since report // in columns and pixels. var buf: [1024]u8 = undefined; - const message = switch (style) { - .mode_2048 => try std.fmt.bufPrint( - &buf, - "\x1B[48;{};{};{};{}t", - .{ - grid_size.rows, - grid_size.columns, - grid_size.rows * self.size.cell.height, - grid_size.columns * self.size.cell.width, - }, - ), - .csi_14_t => try std.fmt.bufPrint( - &buf, - "\x1b[4;{};{}t", - .{ - grid_size.rows * self.size.cell.height, - grid_size.columns * self.size.cell.width, - }, - ), - .csi_16_t => try std.fmt.bufPrint( - &buf, - "\x1b[6;{};{}t", - .{ - self.size.cell.height, - self.size.cell.width, - }, - ), - .csi_18_t => try std.fmt.bufPrint( - &buf, - "\x1b[8;{};{}t", - .{ - grid_size.rows, - grid_size.columns, - }, - ), - }; + var writer: std.Io.Writer = .fixed(&buf); + try terminalpkg.size_report.encode( + &writer, + style, + report_size, + ); - try self.queueWrite(td, message, false); + try self.queueWrite(td, writer.buffered(), false); } /// Reset the synchronized output mode. This is usually called by timer diff --git a/src/termio/message.zig b/src/termio/message.zig index d7a59bf5e..4ee7f245e 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -93,13 +93,8 @@ pub const Message = union(enum) { }; } - /// The types of size reports that we support - pub const SizeReport = enum { - mode_2048, - csi_14_t, - csi_16_t, - csi_18_t, - }; + /// The types of size reports that we support. + pub const SizeReport = terminal.size_report.Style; }; test {