aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/formatter.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/formatter.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/formatter.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/formatter.zig1523
1 files changed, 1137 insertions, 386 deletions
diff --git a/vendor/libghostty-vt/src/terminal/formatter.zig b/vendor/libghostty-vt/src/terminal/formatter.zig
index 553ea28d..4feedda5 100644
--- a/vendor/libghostty-vt/src/terminal/formatter.zig
+++ b/vendor/libghostty-vt/src/terminal/formatter.zig
@@ -1,5 +1,6 @@
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
+const fastprint = @import("../fastprint.zig");
const lib = @import("lib.zig");
const Allocator = std.mem.Allocator;
const color = @import("color.zig");
@@ -119,9 +120,117 @@ pub const Options = struct {
///
/// Used by formatters that operate on PageLists to track the source position
/// of each byte written. The caller is responsible for freeing the map.
+///
+/// The mapping is stored in two parts: a per-byte x/y coordinate (8
+/// bytes per output byte, half the size of a Pin) and a tiny table of
+/// page nodes covering byte ranges (there are only ever a handful of
+/// pages). This also lets page formatters write coordinates directly
+/// into the map without a separate coordinate-to-pin conversion pass.
pub const PinMap = struct {
alloc: Allocator,
- map: *std.ArrayList(Pin),
+ map: *Map,
+
+ /// The type of the page node referenced by pins.
+ pub const Node = @FieldType(Pin, "node");
+
+ /// A page node covering output bytes starting at `offset`
+ /// (inclusive) until the next entry's offset (or the end of the
+ /// output).
+ pub const NodeRun = struct {
+ offset: usize,
+ node: Node,
+ };
+
+ pub const Map = struct {
+ /// The x/y coordinate within its page for every output byte.
+ points: std.ArrayList(Coordinate) = .empty,
+
+ /// The page node for ranges of output bytes, ordered by offset.
+ nodes: std.ArrayList(NodeRun) = .empty,
+
+ pub const empty: Map = .{};
+
+ pub fn deinit(self: *Map, alloc: Allocator) void {
+ self.points.deinit(alloc);
+ self.nodes.deinit(alloc);
+ }
+
+ pub fn clearRetainingCapacity(self: *Map) void {
+ self.points.clearRetainingCapacity();
+ self.nodes.clearRetainingCapacity();
+ }
+
+ /// The total number of bytes mapped.
+ pub fn count(self: *const Map) usize {
+ return self.points.items.len;
+ }
+
+ /// Set the page node for all bytes appended from here on,
+ /// until the next call. No-op if the node is unchanged.
+ pub fn setNode(
+ self: *Map,
+ alloc: Allocator,
+ node: Node,
+ ) Allocator.Error!void {
+ if (self.nodes.getLastOrNull()) |last| {
+ if (last.node == node) return;
+ }
+
+ try self.nodes.append(alloc, .{
+ .offset = self.points.items.len,
+ .node = node,
+ });
+ }
+
+ /// Append `n` bytes that map to `pin`.
+ pub fn append(
+ self: *Map,
+ alloc: Allocator,
+ pin: Pin,
+ n: usize,
+ ) Allocator.Error!void {
+ if (n == 0) return;
+ try self.setNode(alloc, pin.node);
+ try self.points.appendNTimes(
+ alloc,
+ .{ .x = pin.x, .y = pin.y },
+ n,
+ );
+ }
+
+ /// Returns the pin that the byte at the given offset maps to,
+ /// or null if the offset is out of range.
+ pub fn get(self: *const Map, offset: usize) ?Pin {
+ if (offset >= self.points.items.len) return null;
+ const coord = self.points.items[offset];
+ return .{
+ .node = findNode(self.nodes.items, offset) orelse return null,
+ .x = coord.x,
+ .y = @intCast(coord.y),
+ };
+ }
+
+ /// Returns the last pin in the map, if any.
+ pub fn getLastOrNull(self: *const Map) ?Pin {
+ const len = self.points.items.len;
+ if (len == 0) return null;
+ return self.get(len - 1);
+ }
+ };
+
+ /// Binary search for the node covering `offset` in a slice of node
+ /// runs sorted by offset. Returns null only if the slice is empty
+ /// or the offset precedes the first run.
+ pub fn findNode(runs: []const NodeRun, offset: usize) ?Node {
+ if (runs.len == 0 or offset < runs[0].offset) return null;
+ var lo: usize = 0;
+ var hi: usize = runs.len;
+ while (lo + 1 < hi) {
+ const mid = lo + (hi - lo) / 2;
+ if (runs[mid].offset <= offset) lo = mid else hi = mid;
+ }
+ return runs[lo].node;
+ }
};
/// Terminal formatter formats the active terminal screen.
@@ -288,7 +397,7 @@ pub const TerminalFormatter = struct {
// Map all those bytes to the same pin. Use the top left to ensure
// the node pointer is always properly initialized.
- m.map.appendNTimes(
+ m.map.append(
m.alloc,
self.terminal.screens.active.pages.getTopLeft(.screen),
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
@@ -326,7 +435,47 @@ pub const TerminalFormatter = struct {
// Map all those bytes to the same pin. Use the top left to ensure
// the node pointer is always properly initialized.
- m.map.appendNTimes(
+ m.map.append(
+ m.alloc,
+ self.terminal.screens.active.pages.getTopLeft(.screen),
+ std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
+ ) catch return error.WriteFailed;
+ }
+ }
+
+ // Emit tabstop positions before the screen contents because setting
+ // them moves the cursor. Screen formatting will restore the requested
+ // cursor position afterwards.
+ if (self.opts.emit == .vt and self.extra.tabstops) {
+ // Clear all tabs (CSI 3 g)
+ try writer.print("\x1b[3g", .{});
+
+ // Set each configured tabstop by moving cursor and using HTS
+ for (0..self.terminal.cols) |col| {
+ if (self.terminal.tabstops.get(col)) {
+ // Move cursor to the column (1-indexed)
+ try writer.print("\x1b[{d}G", .{col + 1});
+ // Set tab (HTS)
+ try writer.print("\x1bH", .{});
+ }
+ }
+
+ // Screen contents are formatted relative to the top-left.
+ try writer.writeAll("\x1b[H");
+
+ // If we have a pin_map, add the bytes we wrote to map.
+ if (self.pin_map) |*m| {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ var extra_formatter: TerminalFormatter = self;
+ extra_formatter.content = .none;
+ extra_formatter.pin_map = null;
+ extra_formatter.extra = .none;
+ extra_formatter.extra.tabstops = true;
+ try extra_formatter.format(&discarding.writer);
+
+ // Map all those bytes to the same pin. Use the top left to ensure
+ // the node pointer is always properly initialized.
+ m.map.append(
m.alloc,
self.terminal.screens.active.pages.getTopLeft(.screen),
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
@@ -336,7 +485,6 @@ pub const TerminalFormatter = struct {
var screen_formatter: ScreenFormatter = .init(self.terminal.screens.active, self.opts);
screen_formatter.content = self.content;
- screen_formatter.extra = self.extra.screen;
screen_formatter.pin_map = self.pin_map;
try screen_formatter.format(writer);
@@ -360,22 +508,6 @@ pub const TerminalFormatter = struct {
}
}
- // Emit tabstop positions
- if (self.extra.tabstops) {
- // Clear all tabs (CSI 3 g)
- try writer.print("\x1b[3g", .{});
-
- // Set each configured tabstop by moving cursor and using HTS
- for (0..self.terminal.cols) |col| {
- if (self.terminal.tabstops.get(col)) {
- // Move cursor to the column (1-indexed)
- try writer.print("\x1b[{d}G", .{col + 1});
- // Set tab (HTS)
- try writer.print("\x1bH", .{});
- }
- }
- }
-
// Emit keyboard modes such as ModifyOtherKeys
if (self.extra.keyboard) {
// Only emit if modify_other_keys_2 is true
@@ -398,25 +530,25 @@ pub const TerminalFormatter = struct {
extra_formatter.pin_map = null;
extra_formatter.extra = .none;
extra_formatter.extra.scrolling_region = self.extra.scrolling_region;
- extra_formatter.extra.tabstops = self.extra.tabstops;
extra_formatter.extra.keyboard = self.extra.keyboard;
extra_formatter.extra.pwd = self.extra.pwd;
try extra_formatter.format(&discarding.writer);
- m.map.appendNTimes(
+ m.map.append(
m.alloc,
- if (m.map.items.len > 0) pin: {
- const last = m.map.items[m.map.items.len - 1];
- break :pin .{
- .node = last.node,
- .x = last.x,
- .y = last.y,
- };
- } else self.terminal.screens.active.pages.getTopLeft(.screen),
+ m.map.getLastOrNull() orelse
+ self.terminal.screens.active.pages.getTopLeft(.screen),
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}
}
+
+ // Emit extra screen state last because terminal state such
+ // as scrolling regions can move the cursor, so we have to set
+ // cursor last.
+ screen_formatter.content = .none;
+ screen_formatter.extra = self.extra.screen;
+ try screen_formatter.format(writer);
}
};
@@ -563,6 +695,43 @@ pub const ScreenFormatter = struct {
.html => return,
}
+ // Emit cursor position before the other extras because restoring a
+ // pending wrap requires reprinting the cell at the right edge. That
+ // print uses and changes active screen state, so the requested style,
+ // hyperlink, protection, and charset must be restored afterwards.
+ if (self.extra.cursor) cursor: {
+ const cursor = &self.screen.cursor;
+
+ // If we don't have pending wrap, then we can just use CUP.
+ if (!cursor.pending_wrap or cursor.x != self.screen.pages.cols - 1) {
+ try writer.print("\x1b[{d};{d}H", .{ cursor.y + 1, cursor.x + 1 });
+ break :cursor;
+ }
+
+ // Pending wrap, we can't use CUP because it resets pending wrap.
+ const start_x = switch (cursor.page_cell.wide) {
+ .spacer_tail => cursor.x - 1,
+ .narrow, .wide, .spacer_head => cursor.x,
+ };
+
+ // Move cursor to the edge.
+ try writer.print(
+ "\x1b[{d};{d}H",
+ .{ cursor.y + 1, start_x + 1 },
+ );
+
+ // Reformat the cell which sets the proper pending wrap state.
+ var cell_formatter: PageFormatter = .init(
+ cursor.page_pin.node.page(),
+ self.opts,
+ );
+ cell_formatter.start_x = cursor.x;
+ cell_formatter.end_x = cursor.x;
+ cell_formatter.start_y = cursor.page_pin.y;
+ cell_formatter.end_y = cursor.page_pin.y;
+ try cell_formatter.format(writer);
+ }
+
// Emit current SGR style state
if (self.extra.style) {
const cursor = &self.screen.cursor;
@@ -652,13 +821,6 @@ pub const ScreenFormatter = struct {
}
}
- // Emit cursor position using CUP (CSI H)
- if (self.extra.cursor) {
- const cursor = &self.screen.cursor;
- // CUP is 1-indexed
- try writer.print("\x1b[{d};{d}H", .{ cursor.y + 1, cursor.x + 1 });
- }
-
// If we have a pin_map, we need to count how many bytes the extras
// will emit so we can map them all to the same pin. We do this by
// formatting to a discarding writer with content=none.
@@ -671,21 +833,10 @@ pub const ScreenFormatter = struct {
// Map all those bytes to the same pin. Use the first page node
// to ensure the node pointer is always properly initialized.
- m.map.appendNTimes(
+ m.map.append(
m.alloc,
- if (m.map.items.len > 0) pin: {
- // There is a weird Zig miscompilation here on 0.15.2.
- // If I return the m.map.items value directly then we
- // get undefined memory (even though we're copying a
- // Pin struct). If we duplicate here like this we do
- // not.
- const last = m.map.items[m.map.items.len - 1];
- break :pin .{
- .node = last.node,
- .x = last.x,
- .y = last.y,
- };
- } else self.screen.pages.getTopLeft(.screen),
+ m.map.getLastOrNull() orelse
+ self.screen.pages.getTopLeft(.screen),
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}
@@ -737,10 +888,6 @@ pub const PageListFormatter = struct {
const tl: PageList.Pin = self.top_left orelse self.list.getTopLeft(.screen);
const br: PageList.Pin = self.bottom_right orelse self.list.getBottomRight(.screen).?;
- // If we keep track of pins, we'll need this.
- var point_map: std.ArrayList(Coordinate) = .empty;
- defer if (self.pin_map) |*m| point_map.deinit(m.alloc);
-
var page_state: ?PageFormatter.TrailingState = null;
var iter = tl.pageIterator(.right_down, br);
while (iter.next()) |chunk| {
@@ -763,27 +910,19 @@ pub const PageListFormatter = struct {
if (chunk.node == br.node) formatter.end_x = br.x;
}
- // If we're tracking pins, then we setup a point map for the
- // page formatter (cause it can't track pins). And then we convert
- // this to pins later.
+ // If we're tracking pins, the page formatter writes its
+ // per-byte coordinates directly into our map's point list
+ // and we record which page node covers those bytes.
if (self.pin_map) |*m| {
- point_map.clearRetainingCapacity();
- formatter.point_map = .{ .alloc = m.alloc, .map = &point_map };
+ m.map.setNode(m.alloc, chunk.node) catch return error.WriteFailed;
+ formatter.point_map = .{
+ .alloc = m.alloc,
+ .map = &m.map.points,
+ .base = m.map.points.items.len,
+ };
}
page_state = try formatter.formatWithState(writer);
-
- // If we're tracking pins then grab our points and write them
- // to our pin map.
- if (self.pin_map) |*m| {
- for (point_map.items) |coord| {
- m.map.append(m.alloc, .{
- .node = chunk.node,
- .x = coord.x,
- .y = @intCast(coord.y),
- }) catch return error.WriteFailed;
- }
- }
}
}
};
@@ -831,16 +970,25 @@ pub const PageFormatter = struct {
/// The x/y coordinate will be the coordinates within the page.
///
/// Warning: there is a significant performance hit to track this
- point_map: ?struct {
- alloc: Allocator,
- map: *std.ArrayList(Coordinate),
- },
+ point_map: ?PointMap,
/// The previous trailing state from the prior page. If you're iterating
/// over multiple pages this helps ensure that unwrapping and other
/// accounting works properly.
trailing_state: ?TrailingState,
+ /// See point_map.
+ pub const PointMap = struct {
+ alloc: Allocator,
+ map: *std.ArrayList(Coordinate),
+
+ /// The index in `map` at which this formatter's output begins.
+ /// Entries before this index belong to a caller (e.g. previous
+ /// pages of a PageListFormatter) and aren't inspected. This
+ /// exists so that callers can share one list across pages.
+ base: usize = 0,
+ };
+
/// Trailing state. This is used to ensure that rows wrapped across
/// multiple pages are unwrapped properly, as well as other accounting
/// we may do in the future.
@@ -878,6 +1026,18 @@ pub const PageFormatter = struct {
self: PageFormatter,
writer: *std.Io.Writer,
) std.Io.Writer.Error!TrailingState {
+ // Specialize the hot path on the emitted format so that the
+ // per-cell loop contains no per-cell format dispatch.
+ switch (self.opts.emit) {
+ inline else => |emit| return self.formatWithStateEmit(writer, emit),
+ }
+ }
+
+ fn formatWithStateEmit(
+ self: PageFormatter,
+ writer: *std.Io.Writer,
+ comptime emit: Format,
+ ) std.Io.Writer.Error!TrailingState {
var blank_rows: usize = 0;
var blank_cells: usize = 0;
@@ -934,15 +1094,14 @@ pub const PageFormatter = struct {
}
// Wrap HTML output in monospace font styling
- switch (self.opts.emit) {
+ switch (emit) {
.plain => {},
.html => {
// Setup our div. We use a buffer here that should always
// fit the stuff we need, in order to make counting bytes easier.
var buf: [1024]u8 = undefined;
- var stream = std.io.fixedBufferStream(&buf);
- const buf_writer = stream.writer();
+ var buf_writer: std.Io.Writer = .fixed(&buf);
// Monospace and whitespace preserving
buf_writer.writeAll("<div style=\"font-family: monospace; white-space: pre;") catch return error.WriteFailed;
@@ -959,7 +1118,7 @@ pub const PageFormatter = struct {
buf_writer.writeAll("\">") catch return error.WriteFailed;
- const header = stream.getWritten();
+ const header = buf_writer.buffered();
try writer.writeAll(header);
if (self.point_map) |*map| map.map.appendNTimes(
map.alloc,
@@ -971,8 +1130,7 @@ pub const PageFormatter = struct {
.vt => {
// OSC 10 sets foreground color, OSC 11 sets background color
var buf: [512]u8 = undefined;
- var stream = std.io.fixedBufferStream(&buf);
- const buf_writer = stream.writer();
+ var buf_writer: std.Io.Writer = .fixed(&buf);
if (self.opts.foreground) |fg| {
buf_writer.print(
"\x1b]10;rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\",
@@ -986,7 +1144,7 @@ pub const PageFormatter = struct {
) catch return error.WriteFailed;
}
- const header = stream.getWritten();
+ const header = buf_writer.buffered();
try writer.writeAll(header);
if (self.point_map) |*map| map.map.appendNTimes(
map.alloc,
@@ -996,8 +1154,23 @@ pub const PageFormatter = struct {
},
}
- // Our style for non-plain formats
+ // Our style for non-plain formats. Alongside the style itself we
+ // track the page-local interned style id it corresponds to (styles
+ // are interned per-page so id equality implies style equality).
+ // The id is only a fast-path hint: it is set to `invalid_style_id`
+ // whenever the current style didn't come from an interned id
+ // (e.g. bg-color-only cells which synthesize styles).
+ const invalid_style_id: u32 = std.math.maxInt(u32);
var style: Style = .{};
+ var style_id: u32 = 0;
+
+ // Whether the codepoint map has any entries. Hoisted out of the
+ // per-codepoint path so that the common no-map case can use the
+ // fast cell run path below.
+ const cp_map_empty: bool = if (self.opts.codepoint_map) |m|
+ m.len == 0
+ else
+ true;
// Track hyperlink state for HTML output. We need to close </a> tags
// when the hyperlink changes or ends.
@@ -1052,11 +1225,12 @@ pub const PageFormatter = struct {
// Reset style before emitting newlines to prevent background
// colors from bleeding into the next line's leading cells.
if (!style.default()) {
- try self.formatStyleClose(writer);
+ try self.formatStyleClose(emit, writer);
style = .{};
+ style_id = 0;
}
- const sequence: []const u8 = switch (self.opts.emit) {
+ const sequence: []const u8 = switch (emit) {
// Plaintext just uses standard newlines because newlines
// on their own usually move the cursor back in anywhere
// you type plaintext.
@@ -1079,7 +1253,7 @@ pub const PageFormatter = struct {
// in a prior page, so we just map to the first row of this
// page.
if (self.point_map) |*map| {
- const start: Coordinate = if (map.map.items.len > 0)
+ const start: Coordinate = if (map.map.items.len > map.base)
map.map.items[map.map.items.len - 1]
else
.{ .x = 0, .y = 0 };
@@ -1115,8 +1289,41 @@ pub const PageFormatter = struct {
if (!row.wrap_continuation or !self.opts.unwrap) blank_cells = 0;
// Go through each cell and print it
- for (cells_subset, row_start_x..) |*cell, x_usize| {
- const x: size.CellCountInt = @intCast(x_usize);
+ var cell_i: usize = 0;
+ while (cell_i < cells_subset.len) : (cell_i += 1) {
+ const cell: *const Cell = &cells_subset[cell_i];
+ const x: size.CellCountInt = @intCast(row_start_x + cell_i);
+
+ // Fast path: runs of simple cells (single codepoint, no
+ // style/hyperlink transitions) are encoded in batches,
+ // avoiding all of the per-cell bookkeeping below. This is
+ // only valid when we have no codepoint map and when our
+ // current style/hyperlink state is known-stable.
+ if (cp_map_empty) fast: {
+ if (comptime formatStyled(emit)) {
+ if (style_id == invalid_style_id) break :fast;
+ }
+
+ const consumed = try self.writeCellRun(
+ emit,
+ self.point_map != null,
+ writer,
+ cells_subset[cell_i..],
+ x,
+ y,
+ style_id,
+ current_hyperlink_id,
+ &blank_cells,
+ );
+
+ // Zero cells consumed means the first cell isn't
+ // eligible for the fast path; handle it below.
+ if (consumed == 0) break :fast;
+
+ // The continue expression adds the final one.
+ cell_i += consumed - 1;
+ continue;
+ }
// Skip spacers. These happen naturally when wide characters
// are printed again on the screen (for well-behaved terminals!)
@@ -1132,8 +1339,9 @@ pub const PageFormatter = struct {
// If we're emitting styled output (not plaintext) and
// the cell has some kind of styling or is not empty
// then this isn't blank.
- if (formatStyled(self.opts.emit) and
- (!cell.isEmpty() or cell.hasStyling())) break :blank;
+ if (comptime formatStyled(emit)) {
+ if (!cell.isEmpty() or cell.hasStyling()) break :blank;
+ }
// Cells with no text are blank
if (!cell.hasText()) {
@@ -1155,32 +1363,12 @@ pub const PageFormatter = struct {
if (blank_cells > 0) {
try writer.splatByteAll(' ', blank_cells);
- if (self.point_map) |*map| {
- // Map each blank cell to its coordinate. Blank cells can span
- // multiple rows if they carry over from wrap continuation.
- var remaining_blanks = blank_cells;
- var blank_x = x;
- var blank_y = y;
- while (remaining_blanks > 0) : (remaining_blanks -= 1) {
- if (blank_x > 0) {
- // We have space in this row
- blank_x -= 1;
- } else if (blank_y > 0) {
- // Wrap to previous row
- blank_y -= 1;
- blank_x = self.page.size.cols - 1;
- } else {
- // Can't go back further, just use (0, 0)
- blank_x = 0;
- blank_y = 0;
- }
-
- map.map.append(
- map.alloc,
- .{ .x = blank_x, .y = blank_y },
- ) catch return error.WriteFailed;
- }
- }
+ if (self.point_map) |*map| try self.appendBlankPoints(
+ map,
+ blank_cells,
+ x,
+ y,
+ );
blank_cells = 0;
}
@@ -1188,24 +1376,44 @@ pub const PageFormatter = struct {
style: {
// If we aren't emitting styled output then we don't
// have to worry about styles.
- if (!formatStyled(self.opts.emit)) break :style;
+ if (!comptime formatStyled(emit)) break :style;
+
+ // Fast path: styles are interned per-page, so if this
+ // cell's style id matches the id of our current style
+ // then the style is unchanged.
+ const cell_style_id: u32 = switch (cell.content_tag) {
+ .codepoint, .codepoint_grapheme => cell.style_id,
+ .bg_color_palette, .bg_color_rgb => invalid_style_id,
+ };
+ if (cell_style_id == style_id and
+ cell_style_id != invalid_style_id) break :style;
// Get our cell style.
const cell_style = self.cellStyle(cell);
// If the style hasn't changed, don't bloat output.
- if (cell_style.eql(style)) break :style;
+ // When both ids are interned (and thus different, since
+ // equal ids broke out above), interning guarantees the
+ // styles differ so we can skip the comparison entirely.
+ if (cell_style_id == invalid_style_id or
+ style_id == invalid_style_id)
+ {
+ if (cell_style.eql(style)) {
+ style_id = cell_style_id;
+ break :style;
+ }
+ }
// If we had a previous style, we need to close it,
// because we've confirmed we have some new style
// (which is maybe default).
- if (!style.default()) switch (self.opts.emit) {
- .html => try self.formatStyleClose(writer),
+ if (!style.default()) switch (emit) {
+ .html => try self.formatStyleClose(emit, writer),
// For VT, we only close if we're switching to a default
// style because any non-default style will emit
// a \x1b[0m as the start of a VT coloring sequence.
- .vt => if (cell_style.default()) try self.formatStyleClose(writer),
+ .vt => if (cell_style.default()) try self.formatStyleClose(emit, writer),
// Unreachable because of the styled() check at the
// top of this block.
@@ -1214,12 +1422,14 @@ pub const PageFormatter = struct {
// At this point, we can copy our style over
style = cell_style;
+ style_id = cell_style_id;
// If we're just the default style now, we're done.
if (cell_style.default()) break :style;
// New style, emit it.
try self.formatStyleOpen(
+ emit,
writer,
&style,
);
@@ -1229,16 +1439,18 @@ pub const PageFormatter = struct {
if (self.point_map) |*map| {
var discarding: std.Io.Writer.Discarding = .init(&.{});
try self.formatStyleOpen(
+ emit,
&discarding.writer,
&style,
);
- for (0..std.math.cast(
- usize,
- discarding.count,
- ) orelse return error.WriteFailed) |_| map.map.append(map.alloc, .{
- .x = x,
- .y = y,
- }) catch return error.WriteFailed;
+ map.map.appendNTimes(
+ map.alloc,
+ .{ .x = x, .y = y },
+ std.math.cast(
+ usize,
+ discarding.count,
+ ) orelse return error.WriteFailed,
+ ) catch return error.WriteFailed;
}
}
@@ -1247,7 +1459,7 @@ pub const PageFormatter = struct {
// We currently only emit hyperlinks for HTML. In the
// future we can support emitting OSC 8 hyperlinks for
// VT output as well.
- if (self.opts.emit != .html) break :hyperlink;
+ if (comptime emit != .html) break :hyperlink;
// Get the hyperlink ID. This ID is our internal ID,
// not necessarily the OSC8 ID.
@@ -1263,7 +1475,7 @@ pub const PageFormatter = struct {
// If our prior hyperlink ID was non-null, we need to
// close it because the ID has changed.
if (current_hyperlink_id != null) {
- try self.formatHyperlinkClose(writer);
+ try self.formatHyperlinkClose(emit, writer);
current_hyperlink_id = null;
}
@@ -1280,6 +1492,7 @@ pub const PageFormatter = struct {
break :uri link.uri.offset.ptr(self.page.memory)[0..link.uri.len];
};
try self.formatHyperlinkOpen(
+ emit,
writer,
uri,
);
@@ -1289,16 +1502,18 @@ pub const PageFormatter = struct {
if (self.point_map) |*map| {
var discarding: std.Io.Writer.Discarding = .init(&.{});
try self.formatHyperlinkOpen(
+ emit,
&discarding.writer,
uri,
);
- for (0..std.math.cast(
- usize,
- discarding.count,
- ) orelse return error.WriteFailed) |_| map.map.append(map.alloc, .{
- .x = x,
- .y = y,
- }) catch return error.WriteFailed;
+ map.map.appendNTimes(
+ map.alloc,
+ .{ .x = x, .y = y },
+ std.math.cast(
+ usize,
+ discarding.count,
+ ) orelse return error.WriteFailed,
+ ) catch return error.WriteFailed;
}
}
@@ -1306,20 +1521,21 @@ pub const PageFormatter = struct {
// We combine codepoint and graphemes because both have
// shared style handling. We use comptime to dup it.
inline .codepoint, .codepoint_grapheme => |tag| {
- try self.writeCell(tag, writer, cell);
+ try self.writeCell(tag, emit, writer, cell);
// If we have a point map, all codepoints map to this
// cell.
if (self.point_map) |*map| {
var discarding: std.Io.Writer.Discarding = .init(&.{});
- try self.writeCell(tag, &discarding.writer, cell);
- for (0..std.math.cast(
- usize,
- discarding.count,
- ) orelse return error.WriteFailed) |_| map.map.append(map.alloc, .{
- .x = x,
- .y = y,
- }) catch return error.WriteFailed;
+ try self.writeCell(tag, emit, &discarding.writer, cell);
+ map.map.appendNTimes(
+ map.alloc,
+ .{ .x = x, .y = y },
+ std.math.cast(
+ usize,
+ discarding.count,
+ ) orelse return error.WriteFailed,
+ ) catch return error.WriteFailed;
}
},
@@ -1337,13 +1553,13 @@ pub const PageFormatter = struct {
}
// If the style is non-default, we need to close our style tag.
- if (!style.default()) try self.formatStyleClose(writer);
+ if (!style.default()) try self.formatStyleClose(emit, writer);
// Close any open hyperlink for HTML output
- if (current_hyperlink_id != null) try self.formatHyperlinkClose(writer);
+ if (current_hyperlink_id != null) try self.formatHyperlinkClose(emit, writer);
// Close the monospace wrapper for HTML output
- if (self.opts.emit == .html) {
+ if (comptime emit == .html) {
const closing = "</div>";
try writer.writeAll(closing);
if (self.point_map) |*map| {
@@ -1356,14 +1572,315 @@ pub const PageFormatter = struct {
closing.len,
);
}
+ // Closing the div creates a newline in the output
+ // so make sure to create one less newline
+ if (blank_rows >= 1) blank_rows -= 1;
}
return .{ .rows = blank_rows, .cells = blank_cells };
}
+ /// Fast path for writing runs of simple cells: single-codepoint cells
+ /// that require no style or hyperlink handling. Output bytes are
+ /// batched into a stack buffer to avoid per-cell writer dispatch.
+ /// Returns the number of cells consumed, which may be zero if the
+ /// first cell isn't eligible for the fast path (in which case the
+ /// caller must handle it via the slow path).
+ ///
+ /// Requirements (asserted by the caller, not here):
+ ///
+ /// - The codepoint map is empty.
+ /// - For styled formats, `run_style_id` is the valid interned
+ /// page-local id of the currently active style.
+ /// - For HTML, no hyperlink is currently open.
+ ///
+ /// `run_x`/`run_y` are the page coordinates of `cells[0]`, used for
+ /// point map tracking.
+ ///
+ /// Blank cell accounting matches the slow path: accumulated blanks
+ /// are only materialized once a non-blank cell is found, and any
+ /// remainder is written back to `blank_cells`.
+ // Deliberately not inlined: this is instantiated per emit format and
+ // inlining every copy into formatWithStateEmit's row loop bloats the
+ // binary. track_points is a runtime bool for the same reason: a
+ // comptime bool doubles the instantiation count for one predictable
+ // branch per emitted cell.
+ noinline fn writeCellRun(
+ self: *const PageFormatter,
+ comptime emit: Format,
+ track_points: bool,
+ writer: *std.Io.Writer,
+ cells: []const Cell,
+ run_x: size.CellCountInt,
+ run_y: size.CellCountInt,
+ run_style_id: u32,
+ run_hyperlink_id: ?hyperlink.Id,
+ blank_cells: *usize,
+ ) std.Io.Writer.Error!usize {
+ assert(track_points == (self.point_map != null));
+
+ // The largest single-cell encoding must fit after a flush: the
+ // HTML entity for the maximum codepoint ("&#2097151;") is 10
+ // bytes, escapes are up to 6.
+ const max_encoding_len = 16;
+ var buf: [512]u8 = undefined;
+ var len: usize = 0;
+ var pending: usize = blank_cells.*;
+
+ var i: usize = 0;
+ while (i < cells.len) : (i += 1) {
+ const cell = &cells[i];
+
+ // Spacers produce no output, matching the slow path which
+ // skips them before any blank/style handling.
+ switch (cell.wide) {
+ .narrow, .wide => {},
+ .spacer_head, .spacer_tail => continue,
+ }
+
+ // Only text cells: bg-color cells synthesize styles and take
+ // the slow path.
+ switch (cell.content_tag) {
+ .codepoint, .codepoint_grapheme => {},
+ .bg_color_palette, .bg_color_rgb => break,
+ }
+
+ if (comptime formatStyled(emit)) {
+ // Style transition, take the slow path.
+ if (cell.style_id != run_style_id) break;
+ }
+
+ const cp: u21 = cell.content.codepoint.data;
+
+ // Blank cell accounting, matching the slow path blank block.
+ if (comptime formatStyled(emit)) {
+ // Styled formats only treat unstyled empty cells as
+ // blank; anything else (including spaces) is written
+ // so that styling is preserved.
+ if (cp == 0 and cell.wide == .narrow and run_style_id == 0) {
+ pending += 1;
+ continue;
+ }
+ } else {
+ // Cells with no text are blank.
+ if (cp == 0) {
+ pending += 1;
+ continue;
+ }
+
+ // Trailing spaces are blank.
+ if (cp == ' ' and self.opts.trim) {
+ pending += 1;
+ continue;
+ }
+ }
+
+ // Hyperlink state must be stable within a run: any non-blank
+ // cell must belong to the currently open hyperlink (or none).
+ // Transitions take the slow path. This is checked after blank
+ // accounting because blank cells never touch hyperlink state.
+ if (comptime emit == .html) {
+ if (cell.hyperlink) {
+ const run_id = run_hyperlink_id orelse break;
+ const cell_id = self.page.lookupHyperlink(cell) orelse break;
+ if (cell_id != run_id) break;
+ } else if (run_hyperlink_id != null) break;
+ }
+
+ // The page coordinate of this cell, for point tracking.
+ const x: size.CellCountInt = @intCast(run_x + i);
+
+ // This cell produces output: materialize accumulated blanks.
+ if (pending > 0) {
+ if (track_points) try self.appendBlankPoints(
+ &self.point_map.?,
+ pending,
+ x,
+ run_y,
+ );
+
+ while (pending > 0) {
+ if (len == buf.len) {
+ try writer.writeAll(buf[0..len]);
+ len = 0;
+ }
+ const n = @min(pending, buf.len - len);
+ @memset(buf[len..][0..n], ' ');
+ len += n;
+ pending -= n;
+ }
+ }
+
+ // Flush if the largest possible encoding may not fit.
+ if (len + max_encoding_len > buf.len) {
+ try writer.writeAll(buf[0..len]);
+ len = 0;
+ }
+
+ var cell_bytes: usize = 0;
+
+ // Empty (but styled or wide) cells emit a space, matching
+ // writeCell.
+ if (cp == 0) {
+ buf[len] = ' ';
+ len += 1;
+ cell_bytes = 1;
+ } else {
+ cell_bytes = encodeCodepoint(emit, &buf, &len, cp);
+
+ // Multi-codepoint graphemes emit their extra codepoints,
+ // matching writeCell. This is out-of-line to keep the
+ // hot loop for the common single-codepoint case small.
+ if (cell.content_tag == .codepoint_grapheme) {
+ @branchHint(.unlikely);
+ cell_bytes += try self.writeGraphemeCps(
+ emit,
+ writer,
+ cell,
+ &buf,
+ &len,
+ );
+ }
+ }
+
+ // All of the cell's bytes map to the cell's coordinate.
+ if (track_points) {
+ const map = &self.point_map.?;
+ map.map.appendNTimes(
+ map.alloc,
+ .{ .x = x, .y = run_y },
+ cell_bytes,
+ ) catch return error.WriteFailed;
+ }
+ }
+
+ if (len > 0) try writer.writeAll(buf[0..len]);
+ blank_cells.* = pending;
+ return i;
+ }
+
+ /// Encode the extra codepoints of a multi-codepoint grapheme into
+ /// buf, flushing to the writer as needed. Returns the number of
+ /// bytes written. This is deliberately not inlined so that the
+ /// (rare) grapheme case doesn't bloat the writeCellRun hot loop.
+ noinline fn writeGraphemeCps(
+ self: *const PageFormatter,
+ comptime emit: Format,
+ writer: *std.Io.Writer,
+ cell: *const Cell,
+ buf: *[512]u8,
+ len: *usize,
+ ) std.Io.Writer.Error!usize {
+ const max_encoding_len = 16;
+ var bytes: usize = 0;
+ for (self.page.lookupGrapheme(cell).?) |gcp| {
+ if (len.* + max_encoding_len > buf.len) {
+ try writer.writeAll(buf[0..len.*]);
+ len.* = 0;
+ }
+ bytes += encodeCodepoint(emit, buf, len, gcp);
+ }
+ return bytes;
+ }
+
+ /// Encode a single codepoint into buf at len, advancing len and
+ /// returning the number of bytes written. The caller must guarantee
+ /// enough remaining buffer space for the largest possible encoding.
+ inline fn encodeCodepoint(
+ comptime emit: Format,
+ buf: *[512]u8,
+ len: *usize,
+ cp: u21,
+ ) usize {
+ const start = len.*;
+ switch (emit) {
+ .plain, .vt => if (cp < 0x80) {
+ buf[start] = @intCast(cp);
+ len.* += 1;
+ } else {
+ len.* += std.unicode.utf8Encode(cp, buf[start..][0..4]) catch l: {
+ // Matches Writer.printUnicodeCodepoint: invalid
+ // codepoints become the replacement character.
+ buf[start..][0..3].* = std.unicode.replacement_character_utf8;
+ break :l 3;
+ };
+ },
+
+ .html => html: {
+ const esc: ?[]const u8 = switch (cp) {
+ '<' => "&lt;",
+ '>' => "&gt;",
+ '&' => "&amp;",
+ '"' => "&quot;",
+ '\'' => "&#39;",
+ else => null,
+ };
+ if (esc) |s| {
+ @memcpy(buf[start..][0..s.len], s);
+ len.* += s.len;
+ break :html;
+ }
+
+ // ASCII is emitted directly, everything else as a
+ // numeric entity. See writeCodepoint.
+ if (cp < 0x80) {
+ buf[start] = @intCast(cp);
+ len.* += 1;
+ break :html;
+ }
+
+ buf[start..][0..2].* = "&#".*;
+ len.* += 2;
+ len.* += fastprint.printDecimal(u21, buf[len.*..], cp);
+ buf[len.*] = ';';
+ len.* += 1;
+ },
+ }
+
+ return len.* - start;
+ }
+
+ /// Append the point map entries for a run of `count` blank cells
+ /// that are materialized as spaces just before the cell at (x, y).
+ /// Blank cells can span multiple rows if they carry over from wrap
+ /// continuation, so this walks backwards from (x, y).
+ fn appendBlankPoints(
+ self: *const PageFormatter,
+ map: *const PointMap,
+ count: usize,
+ x: size.CellCountInt,
+ y: size.CellCountInt,
+ ) std.Io.Writer.Error!void {
+ map.map.ensureUnusedCapacity(
+ map.alloc,
+ count,
+ ) catch return error.WriteFailed;
+
+ var remaining = count;
+ var blank_x = x;
+ var blank_y = y;
+ while (remaining > 0) : (remaining -= 1) {
+ if (blank_x > 0) {
+ // We have space in this row
+ blank_x -= 1;
+ } else if (blank_y > 0) {
+ // Wrap to previous row
+ blank_y -= 1;
+ blank_x = self.page.size.cols - 1;
+ } else {
+ // Can't go back further, just use (0, 0)
+ blank_x = 0;
+ blank_y = 0;
+ }
+
+ map.map.appendAssumeCapacity(.{ .x = blank_x, .y = blank_y });
+ }
+ }
+
fn writeCell(
self: PageFormatter,
comptime tag: Cell.ContentTag,
+ comptime emit: Format,
writer: *std.Io.Writer,
cell: *const Cell,
) !void {
@@ -1375,16 +1892,17 @@ pub const PageFormatter = struct {
return;
}
- try self.writeCodepointWithReplacement(writer, cell.content.codepoint);
+ try self.writeCodepointWithReplacement(emit, writer, cell.content.codepoint.data);
if (comptime tag == .codepoint_grapheme) {
for (self.page.lookupGrapheme(cell).?) |cp| {
- try self.writeCodepointWithReplacement(writer, cp);
+ try self.writeCodepointWithReplacement(emit, writer, cp);
}
}
}
fn writeCodepointWithReplacement(
self: PageFormatter,
+ comptime emit: Format,
writer: *std.Io.Writer,
codepoint: u21,
) !void {
@@ -1406,12 +1924,14 @@ pub const PageFormatter = struct {
// If no replacement, write it directly.
const r = r_ orelse return try self.writeCodepoint(
+ emit,
writer,
codepoint,
);
switch (r) {
.codepoint => |v| try self.writeCodepoint(
+ emit,
writer,
v,
),
@@ -1420,6 +1940,7 @@ pub const PageFormatter = struct {
const view = std.unicode.Utf8View.init(s) catch unreachable;
var it = view.iterator();
while (it.nextCodepoint()) |cp| try self.writeCodepoint(
+ emit,
writer,
cp,
);
@@ -1429,11 +1950,13 @@ pub const PageFormatter = struct {
fn writeCodepoint(
self: PageFormatter,
+ comptime emit: Format,
writer: *std.Io.Writer,
codepoint: u21,
) !void {
- switch (self.opts.emit) {
- .plain, .vt => try writer.print("{u}", .{codepoint}),
+ _ = self;
+ switch (emit) {
+ .plain, .vt => try writer.printUnicodeCodepoint(codepoint),
.html => {
switch (codepoint) {
'<' => try writer.writeAll("&lt;"),
@@ -1448,9 +1971,14 @@ pub const PageFormatter = struct {
// meta tag because we emit partial HTML so this ensures
// proper unicode handling.
if (codepoint < 0x80) {
- try writer.print("{u}", .{codepoint});
+ try writer.writeByte(@intCast(codepoint));
} else {
- try writer.print("&#{d};", .{codepoint});
+ var buf: [16]u8 = undefined;
+ buf[0..2].* = "&#".*;
+ var len: usize = 2 + fastprint.printDecimal(u21, buf[2..], codepoint);
+ buf[len] = ';';
+ len += 1;
+ try writer.writeAll(buf[0..len]);
}
},
}
@@ -1475,7 +2003,7 @@ pub const PageFormatter = struct {
.bg_color_palette => .{
.bg_color = .{
- .palette = cell.content.color_palette,
+ .palette = cell.content.color_palette.data,
},
},
@@ -1495,16 +2023,17 @@ pub const PageFormatter = struct {
/// and other HTML attribute values.
fn formatStyleOpen(
self: PageFormatter,
+ comptime emit: Format,
writer: *std.Io.Writer,
style: *const Style,
) std.Io.Writer.Error!void {
- switch (self.opts.emit) {
+ switch (emit) {
.plain => unreachable,
.vt => {
var formatter = style.formatterVt();
formatter.palette = self.opts.palette;
- try writer.print("{f}", .{formatter});
+ try formatter.format(writer);
},
// We use `display: inline` so that the div doesn't impact
@@ -1512,19 +2041,19 @@ pub const PageFormatter = struct {
.html => {
var formatter = style.formatterHtml();
formatter.palette = self.opts.palette;
- try writer.print(
- "<div style=\"display: inline;{f}\">",
- .{formatter},
- );
+ try writer.writeAll("<div style=\"display: inline;");
+ try formatter.format(writer);
+ try writer.writeAll("\">");
},
}
}
fn formatStyleClose(
self: PageFormatter,
+ comptime emit: Format,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
- const str: []const u8 = switch (self.opts.emit) {
+ const str: []const u8 = switch (emit) {
.plain => return,
.vt => "\x1b[0m",
.html => "</div>",
@@ -1546,16 +2075,18 @@ pub const PageFormatter = struct {
fn formatHyperlinkOpen(
self: PageFormatter,
+ comptime emit: Format,
writer: *std.Io.Writer,
uri: []const u8,
) std.Io.Writer.Error!void {
- switch (self.opts.emit) {
+ switch (emit) {
.plain, .vt => unreachable,
// layout since we're primarily using it as a CSS wrapper.
.html => {
try writer.writeAll("<a href=\"");
for (uri) |byte| try self.writeCodepoint(
+ emit,
writer,
byte,
);
@@ -1566,9 +2097,10 @@ pub const PageFormatter = struct {
fn formatHyperlinkClose(
self: PageFormatter,
+ comptime emit: Format,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
- const str: []const u8 = switch (self.opts.emit) {
+ const str: []const u8 = switch (emit) {
.html => "</a>",
.plain, .vt => return,
};
@@ -1591,11 +2123,12 @@ pub const PageFormatter = struct {
test "Page plain single line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -1638,11 +2171,12 @@ test "Page plain single line" {
test "Page plain single line soft-wrapped unwrapped" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 3,
.rows = 5,
});
@@ -1708,11 +2242,12 @@ test "Page plain single line soft-wrapped unwrapped" {
test "Page plain single wide char" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -1799,11 +2334,12 @@ test "Page plain single wide char" {
test "Page plain single wide char soft-wrapped unwrapped" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 3,
.rows = 24,
});
@@ -1916,11 +2452,12 @@ test "Page plain single wide char soft-wrapped unwrapped" {
test "Page plain multiline" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -1967,11 +2504,12 @@ test "Page plain multiline" {
test "Page plain multiline rectangle" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2021,11 +2559,12 @@ test "Page plain multiline rectangle" {
test "Page plain multi blank lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2074,11 +2613,12 @@ test "Page plain multi blank lines" {
test "Page plain trailing blank lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2127,11 +2667,12 @@ test "Page plain trailing blank lines" {
test "Page plain trailing whitespace" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2180,11 +2721,12 @@ test "Page plain trailing whitespace" {
test "Page plain trailing whitespace no trim" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2236,11 +2778,12 @@ test "Page plain trailing whitespace no trim" {
test "Page plain with prior trailing state rows" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2282,11 +2825,12 @@ test "Page plain with prior trailing state rows" {
test "Page plain with prior trailing state cells no wrapped line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2327,11 +2871,12 @@ test "Page plain with prior trailing state cells no wrapped line" {
test "Page plain with prior trailing state cells with wrap continuation" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2381,11 +2926,12 @@ test "Page plain with prior trailing state cells with wrap continuation" {
test "Page plain soft-wrapped without unwrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 24,
});
@@ -2430,11 +2976,12 @@ test "Page plain soft-wrapped without unwrap" {
test "Page plain soft-wrapped with unwrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 24,
});
@@ -2478,11 +3025,12 @@ test "Page plain soft-wrapped with unwrap" {
test "Page plain soft-wrapped 3 lines without unwrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 24,
});
@@ -2532,11 +3080,12 @@ test "Page plain soft-wrapped 3 lines without unwrap" {
test "Page plain soft-wrapped 3 lines with unwrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 24,
});
@@ -2584,11 +3133,12 @@ test "Page plain soft-wrapped 3 lines with unwrap" {
test "Page plain start_y subset" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2631,11 +3181,12 @@ test "Page plain start_y subset" {
test "Page plain end_y subset" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2678,11 +3229,12 @@ test "Page plain end_y subset" {
test "Page plain start_y and end_y range" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2726,11 +3278,12 @@ test "Page plain start_y and end_y range" {
test "Page plain start_y out of bounds" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2764,11 +3317,12 @@ test "Page plain start_y out of bounds" {
test "Page plain end_y greater than rows" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2807,11 +3361,12 @@ test "Page plain end_y greater than rows" {
test "Page plain end_y less than start_y" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2846,11 +3401,12 @@ test "Page plain end_y less than start_y" {
test "Page plain start_x on first row only" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2888,11 +3444,12 @@ test "Page plain start_x on first row only" {
test "Page plain end_x on last row only" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2941,11 +3498,12 @@ test "Page plain end_x on last row only" {
test "Page plain start_x and end_x multiline" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -2998,11 +3556,12 @@ test "Page plain start_x and end_x multiline" {
test "Page plain start_x out of bounds" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3036,11 +3595,12 @@ test "Page plain start_x out of bounds" {
test "Page plain end_x greater than cols" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3078,11 +3638,12 @@ test "Page plain end_x greater than cols" {
test "Page plain end_x less than start_x single row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3118,11 +3679,12 @@ test "Page plain end_x less than start_x single row" {
test "Page plain start_y non-zero ignores trailing state" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3162,11 +3724,12 @@ test "Page plain start_y non-zero ignores trailing state" {
test "Page plain start_x non-zero ignores trailing state" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3206,11 +3769,12 @@ test "Page plain start_x non-zero ignores trailing state" {
test "Page plain start_y and start_x zero uses trailing state" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3253,11 +3817,12 @@ test "Page plain start_y and start_x zero uses trailing state" {
test "Page plain single line with styling" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3299,11 +3864,12 @@ test "Page plain single line with styling" {
test "Page VT single line plain text" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3338,11 +3904,12 @@ test "Page VT single line plain text" {
test "Page VT single line with bold" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3384,11 +3951,12 @@ test "Page VT single line with bold" {
test "Page VT multiple styles" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3419,11 +3987,12 @@ test "Page VT multiple styles" {
test "Page VT with foreground color" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3465,11 +4034,12 @@ test "Page VT with foreground color" {
test "Page VT with background and foreground colors" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3502,11 +4072,12 @@ test "Page VT with background and foreground colors" {
test "Page VT multi-line with styles" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3539,11 +4110,12 @@ test "Page VT multi-line with styles" {
test "Page VT duplicate style not emitted twice" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3574,11 +4146,12 @@ test "Page VT duplicate style not emitted twice" {
test "PageList plain single line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3589,7 +4162,7 @@ test "PageList plain single line" {
s.nextSlice("hello, world");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(&t.screens.active.pages, .plain);
@@ -3599,22 +4172,23 @@ test "PageList plain single line" {
try testing.expectEqualStrings("hello, world", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| try testing.expectEqual(
Pin{ .node = node, .x = @intCast(i), .y = 0 },
- pin_map.items[i],
+ pin_map.get(i).?,
);
}
test "PageList plain spanning two pages" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3641,7 +4215,7 @@ test "PageList plain spanning two pages" {
s.nextSlice("page two");
// Format the entire PageList
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .plain);
@@ -3652,42 +4226,43 @@ test "PageList plain spanning two pages" {
try testing.expectEqualStrings("page one\npage two", output);
// Verify pin map
- try testing.expectEqual(full_output.len, pin_map.items.len);
+ try testing.expectEqual(full_output.len, pin_map.count());
const first_node = pages.pages.first.?;
const last_node = pages.pages.last.?;
const trimmed_count = full_output.len - output.len;
// First part (trimmed blank lines) maps to first node
for (0..trimmed_count) |i| {
- try testing.expectEqual(first_node, pin_map.items[i].node);
+ try testing.expectEqual(first_node, pin_map.get(i).?.node);
}
// "page one" (8 chars) maps to first node
for (0..8) |i| {
const idx = trimmed_count + i;
- try testing.expectEqual(first_node, pin_map.items[idx].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[idx].x);
+ try testing.expectEqual(first_node, pin_map.get(idx).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(idx).?.x);
}
// \n - maps to last node as it represents the transition to new page
- try testing.expectEqual(last_node, pin_map.items[trimmed_count + 8].node);
+ try testing.expectEqual(last_node, pin_map.get(trimmed_count + 8).?.node);
// "page two" (8 chars) maps to last node
for (0..8) |i| {
const idx = trimmed_count + 9 + i;
- try testing.expectEqual(last_node, pin_map.items[idx].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[idx].x);
+ try testing.expectEqual(last_node, pin_map.get(idx).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(idx).?.x);
}
}
test "PageList soft-wrapped line spanning two pages without unwrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 3,
});
@@ -3707,7 +4282,7 @@ test "PageList soft-wrapped line spanning two pages without unwrap" {
try testing.expect(pages.pages.first != pages.pages.last);
// Format without unwrap - should show line breaks
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .plain);
@@ -3718,40 +4293,41 @@ test "PageList soft-wrapped line spanning two pages without unwrap" {
try testing.expectEqualStrings("hello worl\nd test", output);
// Verify pin map
- try testing.expectEqual(full_output.len, pin_map.items.len);
+ try testing.expectEqual(full_output.len, pin_map.count());
const first_node = pages.pages.first.?;
const last_node = pages.pages.last.?;
const trimmed_count = full_output.len - output.len;
// First part (trimmed blank lines) maps to first node
for (0..trimmed_count) |i| {
- try testing.expectEqual(first_node, pin_map.items[i].node);
+ try testing.expectEqual(first_node, pin_map.get(i).?.node);
}
// First line maps to first node
for (0..10) |i| {
const idx = trimmed_count + i;
- try testing.expectEqual(first_node, pin_map.items[idx].node);
+ try testing.expectEqual(first_node, pin_map.get(idx).?.node);
}
// \n - maps to last node as it represents the transition to new page
- try testing.expectEqual(last_node, pin_map.items[trimmed_count + 10].node);
+ try testing.expectEqual(last_node, pin_map.get(trimmed_count + 10).?.node);
// "d test" (6 chars) maps to last node
for (0..6) |i| {
const idx = trimmed_count + 11 + i;
- try testing.expectEqual(last_node, pin_map.items[idx].node);
+ try testing.expectEqual(last_node, pin_map.get(idx).?.node);
}
}
test "PageList soft-wrapped line spanning two pages with unwrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 3,
});
@@ -3771,7 +4347,7 @@ test "PageList soft-wrapped line spanning two pages with unwrap" {
try testing.expect(pages.pages.first != pages.pages.last);
// Format with unwrap - should join the wrapped lines
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .{ .emit = .plain, .unwrap = true });
@@ -3782,37 +4358,38 @@ test "PageList soft-wrapped line spanning two pages with unwrap" {
try testing.expectEqualStrings("hello world test", output);
// Verify pin map
- try testing.expectEqual(full_output.len, pin_map.items.len);
+ try testing.expectEqual(full_output.len, pin_map.count());
const first_node = pages.pages.first.?;
const last_node = pages.pages.last.?;
const trimmed_count = full_output.len - output.len;
// First part (trimmed blank lines) maps to first node
for (0..trimmed_count) |i| {
- try testing.expectEqual(first_node, pin_map.items[i].node);
+ try testing.expectEqual(first_node, pin_map.get(i).?.node);
}
// First line from first page
for (0..10) |i| {
const idx = trimmed_count + i;
- try testing.expectEqual(first_node, pin_map.items[idx].node);
+ try testing.expectEqual(first_node, pin_map.get(idx).?.node);
}
// "d test" (6 chars) from last page
for (0..6) |i| {
const idx = trimmed_count + 10 + i;
- try testing.expectEqual(last_node, pin_map.items[idx].node);
+ try testing.expectEqual(last_node, pin_map.get(idx).?.node);
}
}
test "PageList VT spanning two pages" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3839,7 +4416,7 @@ test "PageList VT spanning two pages" {
s.nextSlice("page two");
// Format the entire PageList with VT
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .vt);
@@ -3850,14 +4427,15 @@ test "PageList VT spanning two pages" {
try testing.expectEqualStrings("\x1b[0m\x1b[1mpage one\x1b[0m\r\n\x1b[0m\x1b[1mpage two\x1b[0m", output);
// Verify pin map
- try testing.expectEqual(full_output.len, pin_map.items.len);
+ try testing.expectEqual(full_output.len, pin_map.count());
const first_node = pages.pages.first.?;
const last_node = pages.pages.last.?;
// Just verify we have entries for both pages in the pin map
var first_count: usize = 0;
var last_count: usize = 0;
- for (pin_map.items) |pin| {
+ for (0..pin_map.count()) |byte_i| {
+ const pin = pin_map.get(byte_i).?;
if (pin.node == first_node) first_count += 1;
if (pin.node == last_node) last_count += 1;
}
@@ -3868,11 +4446,12 @@ test "PageList VT spanning two pages" {
test "PageList plain with x offset on single page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3886,7 +4465,7 @@ test "PageList plain with x offset on single page" {
const pages = &t.screens.active.pages;
const node = pages.pages.first.?;
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .plain);
@@ -3899,26 +4478,28 @@ test "PageList plain with x offset on single page" {
try testing.expectEqualStrings("world\ntest case\nfoo", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
- for (pin_map.items) |pin| {
+ try testing.expectEqual(output.len, pin_map.count());
+ for (0..pin_map.count()) |byte_i| {
+ const pin = pin_map.get(byte_i).?;
try testing.expectEqual(node, pin.node);
}
// "world" starts at x=6, y=0
for (0..5) |i| {
- try testing.expectEqual(@as(size.CellCountInt, @intCast(6 + i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.items[i].y);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(6 + i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.get(i).?.y);
}
}
test "PageList plain with x offset spanning two pages" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -3946,7 +4527,7 @@ test "PageList plain with x offset spanning two pages" {
const first_node = pages.pages.first.?;
const last_node = pages.pages.last.?;
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .plain);
@@ -3960,35 +4541,36 @@ test "PageList plain with x offset spanning two pages" {
try testing.expectEqualStrings("world\nfoo", output);
// Verify pin map
- try testing.expectEqual(full_output.len, pin_map.items.len);
+ try testing.expectEqual(full_output.len, pin_map.count());
const trimmed_count = full_output.len - output.len;
// "world" (5 chars) from first page
for (0..5) |i| {
const idx = trimmed_count + i;
- try testing.expectEqual(first_node, pin_map.items[idx].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(6 + i)), pin_map.items[idx].x);
+ try testing.expectEqual(first_node, pin_map.get(idx).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(6 + i)), pin_map.get(idx).?.x);
}
// \n - maps to last node as it represents the transition to new page
- try testing.expectEqual(last_node, pin_map.items[trimmed_count + 5].node);
+ try testing.expectEqual(last_node, pin_map.get(trimmed_count + 5).?.node);
// "foo" (3 chars) from last page
for (0..3) |i| {
const idx = trimmed_count + 6 + i;
- try testing.expectEqual(last_node, pin_map.items[idx].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[idx].x);
+ try testing.expectEqual(last_node, pin_map.get(idx).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(idx).?.x);
}
}
test "PageList plain with start_x only" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4002,7 +4584,7 @@ test "PageList plain with start_x only" {
const pages = &t.screens.active.pages;
const node = pages.pages.first.?;
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .plain);
@@ -4014,22 +4596,23 @@ test "PageList plain with start_x only" {
try testing.expectEqualStrings("world", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
for (0..5) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(6 + i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.items[i].y);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(6 + i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.get(i).?.y);
}
}
test "PageList plain with end_x only" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4043,7 +4626,7 @@ test "PageList plain with end_x only" {
const pages = &t.screens.active.pages;
const node = pages.pages.first.?;
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: PageListFormatter = .init(pages, .plain);
@@ -4055,34 +4638,35 @@ test "PageList plain with end_x only" {
try testing.expectEqualStrings("hello world\ntes", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
// "hello world" (11 chars) on y=0
for (0..11) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.items[i].y);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.get(i).?.y);
}
// \n
- try testing.expectEqual(node, pin_map.items[11].node);
+ try testing.expectEqual(node, pin_map.get(11).?.node);
// "tes" (3 chars) on y=1
for (0..3) |i| {
- try testing.expectEqual(node, pin_map.items[12 + i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[12 + i].x);
- try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.items[12 + i].y);
+ try testing.expectEqual(node, pin_map.get(12 + i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(12 + i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.get(12 + i).?.y);
}
}
test "PageList plain rectangle basic" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 30,
.rows = 5,
});
@@ -4118,11 +4702,12 @@ test "PageList plain rectangle basic" {
test "PageList plain rectangle with EOL" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 30,
.rows = 5,
});
@@ -4160,11 +4745,12 @@ test "PageList plain rectangle with EOL" {
test "PageList plain rectangle more complex with breaks" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 30,
.rows = 8,
});
@@ -4206,11 +4792,12 @@ test "PageList plain rectangle more complex with breaks" {
test "TerminalFormatter plain no selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4230,11 +4817,12 @@ test "TerminalFormatter plain no selection" {
test "TerminalFormatter vt with palette" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4255,7 +4843,7 @@ test "TerminalFormatter vt with palette" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4275,11 +4863,12 @@ test "TerminalFormatter vt with palette" {
test "TerminalFormatter with selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4304,11 +4893,12 @@ test "TerminalFormatter with selection" {
test "TerminalFormatter plain with pin_map" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4319,7 +4909,7 @@ test "TerminalFormatter plain with pin_map" {
s.nextSlice("hello, world");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: TerminalFormatter = .init(&t, .plain);
@@ -4330,22 +4920,23 @@ test "TerminalFormatter plain with pin_map" {
try testing.expectEqualStrings("hello, world", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| try testing.expectEqual(
Pin{ .node = node, .x = @intCast(i), .y = 0 },
- pin_map.items[i],
+ pin_map.get(i).?,
);
}
test "TerminalFormatter plain multiline with pin_map" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4356,7 +4947,7 @@ test "TerminalFormatter plain multiline with pin_map" {
s.nextSlice("hello\r\nworld");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: TerminalFormatter = .init(&t, .plain);
@@ -4367,33 +4958,34 @@ test "TerminalFormatter plain multiline with pin_map" {
try testing.expectEqualStrings("hello\nworld", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
// "hello" (5 chars)
for (0..5) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.items[i].y);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.get(i).?.y);
}
// "\n" maps to end of first line
- try testing.expectEqual(node, pin_map.items[5].node);
+ try testing.expectEqual(node, pin_map.get(5).?.node);
// "world" (5 chars)
for (0..5) |i| {
const idx = 6 + i;
- try testing.expectEqual(node, pin_map.items[idx].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[idx].x);
- try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.items[idx].y);
+ try testing.expectEqual(node, pin_map.get(idx).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(idx).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.get(idx).?.y);
}
}
test "TerminalFormatter vt with palette and pin_map" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4406,7 +4998,7 @@ test "TerminalFormatter vt with palette and pin_map" {
s.nextSlice("\x1b]4;0;rgb:12/34/56\x1b\\");
s.nextSlice("test");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: TerminalFormatter = .init(&t, .vt);
@@ -4416,21 +5008,22 @@ test "TerminalFormatter vt with palette and pin_map" {
const output = builder.writer.buffered();
// Verify pin map - palette bytes should be mapped to top left
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
test "TerminalFormatter with selection and pin_map" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4441,7 +5034,7 @@ test "TerminalFormatter with selection and pin_map" {
s.nextSlice("line1\r\nline2\r\nline3");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: TerminalFormatter = .init(&t, .plain);
@@ -4457,24 +5050,25 @@ test "TerminalFormatter with selection and pin_map" {
try testing.expectEqualStrings("line2", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
// "line2" (5 chars) from row 1
for (0..5) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.items[i].y);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.get(i).?.y);
}
}
test "Screen plain single line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4485,7 +5079,7 @@ test "Screen plain single line" {
s.nextSlice("hello, world");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .plain);
@@ -4496,22 +5090,23 @@ test "Screen plain single line" {
try testing.expectEqualStrings("hello, world", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| try testing.expectEqual(
Pin{ .node = node, .x = @intCast(i), .y = 0 },
- pin_map.items[i],
+ pin_map.get(i).?,
);
}
test "Screen plain multiline" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4522,7 +5117,7 @@ test "Screen plain multiline" {
s.nextSlice("hello\r\nworld");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .plain);
@@ -4533,33 +5128,34 @@ test "Screen plain multiline" {
try testing.expectEqualStrings("hello\nworld", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
// "hello" (5 chars)
for (0..5) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.items[i].y);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 0), pin_map.get(i).?.y);
}
// "\n" maps to end of first line
- try testing.expectEqual(node, pin_map.items[5].node);
+ try testing.expectEqual(node, pin_map.get(5).?.node);
// "world" (5 chars)
for (0..5) |i| {
const idx = 6 + i;
- try testing.expectEqual(node, pin_map.items[idx].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[idx].x);
- try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.items[idx].y);
+ try testing.expectEqual(node, pin_map.get(idx).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(idx).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.get(idx).?.y);
}
}
test "Screen plain with selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4570,7 +5166,7 @@ test "Screen plain with selection" {
s.nextSlice("line1\r\nline2\r\nline3");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .plain);
@@ -4586,24 +5182,25 @@ test "Screen plain with selection" {
try testing.expectEqualStrings("line2", output);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
// "line2" (5 chars) from row 1
for (0..5) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
- try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.items[i].x);
- try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.items[i].y);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
+ try testing.expectEqual(@as(size.CellCountInt, @intCast(i)), pin_map.get(i).?.x);
+ try testing.expectEqual(@as(size.CellCountInt, 1), pin_map.get(i).?.y);
}
}
test "Screen vt with cursor position" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4615,7 +5212,7 @@ test "Screen vt with cursor position" {
// Position cursor at a specific location
s.nextSlice("hello\r\nworld");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .vt);
@@ -4626,7 +5223,7 @@ test "Screen vt with cursor position" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4642,27 +5239,87 @@ test "Screen vt with cursor position" {
try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y);
// Verify pin map - the extras should be mapped to the last pin
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
const content_len = "hello\r\nworld".len;
// Content bytes map to their positions
for (0..content_len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
// Extra bytes (cursor position) map to last content pin
for (content_len..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
+test "Terminal vt cursor preserves pending wrap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var builder: std.Io.Writer.Allocating = .init(alloc);
+ defer builder.deinit();
+
+ var source = try Terminal.init(io, alloc, .{
+ .cols = 4,
+ .rows = 2,
+ });
+ defer source.deinit(alloc);
+
+ var source_stream = source.vtStream();
+ defer source_stream.deinit();
+ source_stream.nextSlice("abcd");
+ try testing.expect(source.screens.active.cursor.pending_wrap);
+
+ var pin_map: PinMap.Map = .empty;
+ defer pin_map.deinit(alloc);
+
+ var formatter: TerminalFormatter = .init(&source, .vt);
+ formatter.extra = .none;
+ formatter.extra.screen.cursor = true;
+ formatter.pin_map = .{ .alloc = alloc, .map = &pin_map };
+ try formatter.format(&builder.writer);
+ try testing.expectEqual(builder.writer.buffered().len, pin_map.count());
+
+ var target = try Terminal.init(io, alloc, .{
+ .cols = 4,
+ .rows = 2,
+ });
+ defer target.deinit(alloc);
+
+ var target_stream = target.vtStream();
+ defer target_stream.deinit();
+ target_stream.nextSlice(builder.writer.buffered());
+
+ try testing.expectEqual(
+ source.screens.active.cursor.pending_wrap,
+ target.screens.active.cursor.pending_wrap,
+ );
+
+ source_stream.nextSlice("X");
+ target_stream.nextSlice("X");
+ const source_contents = try source.screens.active.dumpStringAlloc(
+ alloc,
+ .{ .screen = .{} },
+ );
+ defer alloc.free(source_contents);
+ const target_contents = try target.screens.active.dumpStringAlloc(
+ alloc,
+ .{ .screen = .{} },
+ );
+ defer alloc.free(target_contents);
+ try testing.expectEqualStrings(source_contents, target_contents);
+}
+
test "Screen vt with style" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4674,7 +5331,7 @@ test "Screen vt with style" {
// Set some style attributes
s.nextSlice("\x1b[1;31mhello");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .vt);
@@ -4685,7 +5342,7 @@ test "Screen vt with style" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4700,21 +5357,22 @@ test "Screen vt with style" {
try testing.expect(t.screens.active.cursor.style.eql(t2.screens.active.cursor.style));
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
test "Screen vt with hyperlink" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4726,7 +5384,7 @@ test "Screen vt with hyperlink" {
// Set a hyperlink
s.nextSlice("\x1b]8;;http://example.com\x1b\\hello");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .vt);
@@ -4737,7 +5395,7 @@ test "Screen vt with hyperlink" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4760,21 +5418,22 @@ test "Screen vt with hyperlink" {
}
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
test "Screen vt with protection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4786,7 +5445,7 @@ test "Screen vt with protection" {
// Enable protection mode
s.nextSlice("\x1b[1\"qhello");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .vt);
@@ -4797,7 +5456,7 @@ test "Screen vt with protection" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4812,21 +5471,22 @@ test "Screen vt with protection" {
try testing.expectEqual(t.screens.active.cursor.protected, t2.screens.active.cursor.protected);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
test "Screen vt with kitty keyboard" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4838,7 +5498,7 @@ test "Screen vt with kitty keyboard" {
// Set kitty keyboard flags (disambiguate + report_events = 3)
s.nextSlice("\x1b[=3;1uhello");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .vt);
@@ -4849,7 +5509,7 @@ test "Screen vt with kitty keyboard" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4866,21 +5526,22 @@ test "Screen vt with kitty keyboard" {
try testing.expectEqual(flags1, flags2);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
test "Screen vt with charsets" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4892,7 +5553,7 @@ test "Screen vt with charsets" {
// Set G0 to DEC special and shift to G1
s.nextSlice("\x1b(0\x0ehello");
- var pin_map: std.ArrayList(Pin) = .empty;
+ var pin_map: PinMap.Map = .empty;
defer pin_map.deinit(alloc);
var formatter: ScreenFormatter = .init(t.screens.active, .vt);
@@ -4903,7 +5564,7 @@ test "Screen vt with charsets" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4923,21 +5584,22 @@ test "Screen vt with charsets" {
);
// Verify pin map
- try testing.expectEqual(output.len, pin_map.items.len);
+ try testing.expectEqual(output.len, pin_map.count());
const node = t.screens.active.pages.pages.first.?;
for (0..output.len) |i| {
- try testing.expectEqual(node, pin_map.items[i].node);
+ try testing.expectEqual(node, pin_map.get(i).?.node);
}
}
test "Terminal vt with scrolling region" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4956,7 +5618,7 @@ test "Terminal vt with scrolling region" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -4977,11 +5639,12 @@ test "Terminal vt with scrolling region" {
test "Terminal vt with modes" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5003,7 +5666,7 @@ test "Terminal vt with modes" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5023,11 +5686,12 @@ test "Terminal vt with modes" {
test "Terminal vt with tabstops" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5041,16 +5705,21 @@ test "Terminal vt with tabstops" {
s.nextSlice("\x1b[5G\x1bH"); // Set tab at column 5
s.nextSlice("\x1b[15G\x1bH"); // Set tab at column 15
s.nextSlice("\x1b[30G\x1bH"); // Set tab at column 30
- s.nextSlice("hello");
+ s.nextSlice("\x1b[Hhello");
+
+ var pin_map: PinMap.Map = .empty;
+ defer pin_map.deinit(alloc);
var formatter: TerminalFormatter = .init(&t, .vt);
formatter.extra.tabstops = true;
+ formatter.extra.screen.cursor = true;
+ formatter.pin_map = .{ .alloc = alloc, .map = &pin_map };
try formatter.format(&builder.writer);
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5069,16 +5738,33 @@ test "Terminal vt with tabstops" {
try testing.expect(t2.tabstops.get(14)); // Column 15 (1-indexed)
try testing.expect(t2.tabstops.get(29)); // Column 30 (1-indexed)
try testing.expect(!t2.tabstops.get(8)); // Not a tab
+
+ // Tabstop serialization must not offset the screen contents.
+ for ("hello", 0..) |expected, col| {
+ const cell = t2.screens.active.pages.getCell(.{
+ .screen = .{ .x = @intCast(col), .y = 0 },
+ }).?;
+ try testing.expectEqual(expected, cell.cell.codepoint());
+ }
+
+ // Emitting tabstops moves the cursor to each configured column. When
+ // cursor state is included, it must be restored afterwards.
+ try testing.expectEqual(t.screens.active.cursor.x, t2.screens.active.cursor.x);
+ try testing.expectEqual(t.screens.active.cursor.y, t2.screens.active.cursor.y);
+
+ // Verify the reordered terminal state is still represented in the map.
+ try testing.expectEqual(output.len, pin_map.count());
}
test "Terminal vt with keyboard modes" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5098,7 +5784,7 @@ test "Terminal vt with keyboard modes" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5117,11 +5803,12 @@ test "Terminal vt with keyboard modes" {
test "Terminal vt with pwd" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5140,7 +5827,7 @@ test "Terminal vt with pwd" {
const output = builder.writer.buffered();
// Create a second terminal and apply the output
- var t2 = try Terminal.init(alloc, .{
+ var t2 = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5158,11 +5845,12 @@ test "Terminal vt with pwd" {
test "Page html with multiple styles" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5194,11 +5882,12 @@ test "Page html with multiple styles" {
test "Page html plain text" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5226,11 +5915,12 @@ test "Page html plain text" {
test "Page html with colors" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5260,11 +5950,12 @@ test "Page html with colors" {
test "TerminalFormatter html with palette" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5297,11 +5988,12 @@ test "TerminalFormatter html with palette" {
test "Page html with background and foreground colors" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5332,11 +6024,12 @@ test "Page html with background and foreground colors" {
test "Page html with escaping" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5402,11 +6095,12 @@ test "Page html with escaping" {
test "Page html with unicode as numeric entities" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5433,14 +6127,52 @@ test "Page html with unicode as numeric entities" {
);
}
+test "Page html trailing blank lines" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var builder: std.Io.Writer.Allocating = .init(alloc);
+ defer builder.deinit();
+
+ var t = try Terminal.init(io, alloc, .{
+ .cols = 80,
+ .rows = 24,
+ });
+ defer t.deinit(alloc);
+
+ var s = t.vtStream();
+ defer s.deinit();
+
+ s.nextSlice("hello\r\nworld\r\n\r\n");
+
+ const pages = &t.screens.active.pages;
+ try testing.expect(pages.pages.first != null);
+ try testing.expect(pages.pages.first == pages.pages.last);
+
+ const page = pages.pages.last.?.page();
+ var formatter: PageFormatter = .init(page, .{ .emit = .html });
+
+ const state = try formatter.formatWithState(&builder.writer);
+ const output = builder.writer.buffered();
+
+ // The closing div behaves as a newline
+ try testing.expectEqual(@as(usize, page.size.rows - 2), state.rows);
+ try testing.expectEqualStrings(
+ "<div style=\"font-family: monospace; white-space: pre;\">hello\nworld</div>",
+ output,
+ );
+}
+
test "Page html ascii characters unchanged" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5468,11 +6200,12 @@ test "Page html ascii characters unchanged" {
test "Page html mixed ascii and unicode" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5500,11 +6233,12 @@ test "Page html mixed ascii and unicode" {
test "Page VT with palette option emits RGB" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5544,11 +6278,12 @@ test "Page VT with palette option emits RGB" {
test "Page html with palette option emits RGB" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5598,11 +6333,12 @@ test "Page html with palette option emits RGB" {
test "Page VT style reset properly closes styles" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5629,11 +6365,12 @@ test "Page VT style reset properly closes styles" {
test "Page codepoint_map single replacement" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5688,11 +6425,12 @@ test "Page codepoint_map single replacement" {
test "Page codepoint_map conflicting replacement prefers last" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5730,11 +6468,12 @@ test "Page codepoint_map conflicting replacement prefers last" {
test "Page codepoint_map replace with string" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5786,11 +6525,12 @@ test "Page codepoint_map replace with string" {
test "Page codepoint_map range replacement" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5824,11 +6564,12 @@ test "Page codepoint_map range replacement" {
test "Page codepoint_map multiple ranges" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5868,11 +6609,12 @@ test "Page codepoint_map multiple ranges" {
test "Page codepoint_map unicode replacement" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -5933,11 +6675,12 @@ test "Page codepoint_map unicode replacement" {
test "Page codepoint_map with styled formats" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 10,
.rows = 24,
});
@@ -5974,11 +6717,12 @@ test "Page codepoint_map with styled formats" {
test "Page codepoint_map empty map" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -6011,11 +6755,12 @@ test "Page VT background color on trailing blank cells" {
// This causes TUIs like htop to lose background colors on rehydration.
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 20,
.rows = 5,
});
@@ -6062,11 +6807,12 @@ test "Page VT background color on trailing blank cells" {
test "Page HTML with hyperlinks" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -6096,11 +6842,12 @@ test "Page HTML with hyperlinks" {
test "Page HTML with multiple hyperlinks" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -6133,11 +6880,12 @@ test "Page HTML with multiple hyperlinks" {
test "Page HTML with hyperlink escaping" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -6167,11 +6915,12 @@ test "Page HTML with hyperlink escaping" {
test "Page HTML with styled hyperlink" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -6202,11 +6951,12 @@ test "Page HTML with styled hyperlink" {
test "Page HTML hyperlink closes style before anchor" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});
@@ -6237,11 +6987,12 @@ test "Page HTML hyperlink closes style before anchor" {
test "Page HTML hyperlink point map maps closing to previous cell" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
var builder: std.Io.Writer.Allocating = .init(alloc);
defer builder.deinit();
- var t = try Terminal.init(alloc, .{
+ var t = try Terminal.init(io, alloc, .{
.cols = 80,
.rows = 24,
});