aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/Screen.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/Screen.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/Screen.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/Screen.zig2078
1 files changed, 1603 insertions, 475 deletions
diff --git a/vendor/libghostty-vt/src/terminal/Screen.zig b/vendor/libghostty-vt/src/terminal/Screen.zig
index 6c4db323..208e276a 100644
--- a/vendor/libghostty-vt/src/terminal/Screen.zig
+++ b/vendor/libghostty-vt/src/terminal/Screen.zig
@@ -8,6 +8,7 @@ const ansi = @import("ansi.zig");
const charsets = @import("charsets.zig");
const fastmem = @import("../fastmem.zig");
const kitty = @import("kitty.zig");
+const lib = @import("lib.zig");
const sgr = @import("sgr.zig");
const tripwire = @import("../tripwire.zig");
const unicode = @import("../unicode/main.zig");
@@ -16,6 +17,7 @@ const PageList = @import("PageList.zig");
const selection_codepoints = @import("selection_codepoints.zig");
const StringMap = @import("StringMap.zig");
const ScreenFormatter = @import("formatter.zig").ScreenFormatter;
+const PinMap = @import("formatter.zig").PinMap;
const osc = @import("osc.zig");
const pagepkg = @import("page.zig");
const point = @import("point.zig");
@@ -32,6 +34,9 @@ pub const CursorStyle = @import("cursor.zig").Style;
const log = std.log.scoped(.screen);
+/// The I/O implementation to use for any operation that requires one.
+io: std.Io,
+
/// The general purpose allocator to use for all memory allocations.
/// Unfortunately some screen operations do require allocation.
alloc: Allocator,
@@ -111,7 +116,12 @@ pub const SemanticPrompt = struct {
.click = .none,
};
- pub const SemanticClick = union(enum) {
+ pub const SemanticClickKind = lib.Enum(
+ lib.target,
+ &.{ "none", "click_events", "cl" },
+ );
+
+ pub const SemanticClick = union(SemanticClickKind) {
none,
click_events: osc.semantic_prompt.ClickEvents,
cl: osc.semantic_prompt.Click,
@@ -250,9 +260,15 @@ pub const Options = struct {
cols: size.CellCountInt,
rows: size.CellCountInt,
- /// The maximum size of scrollback in bytes. Zero means unlimited. Any
- /// other value will be clamped to support a minimum of the active area.
- max_scrollback: usize = 0,
+ /// The maximum size of scrollback in bytes. Null is unlimited, zero
+ /// disables scrollback, and any other value is clamped to support a
+ /// minimum of the active area.
+ max_scrollback_bytes: ?usize = 0,
+
+ /// The maximum number of physical scrollback rows, excluding the active
+ /// area. Null is unlimited. The effective limit permits at least one
+ /// standard page and only complete historical pages are pruned.
+ max_scrollback_lines: ?usize = null,
/// The total storage limit for Kitty images in bytes for this
/// screen. Kitty image storage is per-screen.
@@ -273,28 +289,29 @@ pub const Options = struct {
pub const default: Options = .{
.cols = 80,
.rows = 24,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
};
};
/// Initialize a new screen.
///
-/// max_scrollback is the amount of scrollback to keep in bytes. This
+/// max_scrollback_bytes is the amount of scrollback to keep in bytes. This
/// will be rounded UP to the nearest page size because our minimum allocation
/// size is that anyways.
///
/// If max scrollback is 0, then no scrollback is kept at all.
pub fn init(
+ io: std.Io,
alloc: Allocator,
opts: Options,
) Allocator.Error!Screen {
// Initialize our backing pages.
- var pages = try PageList.init(
- alloc,
- opts.cols,
- opts.rows,
- opts.max_scrollback,
- );
+ var pages = try PageList.init(alloc, .{
+ .cols = opts.cols,
+ .rows = opts.rows,
+ .max_size = opts.max_scrollback_bytes,
+ .max_lines = opts.max_scrollback_lines,
+ });
errdefer pages.deinit();
// Create our tracked pin for the cursor.
@@ -303,9 +320,10 @@ pub fn init(
const page_rac = page_pin.rowAndCell();
var result: Screen = .{
+ .io = io,
.alloc = alloc,
.pages = pages,
- .no_scrollback = opts.max_scrollback == 0,
+ .no_scrollback = opts.max_scrollback_bytes == 0,
.cursor = .{
.x = 0,
.y = 0,
@@ -316,13 +334,12 @@ pub fn init(
};
if (comptime build_options.kitty_graphics) {
- // This can't fail because the storage is always empty at this point
- // and the only fail-able case is that we have to evict images.
result.kitty_images.setLimit(
+ io,
alloc,
&result,
opts.kitty_image_storage_limit,
- ) catch unreachable;
+ );
result.kitty_images.image_limits = opts.kitty_image_loading_limits;
}
@@ -361,6 +378,16 @@ pub fn assertIntegrity(self: *const Screen) void {
) orelse unreachable;
assert(self.cursor.x == pt.active.x);
assert(self.cursor.y == pt.active.y);
+
+ // The cursor style and hyperlink if non-zero must reference
+ // real data in the page the pin is in.
+ const page: *const Page = self.cursor.page_pin.node.page();
+ if (self.cursor.style_id != style.default_id) {
+ assert(page.styles.refCount(page.memory, self.cursor.style_id) > 0);
+ }
+ if (self.cursor.hyperlink_id != 0) {
+ assert(page.hyperlink_set.refCount(page.memory, self.cursor.hyperlink_id) > 0);
+ }
}
}
@@ -380,11 +407,16 @@ pub fn reset(self: *Screen) void {
self.pages.reset();
// The above reset preserves tracked pins so we can still use
- // our cursor pin, which should be at the top-left already.
+ // our cursor pin, which should be at the top-left already. The
+ // reset marks every tracked pin as garbage, but we keep using
+ // this one at its new valid position, so clear the flag: copies
+ // of the cursor pin (e.g. for Kitty image placements) must not
+ // be born garbage.
const cursor_pin: *PageList.Pin = self.cursor.page_pin;
assert(cursor_pin.node == self.pages.pages.first.?);
assert(cursor_pin.x == 0);
assert(cursor_pin.y == 0);
+ cursor_pin.garbage = false;
const cursor_rac = cursor_pin.rowAndCell();
self.cursor.deinit(self.alloc);
self.cursor = .{
@@ -395,8 +427,14 @@ pub fn reset(self: *Screen) void {
if (comptime build_options.kitty_graphics) {
// Reset kitty graphics storage
+ const image_limits = self.kitty_images.image_limits;
+ const total_limit = self.kitty_images.total_limit;
self.kitty_images.deinit(self.alloc, self);
- self.kitty_images = .{ .dirty = true };
+ self.kitty_images = .{
+ .dirty = true,
+ .image_limits = image_limits,
+ .total_limit = total_limit,
+ };
}
// Reset our basic state
@@ -441,6 +479,7 @@ pub fn reset(self: *Screen) void {
///
pub fn clone(
self: *const Screen,
+ io: std.Io,
alloc: Allocator,
top: point.Point,
bot: ?point.Point,
@@ -565,6 +604,7 @@ pub fn clone(
} else null;
const result: Screen = .{
+ .io = io,
.alloc = alloc,
.pages = pages,
.no_scrollback = self.no_scrollback,
@@ -575,6 +615,7 @@ pub fn clone(
result.assertIntegrity();
return result;
}
+
pub fn increaseCapacity(
self: *Screen,
node: *PageList.List.Node,
@@ -645,6 +686,74 @@ pub fn increaseCapacity(
return new_node;
}
+/// Clone the cells in columns [x_start, x_end) of a source row into
+/// the row at `dst_y` of the given node's page, increasing the node's
+/// capacity as necessary to fit the managed memory (styles,
+/// hyperlinks, etc.) of the copied cells.
+///
+/// This is the Screen-level analog of PageList.cloneRowGrowCapacity:
+/// capacity increases are routed through Screen.increaseCapacity so
+/// that the cursor's style/hyperlink references are migrated when the
+/// destination node is the cursor's page.
+///
+/// Since increasing capacity replaces the node in the page list, the
+/// (possibly replaced) node is returned and the caller must use it in
+/// place of the old node. Tracked pins are updated automatically. The
+/// source must NOT be on the given node since the node's page memory
+/// may be freed on capacity increase.
+///
+/// Callers use this mid-mutation (after rows have been rotated or
+/// shifted), so a failure can't be propagated without leaving the
+/// page list half-mutated (and corrupt). If the capacity can't be
+/// increased (system OOM or the page is already at max capacity),
+/// this panics: a crash is better than corruption.
+pub fn clonePartialRowGrowCapacity(
+ self: *Screen,
+ node: *PageList.List.Node,
+ dst_y: usize,
+ src_page: *Page,
+ src_row: *const Row,
+ x_start: usize,
+ x_end: usize,
+) *PageList.List.Node {
+ assert(src_page != node.page());
+
+ var current = node;
+ while (true) {
+ const cur_page: *Page = current.page();
+ const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr);
+ cur_page.clonePartialRowFrom(
+ src_page,
+ &cur_rows[dst_y],
+ src_row,
+ x_start,
+ x_end,
+ ) catch |err| {
+ // Adjust our page capacity to make room for what we
+ // didn't have space for and retry the copy.
+ current = self.increaseCapacity(
+ current,
+ PageList.IncreaseCapacity.forCloneError(err),
+ ) catch |e| switch (e) {
+ // We can't gracefully recover from either of these
+ // here: our callers have already rotated or shifted
+ // rows, so returning an error would leave the page
+ // list half-mutated (and corrupt), so a crash is
+ // better.
+ error.OutOfMemory,
+ => @panic("increaseCapacity system allocator OOM"),
+
+ error.OutOfSpace,
+ => @panic("increaseCapacity OutOfSpace"),
+ };
+
+ continue;
+ };
+
+ return current;
+ }
+}
+
pub inline fn cursorCellRight(self: *Screen, n: size.CellCountInt) *pagepkg.Cell {
assert(self.cursor.x + n < self.pages.cols);
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
@@ -775,9 +884,31 @@ pub fn cursorReload(self: *Screen) void {
.active,
self.cursor.page_pin.*,
) orelse reset: {
+ // Our cached row/cell pointers may be invalid (that is often
+ // the reason cursorReload is being called), so refresh them
+ // from the pin first since cursorChangePin below marks the
+ // old cursor row as dirty.
+ const old_rac = self.cursor.page_pin.rowAndCell();
+ self.cursor.page_row = old_rac.row;
+ self.cursor.page_cell = old_rac.cell;
+
+ // The cursor style and hyperlink IDs are only valid within the
+ // page that the pin points at, so the pin change must go through
+ // cursorChangePin, which migrates them when the active top-left
+ // is on a different page. Writing the pin directly here would
+ // leave the cursor holding IDs that are dead or alias unrelated
+ // entries on the new page.
const pin = self.pages.pin(.{ .active = .{} }).?;
- self.cursor.page_pin.* = pin;
- break :reset self.pages.pointFromPin(.active, pin).?;
+ self.cursor.x = 0; // Must be set before cursorChangePin
+ self.cursor.y = 0;
+ self.cursorChangePin(pin);
+
+ // cursorChangePin can trigger a page capacity adjustment which
+ // moves the pin again, so we re-read it to derive our point.
+ break :reset self.pages.pointFromPin(
+ .active,
+ self.cursor.page_pin.*,
+ ).?;
};
self.cursor.x = @intCast(pt.active.x);
@@ -785,20 +916,6 @@ pub fn cursorReload(self: *Screen) void {
const page_rac = self.cursor.page_pin.rowAndCell();
self.cursor.page_row = page_rac.row;
self.cursor.page_cell = page_rac.cell;
-
- // If we have a style, we need to ensure it is in the page because this
- // method may also be called after a page change.
- if (self.cursor.style_id != style.default_id) {
- self.manualStyleUpdate() catch |err| {
- // This failure should not happen because manualStyleUpdate
- // handles page splitting, overflow, and more. This should only
- // happen if we're out of RAM. In this case, we'll just degrade
- // gracefully back to the default style.
- log.err("failed to update style on cursor reload err={}", .{err});
- self.cursor.style = .{};
- self.cursor.style_id = 0;
- };
- }
}
/// Scroll the active area and keep the cursor at the bottom of the screen.
@@ -824,6 +941,12 @@ pub fn cursorDownScroll(self: *Screen) !void {
self.cursor.page_row,
page.getCells(self.cursor.page_row),
);
+
+ // The row is a fresh blank row now and must not retain
+ // metadata (wrap state, semantic prompt) from the
+ // discarded content.
+ self.cursor.page_row.reset();
+
self.cursorMarkDirty();
} else {
// The call to `eraseRow` will move the tracked cursor pin up by one
@@ -1035,36 +1158,58 @@ fn cursorScrollAboveRotate(
self: *Screen,
fresh_node: ?*PageList.List.Node,
) !void {
+ // A fresh node given to us is always the newly allocated tail of
+ // the page list (see grow), which is where our iteration starts.
+ assert(fresh_node == null or fresh_node == self.pages.pages.last);
+
self.cursorChangePin(self.cursor.page_pin.down(1).?);
// Go through each of the pages following our pin, shift all rows
- // down by one, and copy the last row of the previous page.
+ // down by one, and copy the last row of the previous page. We start
+ // at the tail, which is the only node that can be freshly allocated.
var current = self.pages.pages.last.?;
- while (current != self.cursor.page_pin.node) : (current = current.prev.?) {
+ var current_is_fresh = fresh_node != null;
+ while (current != self.cursor.page_pin.node) : ({
+ current = current.prev.?;
+ current_is_fresh = false;
+ }) {
const prev = current.prev.?;
- const prev_page = prev.page();
- const cur_page = current.page();
- const prev_rows = prev_page.rows.ptr(prev_page.memory.ptr);
- const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr);
// A newly allocated tail has no earlier references to invalidate.
- if (fresh_node == null or current != fresh_node.?) {
+ if (!current_is_fresh) {
// Rotating this page moves every cached row coordinate down by one.
self.pages.invalidateNodeLayout(current);
}
// Rotate the pages down: [ 0 1 2 3 ] => [ 3 0 1 2 ]
- fastmem.rotateOnceR(Row, cur_rows[0..cur_page.size.rows]);
+ {
+ const cur_page = current.page();
+ const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr);
+ fastmem.rotateOnceR(Row, cur_rows[0..cur_page.size.rows]);
+ }
// Copy the last row of the previous page to the top of current.
- try cur_page.cloneRowFrom(
- prev_page,
- &cur_rows[0],
- &prev_rows[prev_page.size.rows - 1],
- );
+ // If the current page doesn't have enough capacity for the
+ // managed memory of the copied row (styles, hyperlinks, etc.)
+ // then its capacity is increased and the copy retried, which
+ // may replace `current` in the page list. Our loop condition
+ // guarantees `current` is never the cursor page, and `prev` is
+ // unaffected by the replacement.
+ {
+ const prev_page = prev.page();
+ const prev_rows = prev_page.rows.ptr(prev_page.memory.ptr);
+ current = self.clonePartialRowGrowCapacity(
+ current,
+ 0,
+ prev_page,
+ &prev_rows[prev_page.size.rows - 1],
+ 0,
+ self.pages.cols,
+ );
+ }
// Mark dirty on the page, since we are dirtying all rows with this.
- cur_page.dirty = true;
+ current.page().dirty = true;
}
// Our current is our cursor page, we need to rotate down from
@@ -1081,6 +1226,11 @@ fn cursorScrollAboveRotate(
cur_page.getCells(&cur_rows[self.cursor.page_pin.y]),
);
+ // The recycled storage becomes the new blank cursor row and must
+ // not retain metadata (wrap state, semantic prompt) from the row
+ // whose content was moved to the next page.
+ cur_rows[self.cursor.page_pin.y].reset();
+
// Mark the whole page as dirty.
//
// Technically we only need to mark from the cursor row to the
@@ -1154,6 +1304,11 @@ pub fn cursorScrollRegionUp(self: *Screen, limit: usize) !void {
// row with our blank cell, preserving the background color.
self.clearCells(page, row, page.getCells(row));
}
+
+ // The row becomes the new blank cursor row after the rotation
+ // below and must not retain metadata (wrap state, semantic
+ // prompt) from the discarded content.
+ row.reset();
}
// Rotate the region rows so the now-blank top row moves to the
@@ -1366,6 +1521,10 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void {
if (self.cursor.hyperlink != null) {
const old_page: *Page = self.cursor.page_pin.node.page();
old_page.hyperlink_set.release(old_page.memory, self.cursor.hyperlink_id);
+ // Zero the ID, it is invalid now and style changes below may
+ // run integrity checks. We still have self.cursor.hyperlink to
+ // rebuild this later.
+ self.cursor.hyperlink_id = 0;
}
// Update our pin to the new page
@@ -1387,8 +1546,9 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void {
// On the new page, we need to migrate our hyperlink
if (self.cursor.hyperlink) |link| {
- // So we don't attempt to free any memory in the replaced page.
- self.cursor.hyperlink_id = 0;
+ // startHyperlink will try to free old hyperlinks, so set this
+ // to null. We free it ourselves later since we're doing some
+ // ref-counting shenanigans in this function.
self.cursor.hyperlink = null;
// Re-add
@@ -1789,6 +1949,11 @@ pub fn splitCellBoundary(
p_rac.row,
p_cells[p_row.node.cols() - 1 ..][0..1],
);
+
+ // `clearCells` does not mark rows dirty, and our
+ // callers only mark the cursor row, so mark the
+ // previous row here.
+ p_row.markDirty();
}
}
}
@@ -1851,50 +2016,80 @@ pub const Resize = struct {
prompt_redraw: osc.semantic_prompt.Redraw = .false,
};
-/// Resize the screen. The rows or cols can be bigger or smaller.
-///
-/// If this returns an error, the screen is left in a likely garbage state.
-/// It is very hard to undo this operation without blowing up our memory
-/// usage. The only way to recover is to reset the screen. The only way
-/// this really fails is if page allocation is required and fails, which
-/// probably means the system is in trouble anyways. I'd like to improve this
-/// in the future but it is not a priority particularly because this scenario
-/// (resize) is difficult.
+const resize_tw = tripwire.module(enum {
+ saved_cursor_pin,
+ pages,
+}, resize);
+
+/// Resize the screen. The rows or cols can be bigger or smaller. If this
+/// returns an error, the screen is unchanged.
pub inline fn resize(
self: *Screen,
opts: Resize,
-) !void {
+) Allocator.Error!void {
+ const tw = resize_tw;
defer self.assertIntegrity();
- if (comptime build_options.kitty_graphics) {
- // No matter what we mark our image state as dirty
- self.kitty_images.dirty = true;
- }
+ // We need to insert a tracked pin for our saved cursor so we can
+ // modify its X/Y for reflow. Do this before changing any state since
+ // tracking the pin can fail.
+ const saved_cursor_pin: ?*Pin = saved_cursor: {
+ const sc = self.saved_cursor orelse break :saved_cursor null;
+ const pin = self.pages.pin(.{ .active = .{
+ .x = sc.x,
+ .y = sc.y,
+ } }) orelse break :saved_cursor null;
+ try tw.check(.saved_cursor_pin);
+ break :saved_cursor try self.pages.trackPin(pin);
+ };
+ defer if (saved_cursor_pin) |p| self.pages.untrackPin(p);
- // Release the cursor style while resizing just
- // in case the cursor ends up on a different page.
+ // A cursor style and hyperlink are partly stored in the page containing
+ // the cursor. Their IDs only have meaning within that page, and the page
+ // keeps reference counts for them. Resizing can replace or destroy the
+ // page, so below we temporarily remove both values from the cursor before
+ // asking PageList to resize.
+ //
+ // Save everything first so we can put the cursor back together afterward,
+ // or restore it unchanged if the resize fails. We also save the original
+ // node and serial so after a successful resize we can tell whether that
+ // page survived and still needs its temporary references released.
+ const cursor_node = self.cursor.page_pin.node;
+ const cursor_node_serial = cursor_node.serial;
const cursor_style = self.cursor.style;
- self.cursor.style = .{};
- self.manualStyleUpdate() catch unreachable;
- defer {
- // Restore the cursor style.
+ const cursor_style_id = self.cursor.style_id;
+ const cursor_hyperlink = self.cursor.hyperlink;
+ const cursor_hyperlink_id = self.cursor.hyperlink_id;
+
+ // Keep an extra reference to the cursor style and hyperlink while the
+ // resize is in progress. Releasing the cursor references below therefore
+ // can't delete either entry, and an error can restore the cursor without
+ // any allocation.
+ {
+ const page = cursor_node.page();
+ if (cursor_style_id != style.default_id) {
+ page.styles.use(page.memory, cursor_style_id);
+ }
+ if (cursor_hyperlink_id != 0) {
+ page.hyperlink_set.use(page.memory, cursor_hyperlink_id);
+ }
+ }
+ errdefer {
self.cursor.style = cursor_style;
- self.manualStyleUpdate() catch |err| {
- // This failure should not happen because manualStyleUpdate
- // handles page splitting, overflow, and more. This should only
- // happen if we're out of RAM. In this case, we'll just degrade
- // gracefully back to the default style.
- log.err("failed to update style on cursor reload err={}", .{err});
- self.cursor.style = .{};
- self.cursor.style_id = 0;
- };
+ self.cursor.style_id = cursor_style_id;
+ self.cursor.hyperlink = cursor_hyperlink;
+ self.cursor.hyperlink_id = cursor_hyperlink_id;
}
+ // Release the cursor style while resizing just in case the cursor ends
+ // up on a different page.
+ self.cursor.style = .{};
+ self.manualStyleUpdate() catch unreachable;
+
// If we have a hyperlink, release it from the old page
// and then we need to re-add it to the new page. This needs
// to happen because resize below typically reallocates a
// new page so the old hyperlink is invalid.
- const hyperlink_ = self.cursor.hyperlink;
if (self.cursor.hyperlink_id != 0) {
// Note we do NOT use endHyperlink because we want to keep
// our allocated self.cursor.hyperlink valid.
@@ -1904,73 +2099,8 @@ pub inline fn resize(
self.cursor.hyperlink = null;
}
- // We need to insert a tracked pin for our saved cursor so we can
- // modify its X/Y for reflow.
- const saved_cursor_pin: ?*Pin = saved_cursor: {
- const sc = self.saved_cursor orelse break :saved_cursor null;
- const pin = self.pages.pin(.{ .active = .{
- .x = sc.x,
- .y = sc.y,
- } }) orelse break :saved_cursor null;
- break :saved_cursor try self.pages.trackPin(pin);
- };
- defer if (saved_cursor_pin) |p| self.pages.untrackPin(p);
-
- // If our cursor is on a prompt or input line, clear it so the shell can
- // redraw it. This works with OSC 133 semantic prompts.
- //
- // We check cursor.semantic_content rather than page_row.semantic_prompt
- // because some shells (e.g., Nu) mark input areas with OSC 133 B but don't
- // mark continuation lines with k=s. If the input spans multiple lines and
- // continuation lines are unmarked, checking only page_row.semantic_prompt
- // would miss them. By checking semantic_content, we assume that if the
- // cursor is on anything other than command output, we're at a prompt/input
- // line and should clear from there.
- if (opts.prompt_redraw != .false and
- self.cursor.semantic_content != .output)
- prompt: {
- switch (opts.prompt_redraw) {
- .false => unreachable,
-
- // For `.last`, only clear the current line where the cursor is.
- // For `.true`, clear all prompt lines starting from the beginning.
- .last => {
- const page = self.cursor.page_pin.node.page();
- const row = self.cursor.page_row;
- const cells = page.getCells(row);
- self.clearCells(page, row, cells);
- },
-
- .true => {
- const start = start: {
- var it = self.cursor.page_pin.promptIterator(
- .left_up,
- null,
- );
- break :start it.next() orelse {
- // This should never happen because promptIterator should always
- // find a prompt if we already verified our row is some kind of
- // prompt.
- log.warn("cursor on prompt line but promptIterator found no prompt", .{});
- break :prompt;
- };
- };
-
- // Clear cells from our start down. We replace it with spaces,
- // and do not physically erase the rows (eraseRows) because the
- // shell is going to expect this space to be available.
- var it = start.rowIterator(.right_down, null);
- while (it.next()) |pin| {
- const page = pin.node.page();
- const row = pin.rowAndCell().row;
- const cells = page.getCells(row);
- self.clearCells(page, row, cells);
- }
- },
- }
- }
-
// Perform the resize operation.
+ try tw.check(.pages);
try self.pages.resize(.{
.rows = opts.rows,
.cols = opts.cols,
@@ -1982,17 +2112,39 @@ pub inline fn resize(
},
});
+ // No more failures are possible after this. Enforced by compiler
+ // because we do NO cleanup of the state below.
+ errdefer comptime unreachable;
+
+ // Resizing moves image placements, so mark their geometry as dirty.
+ if (comptime build_options.kitty_graphics) self.kitty_images.dirty = true;
+
// If we have no scrollback and we shrunk our rows, we must explicitly
// erase our history. This is because PageList always keeps at least
// a page size of history.
- if (self.no_scrollback) {
- self.pages.eraseHistory(null);
- }
+ if (self.no_scrollback) self.pages.eraseHistory(null);
// If our cursor was updated, we do a full reload so all our cursor
// state is correct.
self.cursorReload();
+ // Clear any redrawable prompt after the fallible resize but before
+ // restoring the cursor style and hyperlink, so cleared cells retain the
+ // same default styling they had with the previous ordering.
+ self.clearPromptForRedraw(opts.prompt_redraw);
+
+ // Restore the cursor style.
+ self.cursor.style = cursor_style;
+ self.manualStyleUpdate() catch |err| {
+ // This failure should not happen because manualStyleUpdate handles
+ // page splitting, overflow, and more. This should only happen if
+ // we're out of RAM. In this case, we'll just degrade gracefully back
+ // to the default style.
+ log.err("failed to update style on cursor reload err={}", .{err});
+ self.cursor.style = .{};
+ self.cursor.style_id = 0;
+ };
+
// If we reflowed a saved cursor, update it.
if (saved_cursor_pin) |p| {
// This should never fail because a non-null saved_cursor_pin
@@ -2024,7 +2176,7 @@ pub inline fn resize(
}
// Fix up our hyperlink if we had one.
- if (hyperlink_) |link| {
+ if (cursor_hyperlink) |link| {
self.startHyperlink(link.uri, switch (link.id) {
.explicit => |v| v,
.implicit => null,
@@ -2039,6 +2191,97 @@ pub inline fn resize(
link.deinit(self.alloc);
self.alloc.destroy(link);
}
+
+ // A tracked pin follows its content when PageList moves or replaces a
+ // page. If the cursor's pin still points to the node we started with,
+ // that node survived the resize and still owns the temporary style and
+ // hyperlink references we added above.
+ //
+ // Comparing pointers is not enough by itself. PageList keeps a pool of
+ // nodes, so it can destroy a node and create a replacement at the same
+ // memory address. Every new use of a node gets a different serial number,
+ // making the pointer-and-serial pair an O(1) identity check.
+ const cursor_node_survived =
+ self.cursor.page_pin.node == cursor_node and
+ self.cursor.page_pin.node.serial == cursor_node_serial;
+ if (comptime build_options.slow_runtime_safety) {
+ assert(cursor_node_survived ==
+ self.pages.nodeIsValid(cursor_node, cursor_node_serial));
+ }
+
+ // A surviving node still contains our temporary references, so remove
+ // them now. If the node was replaced, its destruction removed those
+ // references along with the rest of the old page so we don't need to
+ // fix it up.
+ if (cursor_node_survived) {
+ const page = cursor_node.page();
+ if (cursor_style_id != style.default_id) {
+ page.styles.release(page.memory, cursor_style_id);
+ }
+ if (cursor_hyperlink_id != 0) {
+ page.hyperlink_set.release(page.memory, cursor_hyperlink_id);
+ }
+ }
+}
+
+fn clearPromptForRedraw(
+ self: *Screen,
+ redraw: osc.semantic_prompt.Redraw,
+) void {
+ // If our cursor is on a prompt or input line, clear it so the shell can
+ // redraw it. This works with OSC 133 semantic prompts. We do this after
+ // the fallible resize so an error leaves the original prompt untouched.
+ //
+ // We check cursor.semantic_content rather than page_row.semantic_prompt
+ // because some shells (e.g., Nu) mark input areas with OSC 133 B but don't
+ // mark continuation lines with k=s. If the input spans multiple lines and
+ // continuation lines are unmarked, checking only page_row.semantic_prompt
+ // would miss them. By checking semantic_content, we assume that if the
+ // cursor is on anything other than command output, we're at a prompt/input
+ // line and should clear from there.
+ if (redraw != .false and
+ self.cursor.semantic_content != .output)
+ prompt: {
+ switch (redraw) {
+ .false => unreachable,
+
+ // For `.last`, only clear the current line where the cursor is.
+ // For `.true`, clear all prompt lines starting from the beginning.
+ .last => {
+ const page = self.cursor.page_pin.node.page();
+ const row = self.cursor.page_row;
+ const cells = page.getCells(row);
+ self.clearCells(page, row, cells);
+ },
+
+ .true => {
+ const start = start: {
+ var it = self.cursor.page_pin.promptIterator(
+ .left_up,
+ null,
+ );
+ break :start it.next() orelse {
+ // This should never happen because promptIterator should always
+ // find a prompt if we already verified our row is some kind of
+ // prompt.
+ log.warn("cursor on prompt line but promptIterator found no prompt", .{});
+ break :prompt;
+ };
+ };
+
+ // Clear cells from our start down. We replace it with spaces,
+ // and do not physically erase the rows (eraseRows) because the
+ // shell is going to expect this space to be available.
+ var it = start.rowIterator(.right_down, null);
+ while (it.next()) |pin| {
+ const page = pin.node.page();
+ const row = pin.rowAndCell().row;
+ const cells = page.getCells(row);
+ self.clearCells(page, row, cells);
+ }
+ },
+ }
+ }
}
/// Set a style attribute for the current cursor.
@@ -2684,17 +2927,8 @@ pub const SelectionString = struct {
/// If true, trim whitespace around the selection.
trim: bool = true,
-
- /// If non-null, a stringmap will be written here. This will use
- /// the same allocator as the call to selectionString. The string will
- /// be duplicated here and in the return value so both must be freed.
- map: ?*StringMap = null,
};
-const selectionString_tw = tripwire.module(enum {
- copy_map,
-}, selectionString);
-
/// Returns the raw text associated with a selection. This will unwrap
/// soft-wrapped edges. The returned slice is owned by the caller and allocated
/// using alloc, not the allocator associated with the screen (unless they match).
@@ -2705,6 +2939,32 @@ pub fn selectionString(
alloc: Allocator,
opts: SelectionString,
) Allocator.Error![:0]const u8 {
+ return self.selectionStringImpl(alloc, opts, null);
+}
+
+/// Returns a StringMap associated with a selection. The map contains the
+/// selection's raw text and a mapping from each byte to its screen location.
+///
+/// The returned map is owned by the caller.
+pub fn selectionStringMap(
+ self: *Screen,
+ alloc: Allocator,
+ opts: SelectionString,
+) Allocator.Error!StringMap {
+ var pins: PinMap.Map = .empty;
+ errdefer pins.deinit(alloc);
+ return .{
+ .string = try self.selectionStringImpl(alloc, opts, &pins),
+ .map = pins,
+ };
+}
+
+fn selectionStringImpl(
+ self: *Screen,
+ alloc: Allocator,
+ opts: SelectionString,
+ pins: ?*PinMap.Map,
+) Allocator.Error![:0]const u8 {
// We'll use this as our buffer to build our string.
var aw: std.Io.Writer.Allocating = .init(alloc);
defer aw.deinit();
@@ -2720,33 +2980,15 @@ pub fn selectionString(
);
formatter.content = .{ .selection = opts.sel };
- // If we have a string map, we need to set that up.
- var pins: std.ArrayList(Pin) = .empty;
- defer pins.deinit(alloc);
- if (opts.map != null) formatter.pin_map = .{
+ if (pins) |map| formatter.pin_map = .{
.alloc = alloc,
- .map = &pins,
+ .map = map,
};
// Emit. Since this is an allocating writer, a failed write
// just becomes an OOM.
formatter.format(&aw.writer) catch return error.OutOfMemory;
-
- // Build our final text and if we have a string map set that up.
- const text = try aw.toOwnedSliceSentinel(0);
- errdefer alloc.free(text);
- if (opts.map) |map| {
- const map_string = try alloc.dupeZ(u8, text);
- errdefer alloc.free(map_string);
- try selectionString_tw.check(.copy_map);
- const map_pins = try pins.toOwnedSlice(alloc);
- map.* = .{
- .string = map_string,
- .map = map_pins,
- };
- }
-
- return text;
+ return try aw.toOwnedSliceSentinel(0);
}
pub const SelectLine = struct {
@@ -2894,7 +3136,7 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
const this_whitespace = std.mem.indexOfScalar(
u21,
whitespace,
- cell.content.codepoint,
+ cell.content.codepoint.data,
) != null;
if (this_whitespace) continue;
@@ -2916,7 +3158,7 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
const this_whitespace = std.mem.indexOfScalar(
u21,
whitespace,
- cell.content.codepoint,
+ cell.content.codepoint.data,
) != null;
if (this_whitespace) continue;
@@ -2948,7 +3190,7 @@ pub fn selectAll(self: *Screen) ?Selection {
const this_whitespace = std.mem.indexOfAny(
u32,
whitespace,
- &[_]u32{cell.content.codepoint},
+ &[_]u32{cell.content.codepoint.data},
) != null;
if (this_whitespace) continue;
@@ -2972,7 +3214,7 @@ pub fn selectAll(self: *Screen) ?Selection {
const this_whitespace = std.mem.indexOfAny(
u32,
whitespace,
- &[_]u32{cell.content.codepoint},
+ &[_]u32{cell.content.codepoint.data},
) != null;
if (this_whitespace) continue;
@@ -3040,7 +3282,7 @@ pub fn selectWord(
const expect_boundary = std.mem.indexOfScalar(
u21,
boundary_codepoints,
- start_cell.content.codepoint,
+ start_cell.content.codepoint.data,
) != null;
// Go forwards to find our end boundary
@@ -3058,7 +3300,7 @@ pub fn selectWord(
const this_boundary = std.mem.indexOfScalar(
u21,
boundary_codepoints,
- cell.content.codepoint,
+ cell.content.codepoint.data,
) != null;
if (this_boundary != expect_boundary) break :end prev;
@@ -3095,7 +3337,7 @@ pub fn selectWord(
const this_boundary = std.mem.indexOfScalar(
u21,
boundary_codepoints,
- cell.content.codepoint,
+ cell.content.codepoint.data,
) != null;
if (this_boundary != expect_boundary) break :start prev;
@@ -3517,7 +3759,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
1 => {
self.cursor.page_cell.* = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = c },
+ .content = .{ .codepoint = @bitCast(@as(u24, @intCast(c))) },
.style_id = self.cursor.style_id,
.protected = self.cursor.protected,
.semantic_content = self.cursor.semantic_content,
@@ -3539,7 +3781,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
if (self.cursor.x == self.pages.cols - 1) {
self.cursor.page_cell.* = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = 0 },
+ .content = .{ .codepoint = .{ .data = 0 } },
.wide = .spacer_head,
.protected = self.cursor.protected,
.semantic_content = self.cursor.semantic_content,
@@ -3557,7 +3799,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
// Write our wide char
self.cursor.page_cell.* = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = c },
+ .content = .{ .codepoint = .{ .data = c } },
.style_id = self.cursor.style_id,
.wide = .wide,
.protected = self.cursor.protected,
@@ -3571,7 +3813,7 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
self.cursorRight(1);
self.cursor.page_cell.* = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = 0 },
+ .content = .{ .codepoint = .{ .data = 0 } },
.wide = .spacer_tail,
.protected = self.cursor.protected,
.semantic_content = self.cursor.semantic_content,
@@ -3600,11 +3842,48 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
}
}
+test "Screen forwards optional scrollback limits" {
+ const testing = std.testing;
+ const max_lines: usize = 123;
+ var s = try init(testing.io, testing.allocator, .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback_bytes = null,
+ .max_scrollback_lines = max_lines,
+ });
+ defer s.deinit();
+
+ try testing.expectEqual(
+ std.math.maxInt(usize),
+ s.pages.limits.bytes.explicit,
+ );
+ try testing.expectEqual(max_lines, s.pages.limits.lines.explicit);
+ try testing.expect(!s.no_scrollback);
+}
+
+test "Screen reset cursor pin is not garbage" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
+ defer s.deinit();
+ try s.testWriteString("hello, world");
+
+ // The page reset marks every tracked pin garbage but the screen
+ // keeps using the cursor pin, so it must come back clean: anything
+ // that copies it (e.g. Kitty image placements) would otherwise be
+ // born garbage and reaped.
+ s.reset();
+ try testing.expect(!s.cursor.page_pin.garbage);
+}
+
test "Screen read and write" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
try testing.expectEqual(@as(style.Id, 0), s.cursor.style_id);
@@ -3617,8 +3896,9 @@ test "Screen read and write" {
test "Screen read and write newline" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
try testing.expectEqual(@as(style.Id, 0), s.cursor.style_id);
@@ -3631,8 +3911,9 @@ test "Screen read and write newline" {
test "Screen read and write scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 2, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 2, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.testWriteString("hello\nworld\ntest");
@@ -3651,8 +3932,9 @@ test "Screen read and write scrollback" {
test "Screen read and write no scrollback small" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 2, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("hello\nworld\ntest");
@@ -3671,8 +3953,9 @@ test "Screen read and write no scrollback small" {
test "Screen read and write no scrollback large" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 2, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
for (0..1_000) |i| {
@@ -3692,14 +3975,15 @@ test "Screen read and write no scrollback large" {
test "Screen cursorCopy x/y" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
s.cursorAbsolute(2, 3);
try testing.expect(s.cursor.x == 2);
try testing.expect(s.cursor.y == 3);
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s2.deinit();
try s2.cursorCopy(s.cursor, .{});
try testing.expect(s2.cursor.x == 2);
@@ -3716,11 +4000,12 @@ test "Screen cursorCopy x/y" {
test "Screen cursorCopy style deref" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s2.deinit();
const page = s2.cursor.page_pin.node.page();
@@ -3738,11 +4023,12 @@ test "Screen cursorCopy style deref" {
test "Screen cursorCopy style deref new page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 2048 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 2048 });
defer s2.deinit();
// We need to get the cursor on a new page.
@@ -3811,12 +4097,13 @@ test "Screen cursorCopy style deref new page" {
test "Screen cursorCopy style copy" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.setAttribute(.{ .bold = {} });
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s2.deinit();
const page = s2.cursor.page_pin.node.page();
try s2.cursorCopy(s.cursor, .{});
@@ -3827,11 +4114,12 @@ test "Screen cursorCopy style copy" {
test "Screen cursorCopy hyperlink deref" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s2.deinit();
const page = s2.cursor.page_pin.node.page();
@@ -3849,11 +4137,12 @@ test "Screen cursorCopy hyperlink deref" {
test "Screen write regrows compacted page capacity" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{
+ var s = try Screen.init(io, alloc, .{
.cols = 80,
.rows = 24,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
@@ -3894,14 +4183,91 @@ test "Screen write regrows compacted page capacity" {
try testing.expect(page.graphemeCount() >= 1);
}
+// The cursor style and hyperlink IDs are only meaningful within the page
+// the cursor pin points at. scrollClear can move the active area onto a
+// later page while the cursor pin stays with its content on an earlier
+// page (now scrollback), so the reset in cursorReload must migrate both
+// references to the destination page. It previously replaced the pin
+// directly and then released the old style ID on the new page.
+test "Screen scrollClear across pages migrates cursor style and hyperlink" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{
+ .cols = 10,
+ .rows = 10,
+ .max_scrollback_bytes = std.math.maxInt(usize),
+ });
+ defer s.deinit();
+
+ // Fill the first page so the active area spans two pages.
+ const first_page_size = s.pages.pages.first.?.capacity().rows;
+ s.pages.pages.first.?.page().pauseIntegrityChecks(true);
+ for (0..first_page_size - 5) |_| {
+ try s.testWriteString("\n");
+ }
+ s.pages.pages.first.?.page().pauseIntegrityChecks(false);
+ try s.testWriteString("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
+ try testing.expect(s.pages.pages.first != s.pages.pages.last);
+
+ // Move the cursor to the top of the active area, which is on the
+ // first page, and give it a style and a hyperlink there.
+ s.cursorAbsolute(0, 0);
+ try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
+ try s.setAttribute(.{ .bold = {} });
+ try s.startHyperlink("https://example.com/", null);
+
+ const old_page: *Page = s.cursor.page_pin.node.page();
+ const old_style_id = s.cursor.style_id;
+ const old_hyperlink_id = s.cursor.hyperlink_id;
+ try testing.expect(old_style_id != style.default_id);
+ try testing.expect(old_hyperlink_id != 0);
+
+ // All ten active rows are non-empty, so this moves the active area
+ // fully onto the second page while the cursor pin stays with its
+ // old row, which is now scrollback.
+ try s.scrollClear();
+
+ // The cursor was moved to the new active top-left on the second
+ // page with its style and hyperlink references rebuilt there.
+ const new_page: *Page = s.cursor.page_pin.node.page();
+ try testing.expect(new_page != old_page);
+ try testing.expect(s.cursor.style_id != style.default_id);
+ try testing.expect(s.cursor.hyperlink_id != 0);
+ try testing.expect(new_page.styles.refCount(
+ new_page.memory,
+ s.cursor.style_id,
+ ) > 0);
+ try testing.expect(new_page.hyperlink_set.refCount(
+ new_page.memory,
+ s.cursor.hyperlink_id,
+ ) > 0);
+
+ // The cursor's references on the old page were released. Nothing
+ // else referenced either entry, so both are dead there now.
+ try testing.expectEqual(0, old_page.styles.refCount(
+ old_page.memory,
+ old_style_id,
+ ));
+ try testing.expectEqual(0, old_page.hyperlink_set.refCount(
+ old_page.memory,
+ old_hyperlink_id,
+ ));
+
+ // Printing attaches the migrated style and hyperlink to a cell.
+ try s.testWriteString("B");
+}
+
test "Screen cursorCopy hyperlink deref new page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 2048 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 2048 });
defer s2.deinit();
// We need to get the cursor on a new page.
@@ -3970,8 +4336,9 @@ test "Screen cursorCopy hyperlink deref new page" {
test "Screen cursorCopy hyperlink copy" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
// Create a hyperlink for the cursor.
@@ -3979,7 +4346,7 @@ test "Screen cursorCopy hyperlink copy" {
try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.node.page().hyperlink_set.count());
try testing.expect(s.cursor.hyperlink_id != 0);
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s2.deinit();
const page = s2.cursor.page_pin.node.page();
@@ -3995,8 +4362,9 @@ test "Screen cursorCopy hyperlink copy" {
test "Screen cursorCopy hyperlink copy disabled" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
// Create a hyperlink for the cursor.
@@ -4004,7 +4372,7 @@ test "Screen cursorCopy hyperlink copy disabled" {
try testing.expectEqual(@as(usize, 1), s.cursor.page_pin.node.page().hyperlink_set.count());
try testing.expect(s.cursor.hyperlink_id != 0);
- var s2 = try Screen.init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s2 = try Screen.init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s2.deinit();
const page = s2.cursor.page_pin.node.page();
@@ -4020,8 +4388,9 @@ test "Screen cursorCopy hyperlink copy disabled" {
test "Screen style basics" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
const page = s.cursor.page_pin.node.page();
try testing.expectEqual(@as(usize, 0), page.styles.count());
@@ -4042,8 +4411,9 @@ test "Screen style basics" {
test "Screen style reset to default" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
const page = s.cursor.page_pin.node.page();
try testing.expectEqual(@as(usize, 0), page.styles.count());
@@ -4062,8 +4432,9 @@ test "Screen style reset to default" {
test "Screen style reset with unset" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
const page = s.cursor.page_pin.node.page();
try testing.expectEqual(@as(usize, 0), page.styles.count());
@@ -4082,8 +4453,9 @@ test "Screen style reset with unset" {
test "Screen clearRows active one line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.testWriteString("hello, world");
@@ -4097,8 +4469,9 @@ test "Screen clearRows active one line" {
test "Screen clearRows active multi line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.testWriteString("hello\nworld");
@@ -4113,8 +4486,9 @@ test "Screen clearRows active multi line" {
test "Screen clearRows active styled line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.setAttribute(.{ .bold = {} });
@@ -4138,7 +4512,7 @@ test "Screen clearRows active styled line" {
test "Screen clearCells empty range" {
const testing = std.testing;
- var s = try Screen.init(testing.allocator, .default);
+ var s = try Screen.init(testing.io, testing.allocator, .default);
defer s.deinit();
const page = s.cursor.page_pin.node.page();
@@ -4150,10 +4524,10 @@ test "Screen clearCells empty range" {
test "Screen clearRows uses stored page width" {
const testing = std.testing;
- var s = try Screen.init(testing.allocator, .{
+ var s = try Screen.init(testing.io, testing.allocator, .{
.cols = 2,
.rows = 1,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
@@ -4170,8 +4544,9 @@ test "Screen clearRows uses stored page width" {
test "Screen clearRows protected" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.testWriteString("UNPROTECTED");
@@ -4198,8 +4573,9 @@ test "Screen clearRows protected" {
test "Screen eraseRows history" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.testWriteString("1\n2\n3\n4\n5\n6");
@@ -4232,8 +4608,9 @@ test "Screen eraseRows history" {
test "Screen eraseRows history with more lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 1000 });
+ var s = try Screen.init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 1000 });
defer s.deinit();
try s.testWriteString("A\nB\nC\n1\n2\n3\n4\n5\n6");
@@ -4266,8 +4643,9 @@ test "Screen eraseRows history with more lines" {
test "Screen eraseRows active partial" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try Screen.init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try Screen.init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1\n2\n3");
@@ -4294,10 +4672,10 @@ test "Screen eraseRows active partial" {
test "Screen: cursorCellEndOfPrev across mixed-width pages" {
const testing = std.testing;
- var s = try init(testing.allocator, .{
+ var s = try init(testing.io, testing.allocator, .{
.cols = 4,
.rows = 2,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
@@ -4316,8 +4694,9 @@ test "Screen: cursorCellEndOfPrev across mixed-width pages" {
test "Screen: cursorDown across pages preserves style" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
// Scroll down enough to go to another page
@@ -4368,8 +4747,9 @@ test "Screen: cursorDown across pages preserves style" {
test "Screen: cursorUp across pages preserves style" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
// Scroll down enough to go to another page
@@ -4415,8 +4795,9 @@ test "Screen: cursorUp across pages preserves style" {
test "Screen: cursorAbsolute across pages preserves style" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
// Scroll down enough to go to another page
@@ -4470,8 +4851,9 @@ test "Screen: cursorAbsolute to page with insufficient capacity" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
// Scroll down enough to go to another page
@@ -4537,8 +4919,9 @@ test "Screen: cursorAbsolute to page with insufficient capacity" {
test "Screen: scrolling" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -4579,8 +4962,9 @@ test "Screen: scrolling" {
test "Screen: scrolling with a single-row screen no scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 1, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 1, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD");
@@ -4599,8 +4983,9 @@ test "Screen: scrolling with a single-row screen no scrollback" {
test "Screen: scrolling with a single-row screen with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 1, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 1, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD");
@@ -4629,8 +5014,9 @@ test "Screen: scrolling with a single-row screen with scrollback" {
test "Screen: scrolling across pages preserves style" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.setAttribute(.{ .bold = {} });
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -4658,8 +5044,9 @@ test "Screen: scrolling across pages preserves style" {
test "Screen: scroll down from 0" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -4677,8 +5064,9 @@ test "Screen: scroll down from 0" {
test "Screen: scrollback various cases" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
try s.cursorDownScroll();
@@ -4758,8 +5146,9 @@ test "Screen: scrollback various cases" {
test "Screen: scrollback with multi-row delta" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL");
@@ -4784,8 +5173,9 @@ test "Screen: scrollback with multi-row delta" {
test "Screen: scrollback empty" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 50 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 50 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
s.scroll(.{ .delta_row = 1 });
@@ -4799,8 +5189,9 @@ test "Screen: scrollback empty" {
test "Screen: scrollback doesn't move viewport if not at bottom" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH");
@@ -4834,8 +5225,9 @@ test "Screen: scrollback doesn't move viewport if not at bottom" {
test "Screen: scrolling moves selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -4913,8 +5305,9 @@ test "Screen: scrolling moves selection" {
test "Screen: cursorScrollRegionUp simple" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4MNOP\n5QRST");
@@ -4940,8 +5333,9 @@ test "Screen: cursorScrollRegionUp simple" {
test "Screen: cursorScrollRegionUp renews page generation" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4MNOP\n5QRST");
s.cursorAbsolute(0, 2);
@@ -4956,8 +5350,9 @@ test "Screen: cursorScrollRegionUp renews page generation" {
test "Screen: cursorScrollRegionUp moves selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4MNOP\n5QRST");
@@ -4994,8 +5389,9 @@ test "Screen: cursorScrollRegionUp moves selection" {
test "Screen: cursorScrollRegionUp region spans pages" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
defer s.deinit();
// We need to get the cursor to a new page
@@ -5058,8 +5454,9 @@ test "Screen: cursorScrollRegionUp region spans pages" {
test "Screen: cursorScrollRegionUp region spans pages with background SGR" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
defer s.deinit();
// We need to get the cursor to a new page. See the previous test
@@ -5101,8 +5498,9 @@ test "Screen: cursorScrollRegionUp region spans pages with background SGR" {
test "Screen: cursorScrollRegionUp with styled erased row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
// Write a styled row at the top so the erased row has managed
@@ -5130,8 +5528,9 @@ test "Screen: cursorScrollRegionUp with styled erased row" {
test "Screen: scrolling moves viewport" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n");
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5155,8 +5554,9 @@ test "Screen: scrolling moves viewport" {
test "Screen: scrolling when viewport is pruned" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 215, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 215, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
// Write some to create scrollback and move back into our scrollback.
@@ -5181,8 +5581,9 @@ test "Screen: scrolling when viewport is pruned" {
test "Screen: scroll and clear full screen" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5208,8 +5609,9 @@ test "Screen: scroll and clear full screen" {
test "Screen: scroll and clear partial screen" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
@@ -5235,8 +5637,9 @@ test "Screen: scroll and clear partial screen" {
test "Screen: scroll and clear empty screen" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
try s.scrollClear();
{
@@ -5254,8 +5657,9 @@ test "Screen: scroll and clear empty screen" {
test "Screen: scroll and clear ignore blank lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
try s.scrollClear();
@@ -5297,8 +5701,9 @@ test "Screen: scroll and clear ignore blank lines" {
test "Screen: scroll above same page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5359,8 +5764,9 @@ test "Screen: scroll above same page" {
test "Screen: scroll above same page but cursor on previous page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
defer s.deinit();
// We need to get the cursor to a new page
@@ -5446,8 +5852,9 @@ test "Screen: scroll above same page but cursor on previous page" {
test "Screen: scroll above same page but cursor on previous page last row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
defer s.deinit();
// We need to get the cursor to a new page
@@ -5536,8 +5943,9 @@ test "Screen: scroll above same page but cursor on previous page last row" {
test "Screen: scroll above creates new page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
// We need to get the cursor to a new page
@@ -5611,8 +6019,9 @@ test "Screen: scroll above creates new page" {
test "Screen: scroll above with cursor on non-final row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 4, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 4, .max_scrollback_bytes = 10 });
defer s.deinit();
// Get the cursor to be 2 rows above a new page
@@ -5688,8 +6097,9 @@ test "Screen: scroll above with cursor on non-final row" {
test "Screen: scroll above no scrollback bottom of page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const first_page_size = s.pages.pages.first.?.capacity().rows;
@@ -5750,11 +6160,175 @@ test "Screen: scroll above no scrollback bottom of page" {
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
}
+test "Screen: scroll above hyperlink-dense row to fresh page" {
+ // Regression test for https://github.com/ghostty-org/ghostty/discussions/13160
+ //
+ // When a scroll-above operation pushes a row carrying more unique
+ // hyperlinks than a fresh page's default hyperlink capacity across
+ // a page boundary, the cross-page row clone must increase the
+ // destination page's capacity (like insertLines/deleteLines do)
+ // rather than error out mid-operation, which leaves the page list
+ // half-mutated and aborts later (e.g. in clearCells).
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10000 });
+ defer s.deinit();
+
+ // Fill the first page so it is exactly full and the cursor is on
+ // its last row (which is also the bottom row of the active area).
+ // The next grow() will then allocate a fresh page.
+ const first_page_rows = s.pages.pages.first.?.capacity().rows;
+ s.pages.pages.first.?.page().pauseIntegrityChecks(true);
+ for (0..first_page_rows - 1) |_| try s.testWriteString("\n");
+ s.pages.pages.first.?.page().pauseIntegrityChecks(false);
+ try testing.expect(s.pages.pages.first == s.pages.pages.last);
+ try testing.expectEqual(
+ s.pages.pages.first.?.capacity().rows,
+ s.pages.pages.first.?.page().size.rows,
+ );
+ try testing.expectEqual(s.pages.rows - 1, s.cursor.y);
+
+ // Fill the bottom row with unique hyperlinks: more than a fresh
+ // page can hold with default hyperlink capacity.
+ for (0..s.pages.cols) |i| {
+ var buf: [64]u8 = undefined;
+ const uri = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{i});
+ try s.startHyperlink(uri, null);
+ try s.testWriteString("A");
+ s.endHyperlink();
+ }
+ try testing.expectEqual(
+ @as(usize, s.pages.cols),
+ s.cursor.page_pin.node.page().hyperlink_set.count(),
+ );
+
+ // Move the cursor above the bottom row and scroll. The dense row is
+ // pushed across the page boundary into the freshly allocated page.
+ s.cursorAbsolute(0, 1);
+ try s.cursorScrollAbove();
+
+ // We must have created a second page and the dense row must now be
+ // the top row of that page.
+ try testing.expect(s.pages.pages.first != s.pages.pages.last);
+
+ // All hyperlinks must have survived the scroll intact: every cell
+ // flagged as a hyperlink must resolve to a real entry in its page's
+ // hyperlink map. A half-applied scroll leaves cells whose hyperlink
+ // flag is set but that have no map entry, which aborts in
+ // clearCells later.
+ var node_: ?*PageList.List.Node = s.pages.pages.first;
+ while (node_) |node| : (node_ = node.next) {
+ const page: *Page = node.page();
+ page.assertIntegrity();
+ for (0..page.size.rows) |y| {
+ const row = page.getRow(y);
+ if (!row.hyperlink) continue;
+ for (page.getCells(row)) |*cell| {
+ if (!cell.hyperlink) continue;
+ try testing.expect(page.lookupHyperlink(cell) != null);
+ }
+ }
+ }
+ {
+ const last_page: *Page = s.pages.pages.last.?.page();
+ try testing.expectEqual(
+ @as(usize, s.pages.cols),
+ last_page.hyperlink_set.count(),
+ );
+ }
+
+ // The dense row is still the bottom row of the active area.
+ for (0..s.pages.cols) |x| {
+ const list_cell = s.pages.getCell(.{ .active = .{
+ .x = @intCast(x),
+ .y = 4,
+ } }).?;
+ try testing.expect(list_cell.cell.hyperlink);
+ const page: *Page = list_cell.node.page();
+ const id = page.lookupHyperlink(list_cell.cell).?;
+ const link = page.hyperlink_set.get(page.memory, id);
+ var buf: [64]u8 = undefined;
+ const expected = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{x});
+ try testing.expectEqualStrings(expected, link.uri.slice(page.memory));
+ }
+}
+
+test "Screen: scroll above hyperlink-dense row to existing page" {
+ // Same as the fresh page variant above but the destination page
+ // already exists (fresh_node == null path in cursorScrollAboveRotate).
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10000 });
+ defer s.deinit();
+
+ // Fill the first page so it is exactly full and the cursor is on
+ // its last row.
+ const first_page_rows = s.pages.pages.first.?.capacity().rows;
+ s.pages.pages.first.?.page().pauseIntegrityChecks(true);
+ for (0..first_page_rows - 1) |_| try s.testWriteString("\n");
+ s.pages.pages.first.?.page().pauseIntegrityChecks(false);
+ try testing.expectEqual(s.pages.rows - 1, s.cursor.y);
+
+ // Fill the last row of the first page with unique hyperlinks:
+ // more than a page can hold with default hyperlink capacity.
+ for (0..s.pages.cols) |i| {
+ var buf: [64]u8 = undefined;
+ const uri = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{i});
+ try s.startHyperlink(uri, null);
+ try s.testWriteString("A");
+ s.endHyperlink();
+ }
+
+ // Scroll twice so the active area straddles the page boundary:
+ // the last two active rows are on a second page while the dense
+ // row remains the last row of the first page.
+ try s.testWriteString("\n\n");
+ try testing.expect(s.pages.pages.first != s.pages.pages.last);
+ try testing.expect(s.cursor.page_pin.node == s.pages.pages.last.?);
+
+ // Move the cursor to an active row that is still on the first page
+ // and above the dense row, then scroll. grow() has capacity in the
+ // last page so no fresh page is allocated, but the dense row still
+ // crosses the page boundary during the rotate.
+ s.cursorAbsolute(0, 0);
+ try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
+ try s.cursorScrollAbove();
+
+ // All hyperlinks must have survived the scroll intact: every cell
+ // flagged as a hyperlink must resolve to a real entry in its page's
+ // hyperlink map.
+ var node_: ?*PageList.List.Node = s.pages.pages.first;
+ while (node_) |node| : (node_ = node.next) {
+ const page: *Page = node.page();
+ page.assertIntegrity();
+ for (0..page.size.rows) |y| {
+ const row = page.getRow(y);
+ if (!row.hyperlink) continue;
+ for (page.getCells(row)) |*cell| {
+ if (!cell.hyperlink) continue;
+ try testing.expect(page.lookupHyperlink(cell) != null);
+ }
+ }
+ }
+ {
+ const last_page: *Page = s.pages.pages.last.?.page();
+ try testing.expectEqual(
+ @as(usize, s.pages.cols),
+ last_page.hyperlink_set.count(),
+ );
+ }
+}
+
test "Screen: clone" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
{
@@ -5766,7 +6340,7 @@ test "Screen: clone" {
try testing.expectEqual(@as(usize, 1), s.cursor.y);
// Clone
- var s2 = try s.clone(alloc, .{ .active = .{} }, null);
+ var s2 = try s.clone(io, alloc, .{ .active = .{} }, null);
defer s2.deinit();
{
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
@@ -5795,8 +6369,9 @@ test "Screen: clone" {
test "Screen: clone partial" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
{
@@ -5808,7 +6383,7 @@ test "Screen: clone partial" {
try testing.expectEqual(@as(usize, 1), s.cursor.y);
// Clone
- var s2 = try s.clone(alloc, .{ .active = .{ .y = 1 } }, null);
+ var s2 = try s.clone(io, alloc, .{ .active = .{ .y = 1 } }, null);
defer s2.deinit();
{
const contents = try s2.dumpStringAlloc(alloc, .{ .active = .{} });
@@ -5824,8 +6399,9 @@ test "Screen: clone partial" {
test "Screen: clone partial cursor out of bounds" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH");
{
@@ -5838,6 +6414,7 @@ test "Screen: clone partial cursor out of bounds" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 0 } },
.{ .active = .{ .y = 0 } },
@@ -5857,8 +6434,9 @@ test "Screen: clone partial cursor out of bounds" {
test "Screen: clone contains full selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5871,6 +6449,7 @@ test "Screen: clone contains full selection" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{} },
null,
@@ -5894,8 +6473,9 @@ test "Screen: clone contains full selection" {
test "Screen: clone contains none of selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5908,6 +6488,7 @@ test "Screen: clone contains none of selection" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 1 } },
null,
@@ -5921,8 +6502,9 @@ test "Screen: clone contains none of selection" {
test "Screen: clone contains selection start cutoff" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5935,6 +6517,7 @@ test "Screen: clone contains selection start cutoff" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 1 } },
null,
@@ -5958,8 +6541,9 @@ test "Screen: clone contains selection start cutoff" {
test "Screen: clone contains selection end cutoff" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -5972,6 +6556,7 @@ test "Screen: clone contains selection end cutoff" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 0 } },
.{ .active = .{ .y = 1 } },
@@ -5995,8 +6580,9 @@ test "Screen: clone contains selection end cutoff" {
test "Screen: clone contains selection end cutoff reversed" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
@@ -6009,6 +6595,7 @@ test "Screen: clone contains selection end cutoff reversed" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 0 } },
.{ .active = .{ .y = 1 } },
@@ -6032,8 +6619,9 @@ test "Screen: clone contains selection end cutoff reversed" {
test "Screen: clone contains subset of selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 4, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 4, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD");
@@ -6046,6 +6634,7 @@ test "Screen: clone contains subset of selection" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 1 } },
.{ .active = .{ .y = 2 } },
@@ -6069,8 +6658,9 @@ test "Screen: clone contains subset of selection" {
test "Screen: clone clamps clipped selections to mixed-width pages" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 4, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 4, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const first = s.pages.pages.first.?;
@@ -6086,6 +6676,7 @@ test "Screen: clone clamps clipped selections to mixed-width pages" {
false,
));
var linear = try s.clone(
+ io,
alloc,
.{ .screen = .{} },
.{ .screen = .{ .y = 1 } },
@@ -6101,6 +6692,7 @@ test "Screen: clone clamps clipped selections to mixed-width pages" {
true,
));
var rectangle = try s.clone(
+ io,
alloc,
.{ .screen = .{ .y = 1 } },
null,
@@ -6114,8 +6706,9 @@ test "Screen: clone clamps clipped selections to mixed-width pages" {
test "Screen: clone contains subset of rectangle selection" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 4, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 4, .max_scrollback_bytes = 1 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD");
@@ -6128,6 +6721,7 @@ test "Screen: clone contains subset of rectangle selection" {
// Clone
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 1 } },
.{ .active = .{ .y = 2 } },
@@ -6153,13 +6747,15 @@ test "Screen: clone contains subset of rectangle selection" {
test "Screen: clone basic" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
{
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 1 } },
.{ .active = .{ .y = 1 } },
@@ -6174,6 +6770,7 @@ test "Screen: clone basic" {
{
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 1 } },
.{ .active = .{ .y = 2 } },
@@ -6190,12 +6787,14 @@ test "Screen: clone basic" {
test "Screen: clone empty viewport" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
{
var s2 = try s.clone(
+ io,
alloc,
.{ .viewport = .{ .y = 0 } },
.{ .viewport = .{ .y = 0 } },
@@ -6212,13 +6811,15 @@ test "Screen: clone empty viewport" {
test "Screen: clone one line viewport" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABC");
{
var s2 = try s.clone(
+ io,
alloc,
.{ .viewport = .{ .y = 0 } },
.{ .viewport = .{ .y = 0 } },
@@ -6235,12 +6836,14 @@ test "Screen: clone one line viewport" {
test "Screen: clone empty active" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
{
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 0 } },
.{ .active = .{ .y = 0 } },
@@ -6257,13 +6860,15 @@ test "Screen: clone empty active" {
test "Screen: clone one line active with extra space" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABC");
{
var s2 = try s.clone(
+ io,
alloc,
.{ .active = .{ .y = 0 } },
null,
@@ -6280,8 +6885,9 @@ test "Screen: clone one line active with extra space" {
test "Screen: clear history with no history" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
try s.testWriteString("4ABCD\n5EFGH\n6IJKL");
try testing.expect(s.pages.viewport == .active);
@@ -6304,8 +6910,9 @@ test "Screen: clear history with no history" {
test "Screen: clear history" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL");
try testing.expect(s.pages.viewport == .active);
@@ -6338,8 +6945,9 @@ test "Screen: clear history" {
test "Screen: clear above cursor" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 3 });
defer s.deinit();
try s.testWriteString("4ABCD\n5EFGH\n6IJKL");
s.clearRows(
@@ -6365,8 +6973,9 @@ test "Screen: clear above cursor" {
test "Screen: clear above cursor with history" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n");
try s.testWriteString("4ABCD\n5EFGH\n6IJKL");
@@ -6393,8 +7002,9 @@ test "Screen: clear above cursor with history" {
test "Screen: resize (no reflow) more rows" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6411,8 +7021,9 @@ test "Screen: resize (no reflow) more rows" {
test "Screen: resize (no reflow) less rows" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6434,8 +7045,9 @@ test "Screen: resize (no reflow) less rows" {
test "Screen: resize (no reflow) less rows trims blank lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD";
try s.testWriteString(str);
@@ -6469,8 +7081,9 @@ test "Screen: resize (no reflow) less rows trims blank lines" {
test "Screen: resize (no reflow) more rows trims blank lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD";
try s.testWriteString(str);
@@ -6504,8 +7117,9 @@ test "Screen: resize (no reflow) more rows trims blank lines" {
test "Screen: resize (no reflow) more cols" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6521,8 +7135,9 @@ test "Screen: resize (no reflow) more cols" {
test "Screen: resize (no reflow) less cols" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6539,8 +7154,9 @@ test "Screen: resize (no reflow) less cols" {
test "Screen: resize (no reflow) more rows with scrollback cursor end" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 7, .rows = 3, .max_scrollback = 2 });
+ var s = try init(io, alloc, .{ .cols = 7, .rows = 3, .max_scrollback_bytes = 2 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -6556,8 +7172,9 @@ test "Screen: resize (no reflow) more rows with scrollback cursor end" {
test "Screen: resize (no reflow) less rows with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 7, .rows = 3, .max_scrollback = 2 });
+ var s = try init(io, alloc, .{ .cols = 7, .rows = 3, .max_scrollback_bytes = 2 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -6575,8 +7192,9 @@ test "Screen: resize (no reflow) less rows with scrollback" {
test "Screen: resize (no reflow) less rows with empty trailing" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1\n2\n3\n4\n5\n6\n7\n8";
try s.testWriteString(str);
@@ -6599,8 +7217,9 @@ test "Screen: resize (no reflow) less rows with empty trailing" {
test "Screen: resize (no reflow) more rows with soft wrapping" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
const str = "1A2B\n3C4E\n5F6G";
try s.testWriteString(str);
@@ -6640,8 +7259,9 @@ test "Screen: resize (no reflow) more rows with soft wrapping" {
test "Screen: resize more rows no scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6667,8 +7287,9 @@ test "Screen: resize more rows no scrollback" {
test "Screen: resize more rows with empty scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6694,8 +7315,9 @@ test "Screen: resize more rows with empty scrollback" {
test "Screen: resize more rows with populated scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -6713,7 +7335,7 @@ test "Screen: resize more rows with populated scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint.data);
}
// Resize
@@ -6725,7 +7347,7 @@ test "Screen: resize more rows with populated scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '4'), list_cell.cell.content.codepoint.data);
}
{
@@ -6739,8 +7361,9 @@ test "Screen: resize more rows with populated scrollback" {
test "Screen: resize more cols no reflow" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6768,8 +7391,9 @@ test "Screen: resize more cols no reflow" {
test "Screen: resize more cols perfect split" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH3IJKL";
try s.testWriteString(str);
@@ -6786,8 +7410,9 @@ test "Screen: resize more cols perfect split" {
test "Screen: resize (no reflow) more cols with scrollback scrolled up" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1\n2\n3\n4\n5\n6\n7\n8";
try s.testWriteString(str);
@@ -6819,8 +7444,9 @@ test "Screen: resize (no reflow) more cols with scrollback scrolled up" {
test "Screen: resize (no reflow) less cols with scrollback scrolled up" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1\n2\n3\n4\n5\n6\n7\n8";
try s.testWriteString(str);
@@ -6863,8 +7489,9 @@ test "Screen: resize (no reflow) less cols with scrollback scrolled up" {
test "Screen: resize more cols no reflow preserves semantic prompt" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
// Set one of the rows to be a prompt
@@ -6907,8 +7534,9 @@ test "Screen: resize more cols no reflow preserves semantic prompt" {
test "Screen: resize more cols with reflow that fits full width" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6928,7 +7556,7 @@ test "Screen: resize more cols with reflow that fits full width" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '2'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '2'), list_cell.cell.content.codepoint.data);
}
// Resize and verify we undid the soft wrap because we have space now
@@ -6947,8 +7575,9 @@ test "Screen: resize more cols with reflow that fits full width" {
test "Screen: resize more cols with reflow that ends in newline" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 6, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 6, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -6968,7 +7597,7 @@ test "Screen: resize more cols with reflow that ends in newline" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint.data);
}
// Resize and verify we undid the soft wrap because we have space now
@@ -6985,15 +7614,16 @@ test "Screen: resize more cols with reflow that ends in newline" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint.data);
}
}
test "Screen: resize more cols with reflow that forces more wrapping" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -7005,7 +7635,7 @@ test "Screen: resize more cols with reflow that forces more wrapping" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '2'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '2'), list_cell.cell.content.codepoint.data);
}
// Verify we soft wrapped
@@ -7033,8 +7663,9 @@ test "Screen: resize more cols with reflow that forces more wrapping" {
test "Screen: resize more cols with reflow that unwraps multiple times" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH3IJKL";
try s.testWriteString(str);
@@ -7046,7 +7677,7 @@ test "Screen: resize more cols with reflow that unwraps multiple times" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '3'), list_cell.cell.content.codepoint.data);
}
// Verify we soft wrapped
@@ -7074,8 +7705,9 @@ test "Screen: resize more cols with reflow that unwraps multiple times" {
test "Screen: resize more cols with populated scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD5EFGH";
try s.testWriteString(str);
@@ -7093,7 +7725,7 @@ test "Screen: resize more cols with populated scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '5'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '5'), list_cell.cell.content.codepoint.data);
}
// Resize
@@ -7111,7 +7743,7 @@ test "Screen: resize more cols with populated scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u21, '5'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '5'), list_cell.cell.content.codepoint.data);
}
}
@@ -7123,11 +7755,12 @@ test "Screen: resize more cols bounded scrollback keeps viewport valid" {
// resize path. A direct PageList resize repro does not hit the same bug.
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{
+ var s = try init(io, alloc, .{
.cols = 2,
.rows = 10,
- .max_scrollback = 10_000,
+ .max_scrollback_bytes = 10_000,
});
defer s.deinit();
@@ -7158,7 +7791,7 @@ test "Screen: resize more cols bounded scrollback keeps viewport valid" {
for (0..s.pages.cols) |x| {
page.getRowAndCell(x, y).cell.* = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = 'A' },
+ .content = .{ .codepoint = @bitCast(@as(u24, 'A')) },
};
}
}
@@ -7198,8 +7831,9 @@ test "Screen: resize more cols bounded scrollback keeps viewport valid" {
test "Screen: resize more cols with reflow" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1ABC\n2DEF\n3ABC\n4DEF";
try s.testWriteString(str);
@@ -7211,7 +7845,7 @@ test "Screen: resize more cols with reflow" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u32, 'E'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'E'), list_cell.cell.content.codepoint.data);
}
// Verify we soft wrapped
@@ -7236,11 +7870,208 @@ test "Screen: resize more cols with reflow" {
try testing.expectEqual(@as(size.CellCountInt, 2), s.cursor.y);
}
+test "Screen: resize errors preserve state" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ for (std.meta.tags(resize_tw.FailPoint)) |tag| {
+ const tw = resize_tw;
+ defer tw.end(.reset) catch unreachable;
+
+ var s = try init(io, alloc, .{
+ .cols = 10,
+ .rows = 3,
+ .max_scrollback_bytes = 0,
+ });
+ defer s.deinit();
+
+ s.cursorSetSemanticContent(.{ .prompt = .initial });
+ try s.testWriteString("> ");
+ s.cursorSetSemanticContent(.{ .input = .clear_explicit });
+ try s.testWriteString("echo");
+ try s.setAttribute(.{ .bold = {} });
+ try s.startHyperlink("https://example.com", "resize");
+ s.saved_cursor = .{
+ .x = 1,
+ .y = 0,
+ .style = s.cursor.style,
+ .protected = s.cursor.protected,
+ .pending_wrap = s.cursor.pending_wrap,
+ .origin = false,
+ .charset = s.charset,
+ };
+
+ if (comptime build_options.kitty_graphics) {
+ s.kitty_images.dirty = false;
+ }
+
+ // Keep a shallow copy for all non-page state and a byte-for-byte
+ // copy of the sole page so reference counts and prompt contents are
+ // covered as well.
+ try testing.expectEqual(s.pages.pages.first, s.pages.pages.last);
+ const before = s;
+ const before_viewport_pin = s.pages.viewport_pin.*;
+ const before_tracked_pins = s.pages.countTrackedPins();
+ const before_page = try alloc.dupe(
+ u8,
+ s.pages.pages.first.?.page().memory,
+ );
+ defer alloc.free(before_page);
+
+ tw.errorAlways(tag, error.OutOfMemory);
+ try testing.expectError(error.OutOfMemory, s.resize(.{
+ .cols = 20,
+ .rows = 4,
+ .prompt_redraw = .true,
+ }));
+
+ try testing.expect(std.meta.eql(before.cursor, s.cursor));
+ try testing.expect(std.meta.eql(before.saved_cursor, s.saved_cursor));
+ try testing.expect(std.meta.eql(before.selection, s.selection));
+ try testing.expect(std.meta.eql(before.charset, s.charset));
+ try testing.expectEqual(before.protected_mode, s.protected_mode);
+ try testing.expect(std.meta.eql(before.kitty_keyboard, s.kitty_keyboard));
+ try testing.expect(std.meta.eql(before.semantic_prompt, s.semantic_prompt));
+ try testing.expectEqual(before.dirty, s.dirty);
+ try testing.expectEqual(before.pages.pages.first, s.pages.pages.first);
+ try testing.expectEqual(before.pages.pages.last, s.pages.pages.last);
+ try testing.expectEqual(before.pages.cols, s.pages.cols);
+ try testing.expectEqual(before.pages.rows, s.pages.rows);
+ try testing.expectEqual(before.pages.total_rows, s.pages.total_rows);
+ try testing.expectEqual(before.pages.viewport, s.pages.viewport);
+ try testing.expectEqual(before_viewport_pin, s.pages.viewport_pin.*);
+ try testing.expectEqual(before_tracked_pins, s.pages.countTrackedPins());
+ if (std.valgrind.runningOnValgrind() > 0) {
+ // This assertion deliberately compares the complete raw page,
+ // including semantically irrelevant struct padding.
+ std.valgrind.memcheck.makeMemDefined(before_page);
+ std.valgrind.memcheck.makeMemDefined(
+ s.pages.pages.first.?.page().memory,
+ );
+ }
+ try testing.expectEqualSlices(
+ u8,
+ before_page,
+ s.pages.pages.first.?.page().memory,
+ );
+ if (comptime build_options.kitty_graphics) {
+ try testing.expectEqual(
+ before.kitty_images.dirty,
+ s.kitty_images.dirty,
+ );
+ }
+ }
+}
+
+test "Screen: resize cursor references when node survives" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{
+ .cols = 5,
+ .rows = 3,
+ .max_scrollback_bytes = 1000,
+ });
+ defer s.deinit();
+
+ try s.setAttribute(.bold);
+ try s.startHyperlink("https://example.com/", "resize");
+ try s.testWriteString("abc");
+
+ const original_node = s.cursor.page_pin.node;
+ const original_serial = original_node.serial;
+ {
+ const page = original_node.page();
+ try testing.expectEqual(
+ 4,
+ page.styles.refCount(page.memory, s.cursor.style_id),
+ );
+ try testing.expectEqual(
+ 4,
+ page.hyperlink_set.refCount(page.memory, s.cursor.hyperlink_id),
+ );
+ }
+
+ // A row-only resize grows the existing page without replacing the
+ // cursor's node. The temporary resize references must be released from
+ // this page after the cursor state is restored.
+ try s.resize(.{ .cols = 5, .rows = 4 });
+
+ try testing.expectEqual(original_node, s.cursor.page_pin.node);
+ try testing.expectEqual(original_serial, s.cursor.page_pin.node.serial);
+ {
+ const page = s.cursor.page_pin.node.page();
+ try testing.expectEqual(
+ 4,
+ page.styles.refCount(page.memory, s.cursor.style_id),
+ );
+ try testing.expectEqual(
+ 4,
+ page.hyperlink_set.refCount(page.memory, s.cursor.hyperlink_id),
+ );
+ }
+}
+
+test "Screen: resize cursor references when node is replaced" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{
+ .cols = 5,
+ .rows = 3,
+ .max_scrollback_bytes = 1000,
+ });
+ defer s.deinit();
+
+ try s.setAttribute(.bold);
+ try s.startHyperlink("https://example.com/", "resize");
+ try s.testWriteString("abc");
+
+ const original_node = s.cursor.page_pin.node;
+ const original_serial = original_node.serial;
+ {
+ const page = original_node.page();
+ try testing.expectEqual(
+ 4,
+ page.styles.refCount(page.memory, s.cursor.style_id),
+ );
+ try testing.expectEqual(
+ 4,
+ page.hyperlink_set.refCount(page.memory, s.cursor.hyperlink_id),
+ );
+ }
+
+ // A column resize with reflow replaces the page and remaps the tracked
+ // cursor pin. The old page owns the temporary references, so destroying
+ // it must account for them without attempting to release them afterward.
+ try s.resize(.{ .cols = 10, .rows = 3 });
+
+ try testing.expect(
+ s.cursor.page_pin.node != original_node or
+ s.cursor.page_pin.node.serial != original_serial,
+ );
+ {
+ const page = s.cursor.page_pin.node.page();
+ try testing.expectEqual(
+ 4,
+ page.styles.refCount(page.memory, s.cursor.style_id),
+ );
+ try testing.expectEqual(
+ 4,
+ page.hyperlink_set.refCount(page.memory, s.cursor.hyperlink_id),
+ );
+ }
+}
+
test "Screen: resize more rows and cols with wrapping" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 4, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 4, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1A2B\n3C4D";
try s.testWriteString(str);
@@ -7272,8 +8103,9 @@ test "Screen: resize more rows and cols with wrapping" {
test "Screen: resize less rows no scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -7303,8 +8135,9 @@ test "Screen: resize less rows no scrollback" {
test "Screen: resize less rows moving cursor" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -7316,7 +8149,7 @@ test "Screen: resize less rows moving cursor" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u32, 'I'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'I'), list_cell.cell.content.codepoint.data);
}
// Resize
@@ -7343,8 +8176,9 @@ test "Screen: resize less rows moving cursor" {
test "Screen: resize less rows with empty scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -7366,8 +8200,9 @@ test "Screen: resize less rows with empty scrollback" {
test "Screen: resize less rows with populated scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -7397,8 +8232,9 @@ test "Screen: resize less rows with populated scrollback" {
test "Screen: resize less rows with full scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
const str = "00000\n1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -7437,8 +8273,9 @@ test "Screen: resize less rows with full scrollback" {
test "Screen: resize less cols no reflow" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1AB\n2EF\n3IJ";
try s.testWriteString(str);
@@ -7466,8 +8303,9 @@ test "Screen: resize less cols no reflow" {
test "Screen: resize less cols with reflow but row space" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
const str = "1ABCD";
try s.testWriteString(str);
@@ -7479,7 +8317,7 @@ test "Screen: resize less cols with reflow but row space" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u32, 'D'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'D'), list_cell.cell.content.codepoint.data);
}
try s.resize(.{ .cols = 3, .rows = 3 });
@@ -7504,8 +8342,9 @@ test "Screen: resize less cols with reflow but row space" {
test "Screen: resize less cols with reflow with trimmed rows" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -7528,8 +8367,9 @@ test "Screen: resize less cols with reflow with trimmed rows" {
test "Screen: resize less cols with reflow with trimmed rows and scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
const str = "3IJKL\n4ABCD\n5EFGH";
try s.testWriteString(str);
@@ -7552,8 +8392,9 @@ test "Screen: resize less cols with reflow with trimmed rows and scrollback" {
test "Screen: resize less cols with reflow previously wrapped" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "3IJKL4ABCD5EFGH";
try s.testWriteString(str);
@@ -7585,8 +8426,9 @@ test "Screen: resize less cols with reflow previously wrapped" {
test "Screen: resize less cols with reflow and scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1A\n2B\n3C\n4D\n5E";
try s.testWriteString(str);
@@ -7598,7 +8440,7 @@ test "Screen: resize less cols with reflow and scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u32, 'E'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'E'), list_cell.cell.content.codepoint.data);
}
try s.resize(.{ .cols = 3, .rows = 3 });
@@ -7618,8 +8460,9 @@ test "Screen: resize less cols with reflow and scrollback" {
test "Screen: resize less cols with reflow previously wrapped and scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 2 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 2 });
defer s.deinit();
const str = "1ABCD2EFGH3IJKL4ABCD5EFGH";
try s.testWriteString(str);
@@ -7639,7 +8482,7 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u32, 'H'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'H'), list_cell.cell.content.codepoint.data);
}
try s.resize(.{ .cols = 3, .rows = 3 });
@@ -7665,15 +8508,16 @@ test "Screen: resize less cols with reflow previously wrapped and scrollback" {
.x = s.cursor.x,
.y = s.cursor.y,
} }).?;
- try testing.expectEqual(@as(u32, 'H'), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'H'), list_cell.cell.content.codepoint.data);
}
}
test "Screen: resize less cols with scrollback keeps cursor row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
const str = "1A\n2B\n3C\n4D\n5E";
try s.testWriteString(str);
@@ -7701,8 +8545,9 @@ test "Screen: resize less cols with scrollback keeps cursor row" {
test "Screen: resize more rows, less cols with reflow with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 3 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 3 });
defer s.deinit();
const str = "1ABCD\n2EFGH3IJKL\n4MNOP";
try s.testWriteString(str);
@@ -7742,8 +8587,9 @@ test "Screen: resize more rows, less cols with reflow with scrollback" {
test "Screen: resize more rows then shrink again" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 10 });
defer s.deinit();
const str = "1ABC";
try s.testWriteString(str);
@@ -7791,8 +8637,9 @@ test "Screen: resize more rows then shrink again" {
test "Screen: resize less cols to eliminate wide char" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 1, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 1, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "😀";
try s.testWriteString(str);
@@ -7805,7 +8652,7 @@ test "Screen: resize less cols to eliminate wide char" {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.wide, cell.wide);
- try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint.data);
}
// Resize to 1 column can't fit a wide char. So it should be deleted.
@@ -7818,7 +8665,7 @@ test "Screen: resize less cols to eliminate wide char" {
{
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
@@ -7826,8 +8673,9 @@ test "Screen: resize less cols to eliminate wide char" {
test "Screen: resize less cols to wrap wide char" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 3, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 3, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "x😀";
try s.testWriteString(str);
@@ -7840,7 +8688,7 @@ test "Screen: resize less cols to wrap wide char" {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.wide, cell.wide);
- try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint.data);
}
{
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
@@ -7865,8 +8713,9 @@ test "Screen: resize less cols to wrap wide char" {
test "Screen: resize less cols to eliminate wide char with row space" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 2, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "😀";
try s.testWriteString(str);
@@ -7879,7 +8728,7 @@ test "Screen: resize less cols to eliminate wide char with row space" {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.wide, cell.wide);
- try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint.data);
}
{
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
@@ -7898,7 +8747,8 @@ test "Screen: resize less cols to eliminate wide char with row space" {
test "Screen: resize less cols reflows cursor after wrapped text" {
const testing = std.testing;
const alloc = testing.allocator;
- var s = try Screen.init(alloc, .{ .cols = 50, .rows = 7, .max_scrollback = 0 });
+ const io = testing.io;
+ var s = try Screen.init(io, alloc, .{ .cols = 50, .rows = 7, .max_scrollback_bytes = 0 });
defer s.deinit();
for (0..30) |_| try s.testWriteString("a");
@@ -7915,7 +8765,8 @@ test "Screen: resize less cols reflows cursor after wrapped text" {
test "Screen: resize less cols reflows cursor after empty cells" {
const testing = std.testing;
const alloc = testing.allocator;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 0 });
+ const io = testing.io;
+ var s = try Screen.init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("abc");
@@ -7933,8 +8784,9 @@ test "Screen: resize less cols reflows cursor after empty cells" {
test "Screen: resize more cols with wide spacer head" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 3, .rows = 2, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 3, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = " 😀";
try s.testWriteString(str);
@@ -7974,7 +8826,7 @@ test "Screen: resize more cols with wide spacer head" {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.wide, cell.wide);
- try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint.data);
}
{
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?;
@@ -7986,8 +8838,9 @@ test "Screen: resize more cols with wide spacer head" {
test "Screen: resize more cols with wide spacer head multiple lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 3, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 3, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "xxxyy😀";
try s.testWriteString(str);
@@ -8025,7 +8878,7 @@ test "Screen: resize more cols with wide spacer head multiple lines" {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.wide, cell.wide);
- try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint.data);
}
{
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 6, .y = 0 } }).?;
@@ -8037,8 +8890,9 @@ test "Screen: resize more cols with wide spacer head multiple lines" {
test "Screen: resize more cols requiring a wide spacer head" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 2, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "xx😀";
try s.testWriteString(str);
@@ -8076,7 +8930,7 @@ test "Screen: resize more cols requiring a wide spacer head" {
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.wide, cell.wide);
- try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '😀'), cell.content.codepoint.data);
}
{
const list_cell = s.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?;
@@ -8088,8 +8942,9 @@ test "Screen: resize more cols requiring a wide spacer head" {
test "Screen: resize more cols with cursor at prompt" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
// zig fmt: off
@@ -8128,8 +8983,9 @@ test "Screen: resize more cols with cursor at prompt" {
test "Screen: resize more cols with cursor not at prompt" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
// zig fmt: off
@@ -8169,8 +9025,9 @@ test "Screen: resize more cols with cursor not at prompt" {
test "Screen: resize with prompt_redraw last clears only one line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 4, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 4, .max_scrollback_bytes = 5 });
defer s.deinit();
// zig fmt: off
@@ -8208,8 +9065,9 @@ test "Screen: resize with prompt_redraw last clears only one line" {
test "Screen: resize with prompt_redraw last multiline prompt clears only last line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 5 });
defer s.deinit();
// Create a 3-line prompt: 1 initial + 2 continuation lines
@@ -8248,8 +9106,9 @@ test "Screen: resize with prompt_redraw last multiline prompt clears only last l
test "Screen: select untracked" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("ABC DEF\n 123\n456");
@@ -8268,8 +9127,9 @@ test "Screen: select untracked" {
test "Screen: select replaces existing pins" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("ABC DEF\n 123\n456");
@@ -8294,8 +9154,9 @@ test "Screen: select replaces existing pins" {
test "Screen: reselecting tracked selection preserves its pins" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 2, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.select(Selection.init(
@@ -8311,8 +9172,9 @@ test "Screen: reselecting tracked selection preserves its pins" {
test "Screen: selectAll" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
{
@@ -8347,8 +9209,9 @@ test "Screen: selectAll" {
test "Screen: selectLine" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("ABC DEF\n 123\n456");
@@ -8428,8 +9291,9 @@ test "Screen: selectLine" {
test "Screen: selectLine across soft-wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString(" 12 34012 \n 123");
@@ -8454,8 +9318,9 @@ test "Screen: selectLine across soft-wrap" {
test "Screen: selectLine across full soft-wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1ABCD2EFGH\n3IJKL");
@@ -8479,8 +9344,9 @@ test "Screen: selectLine across full soft-wrap" {
test "Screen: selectLine across soft-wrap ignores blank lines" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString(" 12 34012 \n 123");
@@ -8539,8 +9405,9 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
test "Screen: selectLine disabled whitespace trimming" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString(" 12 34012 \n 123");
@@ -8588,8 +9455,9 @@ test "Screen: selectLine disabled whitespace trimming" {
test "Screen: selectLine with scrollback" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 2, .rows = 3, .max_scrollback = 5 });
+ var s = try init(io, alloc, .{ .cols = 2, .rows = 3, .max_scrollback_bytes = 5 });
defer s.deinit();
try s.testWriteString("1A\n2B\n3C\n4D\n5E");
@@ -8632,8 +9500,9 @@ test "Screen: selectLine with scrollback" {
test "Screen: selectLine semantic prompt boundary" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("ABCDE\n");
s.cursorSetSemanticContent(.{ .prompt = .initial });
@@ -8682,8 +9551,9 @@ test "Screen: selectLine semantic prompt boundary" {
test "Screen: selectLine semantic prompt to input boundary" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Write prompt followed by user input on same row: "$>command"
@@ -8731,8 +9601,9 @@ test "Screen: selectLine semantic prompt to input boundary" {
test "Screen: selectLine semantic input to output boundary" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Row 0: user input
@@ -8776,8 +9647,9 @@ test "Screen: selectLine semantic input to output boundary" {
test "Screen: selectLine semantic mid-row boundary" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Single row with output then prompt then input: "out$>cmd"
@@ -8844,8 +9716,9 @@ test "Screen: selectLine semantic mid-row boundary" {
test "Screen: selectLine semantic boundary soft-wrap with mid-row transition" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Row 0: prompt "$ " + input "cmd" (soft-wraps)
@@ -8913,8 +9786,9 @@ test "Screen: selectLine semantic boundary soft-wrap with mid-row transition" {
test "Screen: selectLine semantic boundary disabled" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Write prompt followed by input
@@ -8945,8 +9819,9 @@ test "Screen: selectLine semantic boundary disabled" {
test "Screen: selectLine semantic boundary first cell of row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Row 0: input that soft-wraps
@@ -9001,8 +9876,9 @@ test "Screen: selectLine semantic boundary first cell of row" {
test "Screen: selectLine semantic boundary across mixed-width pages" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 4, .rows = 2, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 4, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
@@ -9032,8 +9908,9 @@ test "Screen: selectLine semantic boundary across mixed-width pages" {
test "Screen: selectLine semantic all same content" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// All prompt content that soft-wraps
@@ -9066,15 +9943,19 @@ test "Screen: selectLine semantic all same content" {
test "Screen: selectWord" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("ABC DEF\n 123\n456");
// Default boundary codepoints for word selection
const boundary_codepoints = &[_]u21{
- 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';',
- ',', '(', ')', '[', ']', '{', '}', '<', '>', '$',
+ 0, ' ', '\t', '\'', '"',
+ '│',
+ '`', '|', ':', ';', ',',
+ '(', ')', '[', ']', '{',
+ '}', '<', '>', '$',
};
// Outside of active area
@@ -9187,15 +10068,19 @@ test "Screen: selectWord" {
test "Screen: selectWord across soft-wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString(" 1234012\n 123");
// Default boundary codepoints for word selection
const boundary_codepoints = &[_]u21{
- 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';',
- ',', '(', ')', '[', ']', '{', '}', '<', '>', '$',
+ 0, ' ', '\t', '\'', '"',
+ '│',
+ '`', '|', ':', ';', ',',
+ '(', ')', '[', ']', '{',
+ '}', '<', '>', '$',
};
{
@@ -9259,15 +10144,19 @@ test "Screen: selectWord across soft-wrap" {
test "Screen: selectWord whitespace across soft-wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString("1 1\n 123");
// Default boundary codepoints for word selection
const boundary_codepoints = &[_]u21{
- 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';',
- ',', '(', ')', '[', ']', '{', '}', '<', '>', '$',
+ 0, ' ', '\t', '\'', '"',
+ '│',
+ '`', '|', ':', ';', ',',
+ '(', ')', '[', ']', '{',
+ '}', '<', '>', '$',
};
// Going forward
@@ -9325,11 +10214,15 @@ test "Screen: selectWord whitespace across soft-wrap" {
test "Screen: selectWord with character boundary" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
// Default boundary codepoints for word selection
const boundary_codepoints = &[_]u21{
- 0, ' ', '\t', '\'', '"', '│', '`', '|', ':', ';',
- ',', '(', ')', '[', ']', '{', '}', '<', '>', '$',
+ 0, ' ', '\t', '\'', '"',
+ '│',
+ '`', '|', ':', ';', ',',
+ '(', ')', '[', ']', '{',
+ '}', '<', '>', '$',
};
const cases = [_][]const u8{
@@ -9353,7 +10246,7 @@ test "Screen: selectWord with character boundary" {
};
for (cases) |case| {
- var s = try init(alloc, .{ .cols = 20, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.testWriteString(case);
@@ -9432,8 +10325,9 @@ test "Screen: selectWord with character boundary" {
test "Screen: selectOutput" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 15, .max_scrollback_bytes = 0 });
defer s.deinit();
// Build content with cell-level semantic content:
@@ -9528,8 +10422,9 @@ test "Screen: selectOutput" {
test "Screen: selectionString basic" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -9553,8 +10448,9 @@ test "Screen: selectionString basic" {
test "Screen: selectionString start outside of written area" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -9578,8 +10474,9 @@ test "Screen: selectionString start outside of written area" {
test "Screen: selectionString end outside of written area" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -9603,8 +10500,9 @@ test "Screen: selectionString end outside of written area" {
test "Screen: selectionString trim space" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1AB \n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -9640,8 +10538,9 @@ test "Screen: selectionString trim space" {
test "Screen: selectionString trim empty line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1AB \n\n2EFGH\n3IJKL";
try s.testWriteString(str);
@@ -9677,8 +10576,9 @@ test "Screen: selectionString trim empty line" {
test "Screen: selectionString soft wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH3IJKL";
try s.testWriteString(str);
@@ -9702,8 +10602,9 @@ test "Screen: selectionString soft wrap" {
test "Screen: selectionString wide char" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1A⚡";
try s.testWriteString(str);
@@ -9757,8 +10658,9 @@ test "Screen: selectionString wide char" {
test "Screen: selectionString wide char with header" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 3, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABC⚡";
try s.testWriteString(str);
@@ -9783,8 +10685,9 @@ test "Screen: selectionString wide char with header" {
test "Screen: selectionString empty with soft wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 2, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 2, .max_scrollback_bytes = 0 });
defer s.deinit();
// Let me describe the situation that caused this because this
@@ -9816,8 +10719,9 @@ test "Screen: selectionString empty with soft wrap" {
test "Screen: selectionString with zero width joiner" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 1, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 1, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "👨‍"; // this has a ZWJ
try s.testWriteString(str);
@@ -9826,7 +10730,7 @@ test "Screen: selectionString with zero width joiner" {
{
const pin = s.pages.pin(.{ .screen = .{ .y = 0, .x = 0 } }).?;
const cell = pin.rowAndCell().cell;
- try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = pin.node.page().lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
@@ -9852,8 +10756,9 @@ test "Screen: selectionString with zero width joiner" {
test "Screen: selectionString, rectangle, basic" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 30, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 30, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
const str =
\\Lorem ipsum dolor
@@ -9885,8 +10790,9 @@ test "Screen: selectionString, rectangle, basic" {
test "Screen: selectionString, rectangle, w/EOL" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 30, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 30, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
const str =
\\Lorem ipsum dolor
@@ -9920,8 +10826,9 @@ test "Screen: selectionString, rectangle, w/EOL" {
test "Screen: selectionString, rectangle, more complex w/breaks" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 30, .rows = 8, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 30, .rows = 8, .max_scrollback_bytes = 0 });
defer s.deinit();
const str =
\\Lorem ipsum dolor
@@ -9959,8 +10866,9 @@ test "Screen: selectionString, rectangle, more complex w/breaks" {
test "Screen: selectionString multi-page" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 2048 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 2048 });
defer s.deinit();
const first_page_size = s.pages.pages.first.?.capacity().rows;
@@ -9993,8 +10901,9 @@ test "Screen: selectionString multi-page" {
test "Screen: lineIterator" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD\n2EFGH";
try s.testWriteString(str);
@@ -10024,8 +10933,9 @@ test "Screen: lineIterator" {
test "Screen: lineIterator soft wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
const str = "1ABCD2EFGH\n3ABCD";
try s.testWriteString(str);
@@ -10056,8 +10966,9 @@ test "Screen: lineIterator soft wrap" {
test "Screen: hyperlink start/end" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try testing.expect(s.cursor.hyperlink_id == 0);
{
@@ -10083,8 +10994,9 @@ test "Screen: hyperlink start/end" {
test "Screen: hyperlink accepts its current values" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try s.startHyperlink("http://example.com", "current");
@@ -10098,8 +11010,9 @@ test "Screen: hyperlink accepts its current values" {
test "Screen: implicit hyperlink ID wraps" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
s.cursor.hyperlink_implicit_id = std.math.maxInt(size.OffsetInt);
@@ -10135,8 +11048,9 @@ test "Screen: implicit hyperlink ID wraps" {
test "Screen: hyperlink reuse" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
try testing.expect(s.cursor.hyperlink_id == 0);
@@ -10169,12 +11083,13 @@ test "Screen: hyperlink reuse" {
test "Screen: hyperlink cursor state on resize" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
// This test depends on underlying PageList implementation so
// it may be invalid one day. It's here to document/verify the
// current behavior.
- var s = try init(alloc, .{ .cols = 5, .rows = 10, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 10, .max_scrollback_bytes = 0 });
defer s.deinit();
// Start a hyperlink
@@ -10204,8 +11119,9 @@ test "Screen: hyperlink cursor state on resize" {
test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 80, .rows = 24, .max_scrollback_bytes = 0 });
defer s.deinit();
// Start a hyperlink with a URI that just barely fits in the string alloc.
@@ -10238,11 +11154,12 @@ test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" {
test "Screen: increaseCapacity cursor style ref count preserved" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{
+ var s = try init(io, alloc, .{
.cols = 5,
.rows = 5,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
try s.setAttribute(.bold);
@@ -10293,11 +11210,12 @@ test "Screen: increaseCapacity cursor style ref count preserved" {
test "Screen: increaseCapacity cursor hyperlink ref count preserved" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{
+ var s = try init(io, alloc, .{
.cols = 5,
.rows = 5,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
try s.startHyperlink("https://example.com/", null);
@@ -10336,11 +11254,12 @@ test "Screen: increaseCapacity cursor hyperlink ref count preserved" {
test "Screen: increaseCapacity cursor with both style and hyperlink preserved" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{
+ var s = try init(io, alloc, .{
.cols = 5,
.rows = 5,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
@@ -10401,11 +11320,12 @@ test "Screen: increaseCapacity non-cursor page returns early" {
// extra cursor accounting (style/hyperlink re-adding).
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{
+ var s = try init(io, alloc, .{
.cols = 80,
.rows = 24,
- .max_scrollback = 10000,
+ .max_scrollback_bytes = 10000,
});
defer s.deinit();
@@ -10486,9 +11406,10 @@ test "Screen: cursorDown to page with insufficient capacity" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
// Small screen to make page boundary crossing easy to set up
- var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 3, .max_scrollback_bytes = 1 });
defer s.deinit();
// Scroll down enough to create a second page
@@ -10555,9 +11476,10 @@ test "Screen setAttribute increases capacity when style map is full" {
// returns OutOfSpace, manualStyleUpdate will split the page instead.
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
// Use a small screen with multiple rows
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
defer s.deinit();
// Write content to multiple rows
@@ -10604,11 +11526,12 @@ test "Screen setAttribute increases capacity when style map is full" {
test "Screen setAttribute splits page on OutOfSpace at max styles" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{
+ var s = try init(io, alloc, .{
.cols = 10,
.rows = 10,
- .max_scrollback = 0,
+ .max_scrollback_bytes = 0,
});
defer s.deinit();
@@ -10670,42 +11593,12 @@ test "Screen setAttribute splits page on OutOfSpace at max styles" {
try testing.expect(page_was_split);
}
-test "selectionString map allocation failure cleanup" {
- // This test verifies that if toOwnedSlice fails when building
- // the StringMap, we don't leak the already-allocated map.string.
- const testing = std.testing;
- const alloc = testing.allocator;
- var s = try Screen.init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
- defer s.deinit();
-
- try s.testWriteString("hello");
-
- // Get a selection
- const sel = Selection.init(
- s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
- s.pages.pin(.{ .active = .{ .x = 4, .y = 0 } }).?,
- false,
- );
-
- // Trigger allocation failure on toOwnedSlice
- var map: StringMap = undefined;
- selectionString_tw.errorAlways(.copy_map, error.OutOfMemory);
- const result = s.selectionString(alloc, .{
- .sel = sel,
- .map = &map,
- });
- try testing.expectError(error.OutOfMemory, result);
- try selectionString_tw.end(.reset);
-
- // If this test passes without memory leaks (when run with testing.allocator),
- // it means the errdefer properly cleaned up map.string when toOwnedSlice failed.
-}
-
test "Screen: promptClickMove line right basic" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10731,8 +11624,9 @@ test "Screen: promptClickMove line right basic" {
test "Screen: promptClickMove line right cursor not on input" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10758,8 +11652,9 @@ test "Screen: promptClickMove line right cursor not on input" {
test "Screen: promptClickMove line right click on same position" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10784,8 +11679,9 @@ test "Screen: promptClickMove line right click on same position" {
test "Screen: promptClickMove line right skips non-input cells" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10816,8 +11712,9 @@ test "Screen: promptClickMove line right skips non-input cells" {
test "Screen: promptClickMove line right soft-wrapped line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10854,8 +11751,9 @@ test "Screen: promptClickMove line right soft-wrapped line" {
test "Screen: promptClickMove disabled when click is none" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Click mode is .none by default (disabled)
@@ -10880,8 +11778,9 @@ test "Screen: promptClickMove disabled when click is none" {
test "Screen: promptClickMove line right stops at hard wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10912,8 +11811,9 @@ test "Screen: promptClickMove line right stops at hard wrap" {
test "Screen: promptClickMove line right stops at non-continuation row" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10959,8 +11859,9 @@ test "Screen: promptClickMove line right stops at non-continuation row" {
test "Screen: promptClickMove line left basic" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -10986,8 +11887,9 @@ test "Screen: promptClickMove line left basic" {
test "Screen: promptClickMove line left skips non-input cells" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11018,8 +11920,9 @@ test "Screen: promptClickMove line left skips non-input cells" {
test "Screen: promptClickMove line left soft-wrapped line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11056,8 +11959,9 @@ test "Screen: promptClickMove line left soft-wrapped line" {
test "Screen: promptClickMove line left stops at hard wrap" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11088,11 +11992,12 @@ test "Screen: promptClickMove line left stops at hard wrap" {
test "Screen: promptClickMove click right of input same line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
// Set up: "> hello" where "> " is prompt and "hello" is input
// Clicking to the right of the 'o' should move cursor past the input
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11118,8 +12023,9 @@ test "Screen: promptClickMove click right of input same line" {
test "Screen: promptClickMove click right of input cursor at end" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11144,8 +12050,9 @@ test "Screen: promptClickMove click right of input cursor at end" {
test "Screen: promptClickMove click right of input on lower line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11173,8 +12080,9 @@ test "Screen: promptClickMove click right of input on lower line" {
test "Screen: promptClickMove click right of input cursor at end lower line" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11198,8 +12106,9 @@ test "Screen: promptClickMove click right of input cursor at end lower line" {
test "Screen: promptClickMove click right of input cursor on last char" {
const testing = std.testing;
const alloc = testing.allocator;
+ const io = testing.io;
- var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
+ var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();
// Enable line click mode
@@ -11221,3 +12130,222 @@ test "Screen: promptClickMove click right of input cursor on last char" {
try testing.expectEqual(@as(usize, 1), result.right);
try testing.expectEqual(@as(usize, 0), result.left);
}
+
+test "Screen: cursorScrollRegionUp recycled row has default metadata" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 5, .max_scrollback_bytes = 0 });
+ defer s.deinit();
+ try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4MNOP\n5QRST");
+
+ // Simulate the top region row being part of a soft-wrapped,
+ // prompt-marked line. Its Row storage is recycled as the new
+ // blank cursor row and must not retain the metadata.
+ {
+ const rac = s.pages.getCell(.{ .active = .{} }).?;
+ rac.row.wrap = true;
+ rac.row.wrap_continuation = true;
+ rac.row.semantic_prompt = .prompt;
+ }
+
+ s.cursorAbsolute(1, 2);
+ try s.cursorScrollRegionUp(2);
+
+ {
+ const rac = s.pages.getCell(.{ .active = .{ .y = 2 } }).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+ {
+ const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
+ defer alloc.free(contents);
+ try testing.expectEqualStrings("2EFGH\n3IJKL\n\n4MNOP\n5QRST", contents);
+ }
+}
+
+test "Screen: cursorScrollRegionUp cross-page recycled row has default metadata" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
+ defer s.deinit();
+
+ // We need to get the active area to span two pages so that the
+ // scroll region does too, exercising the slow path (eraseRowBounded).
+ const first_page_size = s.pages.pages.first.?.capacity().rows;
+ s.pages.pages.first.?.page().pauseIntegrityChecks(true);
+ for (0..first_page_size - 3) |_| try s.testWriteString("\n");
+ s.pages.pages.first.?.page().pauseIntegrityChecks(false);
+ try s.testWriteString("1A\n2B\n3C\n4D\n5E");
+
+ // The first row of the last page is the Row storage that ends up
+ // recycled as the blank region-bottom row: the erased row's
+ // storage stays on the first page (receiving this row's content
+ // via clone) while this storage is cleared for the blank row.
+ {
+ const rac = s.pages.getCell(.{ .active = .{ .y = 3 } }).?;
+ rac.row.wrap = true;
+ rac.row.wrap_continuation = true;
+ rac.row.semantic_prompt = .prompt;
+ }
+
+ // Region rows 1-3 with the cursor on the region bottom, which is
+ // on the second page while the region top is on the first page.
+ s.cursorAbsolute(0, 3);
+ try s.cursorScrollRegionUp(2);
+
+ {
+ const rac = s.pages.getCell(.{ .active = .{ .y = 3 } }).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+ {
+ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
+ defer alloc.free(contents);
+ try testing.expectEqualStrings("1A\n3C\n4D\n\n5E", contents);
+ }
+}
+
+test "Screen: cursorScrollAbove cross-page recycled row has default metadata" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 10 });
+ defer s.deinit();
+
+ // Get the cursor page and the last page to differ so that
+ // cursorScrollAbove takes the cross-page rotate path.
+ const first_page_size = s.pages.pages.first.?.capacity().rows;
+ s.pages.pages.first.?.page().pauseIntegrityChecks(true);
+ for (0..first_page_size - 3) |_| try s.testWriteString("\n");
+ s.pages.pages.first.?.page().pauseIntegrityChecks(false);
+ try s.testWriteString("1A\n2B\n3C\n4D\n5E");
+ s.cursorAbsolute(0, 1);
+ try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?);
+ try testing.expect(s.pages.pages.first.?.next != null);
+
+ // The last row of the cursor page is the Row storage that gets
+ // recycled as the new blank row below the cursor after its content
+ // is moved down to the next page.
+ {
+ const rac = s.pages.getCell(.{ .active = .{ .y = 2 } }).?;
+ rac.row.wrap = true;
+ rac.row.wrap_continuation = true;
+ rac.row.semantic_prompt = .prompt;
+ }
+
+ try s.cursorScrollAbove();
+
+ // One row scrolled into history, so the blank row is at active
+ // y=1 (just below the cursor's original row).
+ {
+ const rac = s.pages.getCell(.{ .active = .{ .y = 1 } }).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+ {
+ const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
+ defer alloc.free(contents);
+ try testing.expectEqualStrings("2B\n\n3C\n4D\n5E", contents);
+ }
+}
+
+test "Screen: cursorDownScroll no scrollback recycled row has default metadata" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 3, .max_scrollback_bytes = 0 });
+ defer s.deinit();
+ try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
+
+ {
+ const rac = s.pages.getCell(.{ .active = .{} }).?;
+ rac.row.wrap = true;
+ rac.row.wrap_continuation = true;
+ rac.row.semantic_prompt = .prompt;
+ }
+
+ s.cursorAbsolute(0, 2);
+ try s.cursorDownScroll();
+
+ {
+ const rac = s.pages.getCell(.{ .active = .{ .y = 2 } }).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+}
+
+test "Screen: cursorDownScroll single row no scrollback resets metadata" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 5, .rows = 1, .max_scrollback_bytes = 0 });
+ defer s.deinit();
+ try s.testWriteString("1ABCD");
+
+ {
+ const rac = s.pages.getCell(.{ .active = .{} }).?;
+ rac.row.wrap = true;
+ rac.row.wrap_continuation = true;
+ rac.row.semantic_prompt = .prompt;
+ }
+
+ try s.cursorDownScroll();
+
+ {
+ const rac = s.pages.getCell(.{ .active = .{} }).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+}
+
+test "Screen: selectLine does not join lines across a recycled row" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const io = testing.io;
+
+ var s = try init(io, alloc, .{ .cols = 6, .rows = 5, .max_scrollback_bytes = 0 });
+ defer s.deinit();
+
+ // A soft-wrapped line across rows 0 and 1: row 0 gets wrap=true.
+ try s.testWriteString("AAAAAAA");
+
+ // Scroll a region of rows 0-2 up by one: row 0 is discarded and
+ // its Row storage recycled as the blank row 2.
+ s.cursorAbsolute(0, 2);
+ try s.cursorScrollRegionUp(2);
+
+ // Write unrelated single-line words on the recycled row and below.
+ s.cursorAbsolute(0, 2);
+ try s.testWriteString("world");
+ s.cursorAbsolute(0, 3);
+ try s.testWriteString("hello");
+
+ // Selecting the line "world" must not extend into "hello": these
+ // are separate hard lines. A stale wrap flag on the recycled row
+ // would join them.
+ {
+ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
+ .x = 0,
+ .y = 2,
+ } }).? }).?;
+ defer sel.deinit(&s);
+ const contents = try s.selectionString(alloc, .{
+ .sel = sel,
+ .trim = false,
+ });
+ defer alloc.free(contents);
+ try testing.expectEqualStrings("world", contents);
+ }
+}