aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/Terminal.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/Terminal.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/Terminal.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/Terminal.zig3630
1 files changed, 2868 insertions, 762 deletions
diff --git a/vendor/libghostty-vt/src/terminal/Terminal.zig b/vendor/libghostty-vt/src/terminal/Terminal.zig
index 08920127..e9cc3b8e 100644
--- a/vendor/libghostty-vt/src/terminal/Terminal.zig
+++ b/vendor/libghostty-vt/src/terminal/Terminal.zig
@@ -7,8 +7,10 @@ const std = @import("std");
const build_options = @import("terminal_options");
const lib = @import("lib.zig");
const assert = @import("../quirks.zig").inlineAssert;
+const tripwire = @import("../tripwire.zig");
const testing = std.testing;
const Allocator = std.mem.Allocator;
+const simd = @import("../simd/main.zig");
const unicode = @import("../unicode/main.zig");
const uucode = @import("uucode");
@@ -80,12 +82,21 @@ previous_char: ?u21 = null,
/// The modes that this terminal currently has active.
modes: modespkg.ModeState = .{},
+/// Terminal-level cursor state.
+cursor: Cursor = .{},
+
/// The most recently set mouse shape for the terminal.
mouse_shape: mouse.Shape = .text,
/// Per-session Glyph Protocol registrations.
glyph_glossary: glyph.Glossary = .empty,
+/// Kitty drag and drop protocol (OSC 72) state. Allocated when a client
+/// registers to accept drops (t=a) and freed when it unregisters (t=A),
+/// so a terminal that never runs a drag and drop aware program pays
+/// nothing for it. Non-null means a client currently accepts drops.
+kitty_dnd: ?*kitty.dnd.State = null,
+
/// These are just a packed set of flags we may set on the terminal.
flags: packed struct {
// This supports a Kitty extension where programs using semantic
@@ -112,6 +123,10 @@ flags: packed struct {
/// True if the window is focused.
focused: bool = true,
+ /// True if the terminal view may be visible. Unknown visibility is
+ /// represented as visible so callers behave conservatively.
+ visible: bool = true,
+
/// True if the terminal is in a password entry mode. This is set
/// to true based on termios state. This is set
/// to true based on termios state.
@@ -235,16 +250,42 @@ pub const ScrollingRegion = struct {
right: size.CellCountInt,
};
+/// Terminal-level cursor state shared by all screens.
+pub const Cursor = struct {
+ /// Whether the current cursor appearance follows the configured defaults.
+ is_default: bool = true,
+
+ /// Configured style restored by DECSCUSR default and RIS.
+ default_style: Screen.CursorStyle = .block,
+
+ /// Configured blink restored by DECSCUSR default and RIS. Null selects
+ /// the terminal emulator default, which is blinking.
+ default_blink: ?bool = false,
+};
+
pub const Options = struct {
cols: size.CellCountInt,
rows: size.CellCountInt,
- max_scrollback: usize = 10_000,
+
+ /// The maximum size of scrollback in bytes. Null is unlimited and zero
+ /// disables scrollback.
+ max_scrollback_bytes: ?usize = 10_000,
+
+ /// 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,
+
colors: Colors = .default,
/// The default mode state. When the terminal gets a reset, it
/// will revert back to this state.
default_modes: modespkg.ModePacked = .{},
+ /// Cursor state restored by DECSCUSR default and RIS.
+ default_cursor_style: Screen.CursorStyle = .block,
+ default_cursor_blink: ?bool = false,
+
/// The total storage limit for Kitty images in bytes. Has no effect
/// if kitty images are disabled at build-time.
kitty_image_storage_limit: usize = switch (build_options.artifact) {
@@ -268,22 +309,24 @@ pub const Options = struct {
/// Initialize a new terminal.
pub fn init(
+ io_impl: std.Io,
alloc: Allocator,
opts: Options,
) !Terminal {
const cols = opts.cols;
const rows = opts.rows;
- var screen_set: ScreenSet = try .init(alloc, .{
+ var screen_set: ScreenSet = try .init(io_impl, alloc, .{
.cols = cols,
.rows = rows,
- .max_scrollback = opts.max_scrollback,
+ .max_scrollback_bytes = opts.max_scrollback_bytes,
+ .max_scrollback_lines = opts.max_scrollback_lines,
.kitty_image_storage_limit = opts.kitty_image_storage_limit,
.kitty_image_loading_limits = opts.kitty_image_loading_limits,
});
errdefer screen_set.deinit(alloc);
- return .{
+ var result: Terminal = .{
.cols = cols,
.rows = rows,
.screens = screen_set,
@@ -301,15 +344,23 @@ pub fn init(
.values = opts.default_modes,
.default = opts.default_modes,
},
+ .cursor = .{
+ .default_style = opts.default_cursor_style,
+ .default_blink = opts.default_cursor_blink,
+ },
};
+ result.setCursorStyle(.default);
+ return result;
}
pub fn deinit(self: *Terminal, alloc: Allocator) void {
self.tabstops.deinit(alloc);
self.screens.deinit(alloc);
+ self.colors.palette.deinit(alloc);
self.pwd.deinit(alloc);
self.title.deinit(alloc);
self.glyph_glossary.deinit(alloc);
+ if (self.kitty_dnd) |dnd| dnd.destroy(alloc);
self.* = undefined;
}
@@ -328,7 +379,10 @@ pub fn deinit(self: *Terminal, alloc: Allocator) void {
/// for handling escape sequences split across write boundaries), you
/// must store and reuse the returned stream.
pub fn vtStream(self: *Terminal) Stream {
- return .initAlloc(self.gpa(), self.vtHandler());
+ return Stream.init(.{
+ .allocator = self.gpa(),
+ .handler = self.vtHandler(),
+ });
}
/// This is the handler-side only for vtStream.
@@ -336,11 +390,96 @@ pub fn vtHandler(self: *Terminal) Stream.Handler {
return .init(self);
}
+/// Change the cursor's current shape and blink behavior.
+///
+/// The terminal parser uses this for DECSCUSR (`CSI Ps SP q`), but the behavior
+/// is general: `.default` selects the configured defaults, while any other
+/// value selects a concrete appearance until it is changed again or reset.
+pub fn setCursorStyle(self: *Terminal, value: ansi.CursorStyle) void {
+ // Remember whether future configuration changes should update the visible
+ // cursor. An explicit appearance must remain in effect until the program
+ // selects the default again.
+ self.cursor.is_default = value == .default;
+
+ // Convert the request into the concrete values used by the renderer and
+ // terminal mode state. A null default blink means the emulator default.
+ self.modes.set(.cursor_blinking, switch (value) {
+ .default => self.cursor.default_blink orelse true,
+ .steady_block, .steady_bar, .steady_underline => false,
+ .blinking_block, .blinking_bar, .blinking_underline => true,
+ });
+ self.screens.active.cursor.cursor_style = switch (value) {
+ .default => self.cursor.default_style,
+ .blinking_block, .steady_block => .block,
+ .blinking_bar, .steady_bar => .bar,
+ .blinking_underline, .steady_underline => .underline,
+ };
+}
+
+/// Change the default cursor shape.
+///
+/// If the cursor currently follows its defaults, the visible shape changes
+/// immediately. Otherwise the new shape is saved for the next reset or default
+/// selection, such as DECSCUSR `CSI 0 SP q`.
+pub fn setDefaultCursorStyle(
+ self: *Terminal,
+ configured_style: Screen.CursorStyle,
+) void {
+ // Always retain the new default, even while an explicit appearance is
+ // active, so a later reset or default request can restore it.
+ self.cursor.default_style = configured_style;
+
+ // Do not overwrite an appearance explicitly selected by the program.
+ if (self.cursor.is_default) self.setCursorStyle(.default);
+}
+
+/// Change the default cursor blink behavior.
+///
+/// Null selects the terminal emulator default (blinking). Like the default
+/// shape, this is applied immediately only when the cursor currently follows
+/// its defaults; otherwise it is saved for the next reset or default selection.
+pub fn setDefaultCursorBlink(self: *Terminal, blink: ?bool) void {
+ // Keep the configured value separate from the currently resolved mode so
+ // null can continue to mean "use the emulator default."
+ self.cursor.default_blink = blink;
+
+ // Do not overwrite blink behavior explicitly selected by the program.
+ if (self.cursor.is_default) self.setCursorStyle(.default);
+}
+
+/// The I/O implementation we should use for this terminal.
+pub fn io(self: *Terminal) std.Io {
+ return self.screens.active.io;
+}
+
/// The general allocator we should use for this terminal.
pub fn gpa(self: *Terminal) Allocator {
return self.screens.active.alloc;
}
+/// Change the primary screen's maximum scrollback allocation in bytes.
+///
+/// Null removes the byte limit and zero disables scrollback. Disabling
+/// scrollback also immediately erases retained history and changes future
+/// scrolling to use the no-scrollback path. The alternate screen is
+/// intentionally unaffected because it never retains scrollback.
+pub fn setScrollbackMaxBytes(self: *Terminal, max: ?usize) void {
+ const primary = self.screens.get(.primary).?;
+ primary.pages.setMaxBytes(max);
+ primary.no_scrollback = max == 0;
+
+ if (primary.no_scrollback) primary.eraseHistory(null);
+}
+
+/// Change the primary screen's maximum number of physical scrollback lines.
+///
+/// Null removes the line limit. The alternate screen is intentionally
+/// unaffected because it never retains scrollback.
+pub fn setScrollbackMaxLines(self: *Terminal, max: ?usize) void {
+ const primary = self.screens.get(.primary).?;
+ primary.pages.setMaxLines(max);
+}
+
/// Print UTF-8 encoded string to the terminal.
pub fn printString(self: *Terminal, str: []const u8) !void {
const view = try std.unicode.Utf8View.init(str);
@@ -359,9 +498,26 @@ pub fn printString(self: *Terminal, str: []const u8) !void {
/// Print the previous printed character a repeated amount of times.
pub fn printRepeat(self: *Terminal, count_req: usize) !void {
- if (self.previous_char) |c| {
- const count = @max(count_req, 1);
- for (0..count) |_| try self.print(c);
+ const c = self.previous_char orelse return;
+ var remaining = @max(count_req, 1);
+
+ // Print the repeated codepoint in slices so that eligible runs
+ // take the batched printSlice fast path. printSlice is semantically
+ // identical to calling print per codepoint: ineligible characters
+ // or terminal states (insert mode, grapheme clustering, hyperlinks,
+ // etc.) fall back to the per-codepoint print() path internally.
+ //
+ // The buffer is filled with a runtime-bounded loop rather than
+ // `= @splat(c)`: a comptime-known 4096-element splat gets fully
+ // unrolled into ~33KB of consecutive stores (LLVM won't re-roll
+ // or vectorize it, see quirks_memset.zig), and it would fill the
+ // whole buffer even for the typical small repeat counts.
+ var buf: [4096]u32 = undefined;
+ for (buf[0..@min(remaining, buf.len)]) |*cp| cp.* = c;
+ while (remaining > 0) {
+ const n = @min(remaining, buf.len);
+ try self.printSlice(buf[0..n]);
+ remaining -= n;
}
}
@@ -381,11 +537,55 @@ pub fn printRepeat(self: *Terminal, count_req: usize) !void {
/// slower per-codepoint path. They're less common and this is optimized
/// for the aforementioned cases.
pub fn printSlice(self: *Terminal, cps: []const u32) !void {
+ // Check if we can do the fast path up front. If we can't
+ // we need to go back to scalar `print`.
+ const fast = fast: {
+ // Only the main display is supported.
+ if (self.status_display != .main) break :fast false;
+
+ // Modes that require per-codepoint handling in print().
+ // Wraparound is required (its the default) so that our
+ // row-fill logic below can assume soft-wrap semantics. Insert
+ // mode shifts cells per print.
+ if (self.modes.get(.insert)) break :fast false;
+ if (!self.modes.get(.wraparound)) break :fast false;
+
+ // Charset must map ASCII as-is (true unless a DEC special
+ // charset is actively invoked, which is rare).
+ const screen: *Screen = self.screens.active;
+ if (screen.charset.single_shift != null) break :fast false;
+ switch (screen.charset.charsets.get(screen.charset.gl)) {
+ .utf8, .ascii => {},
+ else => break :fast false,
+ }
+
+ // Hyperlinks require per-cell map bookkeeping.
+ if (screen.cursor.hyperlink_id != 0) break :fast false;
+
+ break :fast true;
+ };
+ if (!fast) {
+ for (cps) |cp| try self.print(@intCast(cp));
+ return;
+ }
+
+ const grapheme_cluster = self.modes.get(.grapheme_cluster);
+
+ // When grapheme clustering is enabled and a left margin is set,
+ // print() consults the cell left of the margin after wrapping,
+ // which we can't reason about here. Restrict the fast path to
+ // the [0x10, 0xFF] range in that case (those never cluster).
+ const allow_unicode = !grapheme_cluster or self.scrolling_region.left == 0;
+
var i: usize = 0;
while (i < cps.len) {
// Try the fast-path print first. This will return the number of
// codepoints it consumed.
- const consumed = try self.printSliceFast(cps[i..]);
+ const consumed = try self.printSliceFast(
+ cps[i..],
+ grapheme_cluster,
+ allow_unicode,
+ );
if (consumed > 0) {
i += consumed;
continue;
@@ -407,32 +607,16 @@ pub fn printSlice(self: *Terminal, cps: []const u32) !void {
///
/// The fast path handles runs of narrow (width 1) and wide (width 2)
/// codepoints being written to simple cells. Everything else (zero
-/// width codepoints, grapheme cluster continuations, insert mode,
-/// charset mapping, hyperlinks, complex cells, etc.) is rejected so
-/// `print` can handle it with full generality.
-fn printSliceFast(self: *Terminal, cps: []const u32) !usize {
- // Only the main display is supported.
- if (self.status_display != .main) return 0;
-
- // Modes that require per-codepoint handling in print(). Wraparound
- // is required (its the default) so that our row-fill logic below can
- // assume soft-wrap semantics. Insert mode shifts cells per print.
- if (self.modes.get(.insert)) return 0;
- if (!self.modes.get(.wraparound)) return 0;
-
+/// width codepoints, grapheme cluster continuations, complex cells,
+/// etc.) is rejected so `print` can handle it with full generality.
+fn printSliceFast(
+ self: *Terminal,
+ cps: []const u32,
+ grapheme_cluster: bool,
+ allow_unicode: bool,
+) !usize {
const screen: *Screen = self.screens.active;
- // Charset must map ASCII as-is (true unless a DEC special charset
- // is actively invoked, which is rare).
- if (screen.charset.single_shift != null) return 0;
- switch (screen.charset.charsets.get(screen.charset.gl)) {
- .utf8, .ascii => {},
- else => return 0,
- }
-
- // Hyperlinks require per-cell map bookkeeping.
- if (screen.cursor.hyperlink_id != 0) return 0;
-
// Codepoints in [0x10, 0xFF] are always narrow (width 1, matching
// the c <= 0xFF fast path in print) and can never interact with
// grapheme clustering (which requires a codepoint > 0xFF).
@@ -443,13 +627,6 @@ fn printSliceFast(self: *Terminal, cps: []const u32) !usize {
// 2027) is enabled, if they are a grapheme break from the
// previously printed codepoint (so print would never attach them
// to the previous cell).
- const grapheme_cluster = self.modes.get(.grapheme_cluster);
-
- // When grapheme clustering is enabled and a left margin is set,
- // print() consults the cell left of the margin after wrapping,
- // which we can't reason about here. Restrict the fast path to
- // the [0x10, 0xFF] range in that case (those never cluster).
- const allow_unicode = !grapheme_cluster or self.scrolling_region.left == 0;
// Codepoints in [0x10, 0xFF] are always narrow: print()
// hardcodes width 1 for c <= 0xFF (no width table lookup).
@@ -478,14 +655,46 @@ fn printSliceFast(self: *Terminal, cps: []const u32) !usize {
}
// The first codepoint requires care when grapheme clustering is
- // enabled: print() examines the previous *cell* which can hold
- // state (grapheme data) that we can't cheaply reason about here.
- // Note this includes the pending-wrap state: print() may attach
- // to the pending cell *instead of wrapping*. We only take the
- // first codepoint if the cursor is at column zero with no pending
- // wrap, where print() skips clustering entirely.
- if (grapheme_cluster) {
- if (screen.cursor.pending_wrap or screen.cursor.x != 0) return 0;
+ // enabled: print() may attach it to the previous *cell* instead
+ // of writing a new one. Take the first codepoint only when we can
+ // determine — computing exactly what print() would — that it
+ // starts a new cluster. At column zero with no pending wrap,
+ // print() skips clustering entirely. Otherwise resolve the
+ // previous cell the way print() does and check for a break.
+ //
+ // Note the pending-wrap rejection: print() may attach to the
+ // pending cell *instead of wrapping*, which we can't model here.
+ if (grapheme_cluster and screen.cursor.x != 0) gate: {
+ if (screen.cursor.pending_wrap) return 0;
+
+ // Resolve the content cell to our left exactly like print():
+ // if the immediate left cell is a wide spacer tail, the
+ // content lives one further left. (A spacer tail can never
+ // be at column zero — its wide half would have to be in the
+ // previous row — so the second cursorCellLeft is in bounds.)
+ const immediate = screen.cursorCellLeft(1);
+ const prev: *Cell = switch (immediate.wide) {
+ .spacer_tail => screen.cursorCellLeft(2),
+ else => immediate,
+ };
+
+ // An empty previous cell is necessarily a grapheme break.
+ if (prev.codepoint() == 0) break :gate;
+
+ // Grapheme data on the previous cell requires the full
+ // cluster state machine replay; only print() can do that.
+ if (prev.hasGrapheme()) return 0;
+
+ // A simple single-codepoint previous cell: print() would run
+ // exactly this break check from the default state.
+ var state: uucode.grapheme.BreakState = .default;
+ if (!unicode.graphemeBreak(
+ prev.content.codepoint.data,
+ @intCast(cp0),
+ &state,
+ )) return 0;
+ } else if (grapheme_cluster) {
+ if (screen.cursor.pending_wrap) return 0;
}
// The width lookup is a runtime value while printSliceFill is
@@ -540,6 +749,85 @@ inline fn printSliceEligible(cp: u32, comptime width: PrintSliceWidth) bool {
});
}
+/// Store a run of narrow codepoint cells built from a bit template:
+/// for each `idx` in `[from, to)`, `cells[idx]` is assigned the bits
+/// `template_bits | (cps[idx] << cp_shift)`.
+///
+/// The template is a complete Cell (content tag, style, wide state,
+/// etc. already baked in by the caller) whose codepoint content bits
+/// are zero. Since Cell is a packed struct(u64), OR-ing a codepoint
+/// into the content field's bit position yields a finished cell as a
+/// single integer, keeping the loop pure data movement: no per-cell
+/// field assignments and no branches.
+///
+/// This loop is manually vectorized: Zig 0.16 (LLVM 21) no longer
+/// auto-vectorizes the scalar form the way Zig 0.15 (LLVM 20) did.
+inline fn printSliceStoreRun(
+ cells: [*]Cell,
+ cps: [*]const u32,
+ from: usize,
+ to: usize,
+ template_bits: u64,
+) void {
+ // The bit position of the `content` field within the packed
+ // Cell. A codepoint occupies the low bits of `content`, so
+ // shifting a codepoint left by this amount places it exactly
+ // where `.content = .{ .codepoint = .{ .data = cp } }` would.
+ const cp_shift = @bitOffsetOf(Cell, "content");
+
+ // Since codepoints are OR'd into the content field rather than
+ // assigned, any nonzero content bits in the template would
+ // corrupt the stored codepoints.
+ const content_mask: u64 = comptime mask: {
+ const bits = @bitSizeOf(@FieldType(Cell, "content"));
+ break :mask ((1 << bits) - 1) << cp_shift;
+ };
+ assert(template_bits & content_mask == 0);
+
+ var idx = from;
+
+ // Vectorized bulk of the run.
+ if (simd.lanes(u64)) |lanes| {
+ // u64 due to backing integer of Cell
+ const V = @Vector(lanes, u64);
+
+ // The template and shift amount are loop-invariant, so
+ // broadcast them to every lane once up front.
+ const template: V = @splat(template_bits);
+ const shift: @Vector(
+ lanes,
+ std.math.Log2Int(u64),
+ ) = @splat(cp_shift);
+
+ while (idx + lanes <= to) : (idx += lanes) {
+ // Load `lanes` decoded codepoints...
+ const narrow: @Vector(lanes, u32) = cps[idx..][0..lanes].*;
+
+ // ...widen each u32 lane to the u64 cell size...
+ const wide: V = @intCast(narrow);
+
+ // ...shift each codepoint into the content field's bit
+ // position and merge with the template, producing
+ // `lanes` finished cells (coerced from vector to array
+ // so they can be bitcast for the store below)...
+ const bits: [lanes]u64 = template | (wide << shift);
+
+ // ...and store them contiguously. Cell is a packed
+ // struct(u64) so an array of u64 bit patterns has
+ // identical layout to an array of cells.
+ cells[idx..][0..lanes].* = @bitCast(bits);
+ }
+ }
+
+ // Scalar tail: the final `< lanes` cells of the run, or the
+ // entire run on targets without SIMD. Note `idx` carries over
+ // from the vector loop above. This is the same computation as
+ // the vector body, one cell at a time.
+ while (idx < to) : (idx += 1) {
+ cells[idx] = @bitCast(template_bits | (@as(u64, cps[idx]) << cp_shift));
+ }
+}
+
/// The row-filling portion of the printSlice fast path, specialized by
/// width class. The first codepoint must already be validated by the
/// caller (printSliceFast).
@@ -581,19 +869,20 @@ fn printSliceFill(
// Anything else (including eligible unicode) proceeds via
// the scalar loop below.
if (comptime width == .narrow) {
- const lanes = 8;
- const V = @Vector(lanes, u32);
- const lo: V = @splat(0x10);
- const hi: V = @splat(0xFF);
- while (idx + lanes <= cps.len) {
- const v: V = cps[idx..][0..lanes].*;
- const in_range = (v >= lo) & (v <= hi);
- if (!@reduce(.And, in_range)) {
- const bits: std.meta.Int(.unsigned, lanes) = @bitCast(in_range);
- idx += @ctz(~bits);
- break;
+ if (simd.lanes(u32)) |lanes| {
+ const V = @Vector(lanes, u32);
+ const lo: V = @splat(0x10);
+ const hi: V = @splat(0xFF);
+ while (idx + lanes <= cps.len) {
+ const v: V = cps[idx..][0..lanes].*;
+ const in_range = (v >= lo) & (v <= hi);
+ if (!@reduce(.And, in_range)) {
+ const bits: std.meta.Int(.unsigned, lanes) = @bitCast(in_range);
+ idx += @ctz(~bits);
+ break;
+ }
+ idx += lanes;
}
- idx += lanes;
}
}
@@ -650,7 +939,7 @@ fn printSliceFill(
const style_id = cursor.style_id;
const template: Cell = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = 0 },
+ .content = .{ .codepoint = .{ .data = 0 } },
.style_id = style_id,
.wide = .narrow,
.protected = cursor.protected,
@@ -727,12 +1016,38 @@ fn printSliceFill(
// We can only write whole (wide, spacer) pairs.
const pair_end = k + (simple - k) / 2 * 2;
var idx = k;
+
+ // Manually vectorized for the same reason as
+ // printSliceStoreRun: build a vector of wide cells,
+ // interleave with spacer tails, and store (wide,
+ // spacer) pairs several at a time.
+ if (simd.lanes(u64)) |lanes| {
+ const pair_lanes = lanes / 2;
+ const Vp = @Vector(pair_lanes, u64);
+ const wide_v: Vp = @splat(wide_bits);
+ const spacer_v: Vp = @splat(spacer_bits);
+ const shift_v: @Vector(
+ pair_lanes,
+ std.math.Log2Int(u64),
+ ) = @splat(cp_shift);
+ while (idx + 2 * pair_lanes <= pair_end) : (idx += 2 * pair_lanes) {
+ const narrow: @Vector(pair_lanes, u32) =
+ cps[printed + idx / 2 ..][0..pair_lanes].*;
+ const wides: Vp = wide_v | (@as(Vp, @intCast(narrow)) << shift_v);
+ const inter: [2 * pair_lanes]u64 = std.simd.interlace(.{
+ wides,
+ spacer_v,
+ });
+ cells[idx..][0 .. 2 * pair_lanes].* = @bitCast(inter);
+ }
+ }
while (idx < pair_end) : (idx += 2) {
cells[idx] = @bitCast(
wide_bits | (@as(u64, cps[printed + idx / 2]) << cp_shift),
);
cells[idx + 1] = @bitCast(spacer_bits);
}
+
// If the simple run ended mid-pair we stop at the pair
// boundary and handle the offending cell below.
k = pair_end;
@@ -742,11 +1057,13 @@ fn printSliceFill(
simple = pair_end;
}
} else {
- for (k..simple) |idx| {
- cells[idx] = @bitCast(
- template_bits | (@as(u64, cps[printed + idx]) << cp_shift),
- );
- }
+ printSliceStoreRun(
+ cells,
+ cps.ptr + printed,
+ k,
+ simple,
+ template_bits,
+ );
k = simple;
}
if (k >= cell_count) break;
@@ -798,11 +1115,13 @@ fn printSliceFill(
page.styles.useMultiple(page.memory, style_id, @intCast(n));
}
- for (k..m) |idx| {
- cells[idx] = @bitCast(
- template_bits | (@as(u64, cps[printed + idx]) << cp_shift),
- );
- }
+ printSliceStoreRun(
+ cells,
+ cps.ptr + printed,
+ k,
+ m,
+ template_bits,
+ );
k = m;
continue :fill;
}
@@ -945,7 +1264,7 @@ pub fn print(self: *Terminal, c: u21) !void {
// necessarily a grapheme break.
if (prev.cell.codepoint() == 0) break :grapheme;
- var previous_codepoint: u21 = prev.cell.content.codepoint;
+ var previous_codepoint: u21 = prev.cell.content.codepoint.data;
const grapheme_break = brk: {
var state: uucode.grapheme.BreakState = .default;
if (prev.cell.hasGrapheme()) {
@@ -992,13 +1311,13 @@ pub fn print(self: *Terminal, c: u21) !void {
const row_wrap = right_limit == self.cols;
if (row_wrap) self.screens.active.cursor.page_row.wrap = true;
- const prev_cp = prev.cell.content.codepoint;
+ const prev_cp = prev.cell.content.codepoint.data;
if (prev.cell.hasGrapheme()) {
// This is like printCell but without clearing the
// grapheme data from the cell, so we can move it
// later.
prev.cell.wide = if (row_wrap) .spacer_head else .narrow;
- prev.cell.content.codepoint = 0;
+ prev.cell.content.codepoint.data = 0;
try self.printWrap();
self.printCell(prev_cp, .wide);
@@ -1012,14 +1331,21 @@ pub fn print(self: *Terminal, c: u21) !void {
const old_rac = old_pin.rowAndCell();
if (new_pin.node == old_pin.node) {
- new_pin.node.page().moveGrapheme(prev.cell, new_rac.cell);
- prev.cell.content_tag = .codepoint;
+ new_pin.node.page().moveGrapheme(old_rac.cell, new_rac.cell);
+ old_rac.cell.content_tag = .codepoint;
new_rac.cell.content_tag = .codepoint_grapheme;
new_rac.row.grapheme = true;
} else {
const cps = old_pin.node.page().lookupGrapheme(old_rac.cell).?;
for (cps) |cp| {
- try self.screens.active.appendGrapheme(new_rac.cell, cp);
+ // appendGrapheme can grow the cursor
+ // page, so read the destination from
+ // the cursor each time rather than
+ // holding a pointer across the call.
+ try self.screens.active.appendGrapheme(
+ self.screens.active.cursor.page_cell,
+ cp,
+ );
}
old_pin.node.page().clearGrapheme(old_rac.cell);
}
@@ -1029,7 +1355,7 @@ pub fn print(self: *Terminal, c: u21) !void {
// Point prev.cell to our new previous cell that
// we'll be appending graphemes to
- prev.cell = new_rac.cell;
+ prev.cell = self.screens.active.cursor.page_cell;
} else {
self.printCell(
0,
@@ -1048,8 +1374,30 @@ pub fn print(self: *Terminal, c: u21) !void {
// Write our spacer, since prev.cell is now wide
self.screens.active.cursorRight(1);
+
+ // Writing the spacer can grow the page to make room for
+ // the cursor hyperlink. Growing replaces the page, which
+ // invalidates `prev.cell`. Record the page identity first
+ // so the common case where nothing grows stays free.
+ //
+ // A pointer comparison alone isn't enough: pages are
+ // pooled, so a replacement can reuse the same address.
+ // The serial makes the pair a unique identity.
+ const spacer_node = self.screens.active.cursor.page_pin.node;
+ const spacer_serial = spacer_node.serial;
+
self.printCell(0, .spacer_tail);
+ if (self.screens.active.cursor.page_pin.node != spacer_node or
+ self.screens.active.cursor.page_pin.node.serial != spacer_serial)
+ {
+ @branchHint(.unlikely);
+
+ // The cursor is on the spacer tail we just wrote, so
+ // the wide cell we append to is the one to its left.
+ prev.cell = self.screens.active.cursorCellLeft(1);
+ }
+
// Move the cursor again so we're beyond our spacer
if (self.screens.active.cursor.x == right_limit - 1) {
self.screens.active.cursor.pending_wrap = true;
@@ -1063,28 +1411,24 @@ pub fn print(self: *Terminal, c: u21) !void {
if (prev.cell.wide != .wide) break :narrow;
prev.cell.wide = .narrow;
- // Remove the wide spacer tail
- const cell = self.screens.active.cursorCellLeft(prev.left - 1);
- cell.wide = .narrow;
-
- // Back track the cursor so that we don't end up with
- // an extra space after the character. Since xterm is
- // not VS aware, it cannot be used as a reference for
- // this behavior; but it does follow the principle of
- // least surprise, and also matches the behavior that
- // can be observed in Kitty, which is one of the only
- // other VS aware terminals.
- if (self.screens.active.cursor.x == right_limit - 1) {
- // If we're already at the right edge, we stay
- // here and set the pending wrap to false since
- // when we pend a wrap, we only move our cursor once
- // even for wide chars (tests verify).
- self.screens.active.cursor.pending_wrap = false;
- } else {
- // Otherwise, move back.
- self.screens.active.cursorLeft(1);
+ // Remove the wide spacer tail. The previous cell may be
+ // under the cursor, so locate the tail from the wide base
+ // rather than by subtracting from the cursor distance.
+ const prev_x = self.screens.active.cursor.x - prev.left;
+ if (prev_x < self.cols - 1) {
+ const cells: [*]Cell = @ptrCast(prev.cell);
+ cells[1].wide = .narrow;
}
+ // Place the cursor one cell after the now-narrow base,
+ // clamped to the right edge. Usually this moves the cursor
+ // back from after the old tail, but saved cursor state or
+ // changed margins can leave it directly on the base.
+ self.screens.active.cursor.pending_wrap = false;
+ self.screens.active.cursorHorizontalAbsolute(
+ @min(prev_x + 1, right_limit - 1),
+ );
+
break :narrow;
},
@@ -1151,7 +1495,7 @@ pub fn print(self: *Terminal, c: u21) !void {
// If this is a emoji variation selector, prev must be an emoji
if (c == 0xFE0F or c == 0xFE0E) {
- const prev_props = unicode.table.get(prev.content.codepoint);
+ const prev_props = unicode.table.get(prev.content.codepoint.data);
const emoji = prev_props.grapheme_break == .extended_pictographic;
if (!emoji) return;
}
@@ -1311,7 +1655,7 @@ fn printCell(
// So integrity checks pass. We fix this up later so we don't
// need to do this without safety checks.
- if (comptime std.debug.runtime_safety) {
+ if (comptime build_options.slow_runtime_safety) {
cell.wide = .narrow;
}
@@ -1364,7 +1708,7 @@ fn printCell(
// Write
cell.* = .{
.content_tag = .codepoint,
- .content = .{ .codepoint = c },
+ .content = .{ .codepoint = .{ .data = c } },
.style_id = self.screens.active.cursor.style_id,
.wide = wide,
.protected = self.screens.active.cursor.protected,
@@ -1397,7 +1741,11 @@ fn printCell(
self.screens.active.cursorSetHyperlink() catch |err| {
@branchHint(.unlikely);
log.warn("error reallocating for more hyperlink space, ignoring hyperlink err={}", .{err});
- assert(!cell.hyperlink);
+
+ // A partially successful grow can replace the page even when the
+ // call fails, so `cell` may be stale here. The cursor pointers are
+ // always reloaded, so read the cell through the cursor.
+ assert(!self.screens.active.cursor.page_cell.hyperlink);
};
} else if (had_hyperlink) {
// If the previous cell had a hyperlink then we need to clear it.
@@ -2040,11 +2388,25 @@ pub fn index(self: *Terminal) !void {
(!screen.no_scrollback or
self.scrolling_region.bottom == 0))
{
+ // If a bottom margin is set, kitty image placements may
+ // need adjusting around the scroll. The rare placements-
+ // present case is handled out of line so this hot path
+ // only pays a count check (a load from a cache line we
+ // already write, above).
+ if (comptime build_options.kitty_graphics) {
+ if (screen.kitty_images.placements.count() != 0) {
+ @branchHint(.unlikely);
+ try self.indexScrollWithImages(.window_shift);
+ return;
+ }
+ }
+
try screen.cursorScrollAbove();
return;
}
// Slow path for left and right scrolling region margins.
+ // scrollUp handles the kitty image adjustment itself.
if (self.scrolling_region.left != 0 or
self.scrolling_region.right != self.cols - 1)
{
@@ -2052,6 +2414,16 @@ pub fn index(self: *Terminal) !void {
return;
}
+ // Kitty image placements may need adjusting around the scroll;
+ // handled out of line like the scrollback path above.
+ if (comptime build_options.kitty_graphics) {
+ if (screen.kitty_images.placements.count() != 0) {
+ @branchHint(.unlikely);
+ try self.indexScrollWithImages(.in_place);
+ return;
+ }
+ }
+
// Otherwise use a fast path function to efficiently scroll
// the contents of the scrolling region.
try screen.cursorScrollRegionUp(
@@ -2067,6 +2439,69 @@ pub fn index(self: *Terminal) !void {
}
}
+/// The operation when we have Kitty image placements during index.
+/// Split out of index() so its hot paths don't carry the adjustment
+/// state in their stack frame, which measurably slows the scroll
+/// hot path.
+fn indexScrollWithImages(
+ self: *Terminal,
+ comptime op: kitty.graphics.ImageStorage.ScrollOp,
+) !void {
+ var kitty_scroll = self.kittyScrollMarginsBegin(-1, op);
+ defer if (kitty_scroll) |*state| state.end();
+ switch (op) {
+ .window_shift => try self.screens.active.cursorScrollAbove(),
+ .in_place => try self.screens.active.cursorScrollRegionUp(
+ self.scrolling_region.bottom - self.scrolling_region.top,
+ ),
+ }
+}
+
+// Handle when Kitty graphics is disabled.
+const KittyScrollMargins = if (build_options.kitty_graphics)
+ kitty.graphics.ImageStorage.ScrollMargins
+else
+ struct {
+ pub inline fn end(self: *@This()) void {
+ _ = self;
+ }
+ };
+
+/// Begin adjusting kitty image placements for a scroll of the
+/// scrolling region by delta rows (negative moves content up). If
+/// adjustment is needed this returns state whose end() must be called
+/// after the scroll's row operations complete (see
+/// ImageStorage.scrollMarginsBegin for why this is two phases). This
+/// returns null when the scrolling region is the full screen, because
+/// placements then follow their anchored rows via pin tracking which
+/// matches kitty's marginless behavior.
+///
+/// Callers must comptime-gate on build_options.kitty_graphics and
+/// check that placements exist before calling, which keeps the cost
+/// on the hot scroll paths cheap.
+fn kittyScrollMarginsBegin(
+ self: *Terminal,
+ delta: isize,
+ op: kitty.graphics.ImageStorage.ScrollOp,
+) ?kitty.graphics.ImageStorage.ScrollMargins {
+ @branchHint(.cold);
+
+ // Full-screen scrolls need no adjustment: placements follow their
+ // anchored rows (possibly into the scrollback) via pin tracking.
+ if (self.scrolling_region.top == 0 and
+ self.scrolling_region.bottom == self.rows - 1 and
+ self.scrolling_region.left == 0 and
+ self.scrolling_region.right == self.cols - 1) return null;
+
+ const screen: *Screen = self.screens.active;
+ return screen.kitty_images.scrollMarginsBegin(
+ self.io(),
+ self,
+ delta,
+ op,
+ );
+}
+
/// Move the cursor to the previous line in the scrolling region, possibly
/// scrolling.
///
@@ -2192,6 +2627,24 @@ pub fn scrollDown(self: *Terminal, count: usize) void {
self.screens.active.cursor.pending_wrap = old_wrap;
}
+ // If margins are set and kitty image placements exist, they need
+ // adjusting around the scroll. Note this wraps scrollDown and NOT
+ // insertLines: kitty scrolls images for SD/RI but leaves them
+ // alone for IL/DL.
+ var kitty_scroll: ?KittyScrollMargins = null;
+ defer if (kitty_scroll) |*state| state.end();
+ if (comptime build_options.kitty_graphics) {
+ if (self.screens.active.kitty_images.placements.count() != 0) {
+ @branchHint(.unlikely);
+ const region_height: usize =
+ @as(usize, self.scrolling_region.bottom - self.scrolling_region.top) + 1;
+ kitty_scroll = self.kittyScrollMarginsBegin(
+ @intCast(@min(count, region_height)),
+ .in_place,
+ );
+ }
+ }
+
// Move to the top of the scroll region
self.screens.active.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top);
self.insertLines(count);
@@ -2214,6 +2667,35 @@ pub fn scrollUp(self: *Terminal, count: usize) !void {
self.screens.active.cursor.pending_wrap = old_wrap;
}
+ // If margins are set and kitty image placements exist, they need
+ // adjusting around the scroll. Note this wraps scrollUp and NOT
+ // deleteLines: kitty scrolls images for SU/IND but leaves them
+ // alone for IL/DL.
+ var kitty_scroll: ?KittyScrollMargins = null;
+ defer if (kitty_scroll) |*state| state.end();
+ if (comptime build_options.kitty_graphics) {
+ if (self.screens.active.kitty_images.placements.count() != 0) {
+ @branchHint(.unlikely);
+
+ // The op must mirror the branch below: the scrollback path
+ // shifts the active window while the deleteLines path
+ // moves rows in place.
+ const region_height: usize =
+ @as(usize, self.scrolling_region.bottom - self.scrolling_region.top) + 1;
+ kitty_scroll = self.kittyScrollMarginsBegin(
+ -@as(isize, @intCast(@min(count, region_height))),
+ if (self.scrolling_region.top == 0 and
+ self.scrolling_region.left == 0 and
+ self.scrolling_region.right == self.cols - 1 and
+ (!self.screens.active.no_scrollback or
+ self.scrolling_region.bottom == self.rows - 1))
+ .window_shift
+ else
+ .in_place,
+ );
+ }
+ }
+
// If our scroll region is at the top and we have no left/right
// margins then we move the scrolled out text into the scrollback.
//
@@ -2283,7 +2765,7 @@ pub const ScrollViewport = union(Tag) {
@This(),
// Padding: largest variant is isize (8 bytes on 64-bit).
// Use [2]u64 (16 bytes) for future expansion.
- [2]u64,
+ .{ .padding = [2]u64 },
);
pub const C = c_union.C;
pub const CValue = c_union.CValue;
@@ -2412,7 +2894,7 @@ fn rowWillBeShifted(
page.clearGrapheme(wide_cell);
page.updateRowGraphemeFlag(row);
}
- wide_cell.content.codepoint = 0;
+ wide_cell.content.codepoint = .{ .data = 0 };
wide_cell.wide = .narrow;
left_cell.wide = .narrow;
}
@@ -2423,7 +2905,7 @@ fn rowWillBeShifted(
page.clearGrapheme(right_cell);
page.updateRowGraphemeFlag(row);
}
- right_cell.content.codepoint = 0;
+ right_cell.content.codepoint.data = 0;
right_cell.wide = .narrow;
tail_cell.wide = .narrow;
}
@@ -2558,60 +3040,19 @@ pub fn insertLines(self: *Terminal, count: usize) void {
// If our page doesn't match, then we need to do a copy from
// one page to another. This is the slow path.
if (src_p.node != dst_p.node) {
- dst_p.node.page().clonePartialRowFrom(
+ // The copy may replace the destination node in order
+ // to increase its capacity. Our pins are tracked so
+ // they update automatically; we can discard the
+ // replacement because the remainder of this iteration
+ // only accesses rows through the pins.
+ _ = self.screens.active.clonePartialRowGrowCapacity(
+ dst_p.node,
+ dst_p.y,
src_p.node.page(),
- dst_row,
src_row,
self.scrolling_region.left,
self.scrolling_region.right + 1,
- ) catch |err| {
- // Adjust our page capacity to make
- // room for we didn't have space for
- _ = self.screens.active.increaseCapacity(
- dst_p.node,
- switch (err) {
- // Rehash the sets
- error.StyleSetNeedsRehash,
- error.HyperlinkSetNeedsRehash,
- => null,
-
- // Increase style memory
- error.StyleSetOutOfMemory,
- => .styles,
-
- // Increase string memory
- error.StringAllocOutOfMemory,
- => .string_bytes,
-
- // Increase hyperlink memory
- error.HyperlinkSetOutOfMemory,
- error.HyperlinkMapOutOfMemory,
- => .hyperlink_bytes,
-
- // Increase grapheme memory
- error.GraphemeMapOutOfMemory,
- error.GraphemeAllocOutOfMemory,
- => .grapheme_bytes,
- },
- ) catch |e| switch (e) {
- // System OOM. We have no way to recover from this
- // currently. We should probably change insertLines
- // to raise an error here.
- error.OutOfMemory,
- => @panic("increaseCapacity system allocator OOM"),
-
- // The page can't accommodate the managed memory required
- // for this operation. We previously just corrupted
- // memory here so a crash is better. The right long
- // term solution is to allocate a new page here
- // move this row to the new page, and start over.
- error.OutOfSpace,
- => @panic("increaseCapacity OutOfSpace"),
- };
-
- // Continue the loop to try handling this row again.
- continue;
- };
+ );
} else {
if (!left_right) {
// Swap the src/dst cells. This ensures that our dst gets the
@@ -2646,6 +3087,14 @@ pub fn insertLines(self: *Terminal, count: usize) void {
cur_row,
cells[self.scrolling_region.left .. self.scrolling_region.right + 1],
);
+
+ // With a full-width scroll region the entire row is a
+ // fresh blank row: reset the metadata so nothing (wrap
+ // state, semantic prompt) is retained from the row whose
+ // storage it recycles. With left/right margins the row
+ // keeps content outside the margins so the metadata is
+ // preserved, matching the shift case above.
+ if (!left_right) cur_row.reset();
}
// Mark the row as dirty
@@ -2759,53 +3208,19 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
// If our page doesn't match, then we need to do a copy from
// one page to another. This is the slow path.
if (src_p.node != dst_p.node) {
- dst_p.node.page().clonePartialRowFrom(
+ // The copy may replace the destination node in order
+ // to increase its capacity. Our pins are tracked so
+ // they update automatically; we can discard the
+ // replacement because the remainder of this iteration
+ // only accesses rows through the pins.
+ _ = self.screens.active.clonePartialRowGrowCapacity(
+ dst_p.node,
+ dst_p.y,
src_p.node.page(),
- dst_row,
src_row,
self.scrolling_region.left,
self.scrolling_region.right + 1,
- ) catch |err| {
- // Adjust our page capacity to make
- // room for we didn't have space for
- _ = self.screens.active.increaseCapacity(
- dst_p.node,
- switch (err) {
- // Rehash the sets
- error.StyleSetNeedsRehash,
- error.HyperlinkSetNeedsRehash,
- => null,
-
- // Increase style memory
- error.StyleSetOutOfMemory,
- => .styles,
-
- // Increase string memory
- error.StringAllocOutOfMemory,
- => .string_bytes,
-
- // Increase hyperlink memory
- error.HyperlinkSetOutOfMemory,
- error.HyperlinkMapOutOfMemory,
- => .hyperlink_bytes,
-
- // Increase grapheme memory
- error.GraphemeMapOutOfMemory,
- error.GraphemeAllocOutOfMemory,
- => .grapheme_bytes,
- },
- ) catch |e| switch (e) {
- // See insertLines
- error.OutOfMemory,
- => @panic("increaseCapacity system allocator OOM"),
-
- error.OutOfSpace,
- => @panic("increaseCapacity OutOfSpace"),
- };
-
- // Continue the loop to try handling this row again.
- continue;
- };
+ );
} else {
if (!left_right) {
// Swap the src/dst cells. This ensures that our dst gets the
@@ -2840,6 +3255,14 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
cur_row,
cells[self.scrolling_region.left .. self.scrolling_region.right + 1],
);
+
+ // With a full-width scroll region the entire row is a
+ // fresh blank row: reset the metadata so nothing (wrap
+ // state, semantic prompt) is retained from the row whose
+ // storage it recycles. With left/right margins the row
+ // keeps content outside the margins so the metadata is
+ // preserved, matching the shift case above.
+ if (!left_right) cur_row.reset();
}
// Mark the row as dirty
@@ -3095,9 +3518,14 @@ pub fn eraseLine(
break :left .{ 0, x + 1 };
},
- // Note that it seems like complete should reset the soft-wrap
- // state of the line but in xterm it does not.
- .complete => .{ 0, self.cols },
+ .complete => complete: {
+ // Xterm preserves this flag for EL2, but it also doesn't reflow
+ // rows when resizing. Since we do, the erased row must no longer
+ // continue onto the next row.
+ self.screens.active.cursorResetWrap();
+
+ break :complete .{ 0, self.cols };
+ },
else => {
log.err("unimplemented erase line mode: {}", .{mode});
@@ -3164,11 +3592,12 @@ pub fn eraseDisplay(
self.screens.active.cursor.pending_wrap = false;
if (comptime build_options.kitty_graphics) {
- // Clear all Kitty graphics state for this screen
- self.screens.active.kitty_images.delete(
+ // Clear only placements still visible after moving the active
+ // area into scrollback.
+ self.screens.active.kitty_images.clearScreen(
+ self.io(),
self.screens.active.alloc,
self,
- .{ .all = true },
);
}
},
@@ -3221,11 +3650,12 @@ pub fn eraseDisplay(
self.screens.active.cursor.pending_wrap = false;
if (comptime build_options.kitty_graphics) {
- // Clear all Kitty graphics state for this screen
- self.screens.active.kitty_images.delete(
+ // ED2 clears visible placements but preserves graphics that
+ // are wholly in scrollback.
+ self.screens.active.kitty_images.clearScreen(
+ self.io(),
self.screens.active.alloc,
self,
- .{ .all = true },
);
}
@@ -3315,7 +3745,7 @@ pub fn decaln(self: *Terminal) !void {
const cells = cells_multi[0..page.size.cols];
@memset(cells, .{
.content_tag = .codepoint,
- .content = .{ .codepoint = 'E' },
+ .content = .{ .codepoint = .{ .data = 'E' } },
.style_id = self.screens.active.cursor.style_id,
// DECALN does not respect protected state. Verified with xterm.
@@ -3353,10 +3783,11 @@ pub fn decaln(self: *Terminal) !void {
/// undefined.
pub fn kittyGraphics(
self: *Terminal,
+ io_impl: std.Io,
alloc: Allocator,
cmd: *kitty.graphics.Command,
) ?kitty.graphics.Response {
- return kitty.graphics.execute(alloc, self, cmd);
+ return kitty.graphics.execute(io_impl, alloc, self, cmd);
}
/// Execute a Glyph Protocol APC command against this terminal's per-session
@@ -3380,12 +3811,12 @@ pub fn setKittyGraphicsSizeLimit(
self: *Terminal,
alloc: Allocator,
limit: usize,
-) !void {
+) void {
if (comptime !build_options.kitty_graphics) return;
var it = self.screens.all.iterator();
while (it.next()) |entry| {
const screen: *Screen = entry.value.*;
- try screen.kitty_images.setLimit(alloc, screen, limit);
+ screen.kitty_images.setLimit(self.io(), alloc, screen, limit);
}
}
@@ -3414,58 +3845,69 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
/// Boolean attributes are printed first, followed by foreground color, then
/// background color. Each attribute is separated by a semicolon.
pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 {
- var stream = std.io.fixedBufferStream(buf);
- const writer = stream.writer();
+ var writer: std.Io.Writer = .fixed(buf);
// The SGR response always starts with a 0. See https://vt100.net/docs/vt510-rm/DECRPSS
try writer.writeByte('0');
const pen = self.screens.active.cursor.style;
- var attrs: [8]u8 = @splat(0);
+ var attrs: [9]u8 = @splat(0);
var i: usize = 0;
if (pen.flags.bold) {
- attrs[i] = '1';
+ attrs[i] = 1;
i += 1;
}
if (pen.flags.faint) {
- attrs[i] = '2';
+ attrs[i] = 2;
i += 1;
}
if (pen.flags.italic) {
- attrs[i] = '3';
+ attrs[i] = 3;
i += 1;
}
if (pen.flags.underline != .none) {
- attrs[i] = '4';
+ attrs[i] = 4;
+ i += 1;
+ }
+
+ if (pen.flags.overline) {
+ attrs[i] = 53;
i += 1;
}
if (pen.flags.blink) {
- attrs[i] = '5';
+ attrs[i] = 5;
i += 1;
}
if (pen.flags.inverse) {
- attrs[i] = '7';
+ attrs[i] = 7;
i += 1;
}
if (pen.flags.invisible) {
- attrs[i] = '8';
+ attrs[i] = 8;
i += 1;
}
if (pen.flags.strikethrough) {
- attrs[i] = '9';
+ attrs[i] = 9;
i += 1;
}
- for (attrs[0..i]) |c| {
- try writer.print(";{c}", .{c});
+ for (attrs[0..i]) |attr| {
+ // Preserve underline styles. Kind of a hack to special case 4
+ // here but its easier than changing how we do all attributes.
+ if (attr == 4 and pen.flags.underline != .single) {
+ try writer.print(";4:{}", .{@intFromEnum(pen.flags.underline)});
+ continue;
+ }
+
+ try writer.print(";{}", .{attr});
}
switch (pen.fg_color) {
@@ -3490,7 +3932,7 @@ pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 {
.rgb => |rgb| try writer.print(";48:2::{[r]}:{[g]}:{[b]}", rgb),
}
- return stream.getWritten();
+ return writer.buffered();
}
/// The modes for DECCOLM.
@@ -3519,86 +3961,666 @@ pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void {
self.modes.set(.@"132_column", mode == .@"132_cols");
// Resize to the requested size
- try self.resize(
- alloc,
- switch (mode) {
+ try self.resize(alloc, .{
+ .cols = switch (mode) {
.@"132_cols" => 132,
.@"80_cols" => 80,
},
- self.rows,
- );
+ .rows = self.rows,
+ });
// Erase our display and move our cursor.
self.eraseDisplay(.complete, false);
self.setCursorPos(1, 1);
}
+/// A terminal resize expressed in cells with optional per-cell pixel
+/// geometry. It is highly recommended that callers supply cell geometry
+/// but a terminal can technically function without it (but some reports
+/// like certain mouse reporting modes and Kitty image protocol will
+/// not be functional).
+///
+/// Cell pixel dimensions must already be scaled for the current display DPI.
+pub const Resize = struct {
+ cols: size.CellCountInt,
+ rows: size.CellCountInt,
+ cell_size_px: ?struct {
+ width: u32,
+ height: u32,
+ } = null,
+};
+
+pub const ResizeError = error{
+ /// Resize requires allocation
+ OutOfMemory,
+
+ /// Input value was invalid, such as a 0-sized dimension.
+ InvalidValue,
+};
+
+const resize_tw = tripwire.module(enum {
+ tabstops,
+ primary_screen,
+ alternate_screen,
+ alternate_screen_init,
+}, resize);
+
/// Resize the underlying terminal.
+///
+/// This has follow-on impacts:
+///
+/// - If the column count changes, tabstops are reset.
+/// - The scroll region is always reset
+/// - Synchronized output mode is reset for every successful resize.
+///
+/// This handles errors gracefully and recovers the terminal back to
+/// a clean usable state.
+///
+/// The only error handling edge case is in the highly exceptional scenario
+/// where the primary screen can be resized but the alternate screen cannot.
+/// In this scenario, we attempt to clear the alt screen at the desired
+/// new size. If that fails, we unconditionally deallocate the alt screen
+/// and move to primary screen. This can break terminal programs but it
+/// requires a really particular scenario where memory exists for one
+/// but not the other and we do our best.
pub fn resize(
self: *Terminal,
alloc: Allocator,
- cols: size.CellCountInt,
- rows: size.CellCountInt,
-) !void {
- // If our cols/rows didn't change then we're done
- if (self.cols == cols and self.rows == rows) return;
-
- // Resize our tabstops
- if (self.cols != cols) {
- const tabstops: Tabstops = try .init(
+ opts: Resize,
+) ResizeError!void {
+ const tw = resize_tw;
+
+ // Screen and scrolling-region invariants require non-zero dimensions.
+ // Validate before changing any terminal state.
+ if (opts.cols == 0 or opts.rows == 0) return error.InvalidValue;
+
+ // Pixel geometry and synchronized output are updated on every valid
+ // resize attempt, including one that doesn't change the grid dimensions.
+ // Save their old values so later allocation failures can roll them back.
+ const old_width_px = self.width_px;
+ const old_height_px = self.height_px;
+ const old_synchronized_output = self.modes.get(.synchronized_output);
+ errdefer {
+ self.width_px = old_width_px;
+ self.height_px = old_height_px;
+ self.modes.set(.synchronized_output, old_synchronized_output);
+ }
+
+ // If our pixel geometry was set, then we set it even if our rows/cols
+ // didn't change.
+ if (opts.cell_size_px) |cell_size| {
+ self.width_px = std.math.mul(
+ u32,
+ opts.cols,
+ cell_size.width,
+ ) catch std.math.maxInt(u32);
+ self.height_px = std.math.mul(
+ u32,
+ opts.rows,
+ cell_size.height,
+ ) catch std.math.maxInt(u32);
+ }
+
+ self.modes.set(.synchronized_output, false);
+
+ // If our cols/rows didn't change, skip grid work but still apply pixels.
+ if (self.cols == opts.cols and self.rows == opts.rows) return;
+
+ // Build replacement tabstops without touching the current table. Keep
+ // ownership here until every fallible resize operation has succeeded.
+ var new_tabstops: ?Tabstops = null;
+ errdefer if (new_tabstops) |*v| v.deinit(alloc);
+ if (self.cols != opts.cols) {
+ try tw.check(.tabstops);
+ new_tabstops = try .init(
alloc,
- cols,
+ opts.cols,
TABSTOP_INTERVAL,
);
- self.tabstops.deinit(alloc);
- self.tabstops = tabstops;
}
- // Resize primary screen, which supports reflow
+ // Resize primary screen, which supports reflow. We do this first
+ // because the cleanup situation is a lot better if this succeeds
+ // and alt fails than the reverse.
+ try tw.check(.primary_screen);
const primary = self.screens.get(.primary).?;
try primary.resize(.{
- .cols = cols,
- .rows = rows,
+ .cols = opts.cols,
+ .rows = opts.rows,
.reflow = self.modes.get(.wraparound),
.prompt_redraw = self.flags.shell_redraws_prompt,
});
- // Alternate screen, if it exists, doesn't reflow
- if (self.screens.get(.alternate)) |alt| try alt.resize(.{
- .cols = cols,
- .rows = rows,
- .reflow = false,
- });
+ // Alternate screen, if it exists, doesn't reflow. The primary resize
+ // above can't be losslessly undone, so if the alternate resize fails we
+ // replace it with an empty screen at the requested size. If that
+ // also fails, we fall back to the primary screen.
+ if (self.screens.get(.alternate)) |alt| alt: {
+ const err: ResizeError = resize: {
+ tw.check(.alternate_screen) catch |err| break :resize err;
+ alt.resize(.{
+ .cols = opts.cols,
+ .rows = opts.rows,
+ .reflow = false,
+ }) catch |err| break :resize err;
+
+ // Resize succeeded.
+ break :alt;
+ };
+
+ log.warn("alternate screen resize failed, replacing it err={}", .{err});
+
+ // If the alternate screen isn't active, then we just free it
+ // and move on. It'll be reallocated when it gets reinitialized lazily.
+ // In this case, we just lose the prior data if the terminal program
+ // expected it to be saved.
+ if (self.screens.active_key != .alternate) {
+ self.screens.remove(alloc, .alternate);
+ break :alt;
+ }
+
+ // The alt screen is active, so we temporarily switch to primary
+ // so we can safely remove the alt and recreate it blank. This loses
+ // the data but hopefully keeps us on the alt screen.
+ const charset = alt.charset;
+ self.screens.switchTo(.primary);
+ self.screens.remove(alloc, .alternate);
+
+ // Replace the alt screen with an empty version. If this fails
+ // we just go back to the primary screen. Not great, but best
+ // we can do.
+ tw.check(.alternate_screen_init) catch break :alt;
+ const replacement = self.screens.getInit(
+ self.io(),
+ alloc,
+ .alternate,
+ .{
+ .cols = opts.cols,
+ .rows = opts.rows,
+ .max_scrollback_bytes = 0,
+ .kitty_image_storage_limit = if (comptime build_options.kitty_graphics)
+ primary.kitty_images.total_limit
+ else
+ 0,
+ .kitty_image_loading_limits = if (comptime build_options.kitty_graphics)
+ primary.kitty_images.image_limits
+ else {},
+ },
+ ) catch |init_err| {
+ log.warn(
+ "alternate screen replacement failed, falling back to primary err={}",
+ .{init_err},
+ );
+ break :alt;
+ };
+
+ replacement.charset = charset;
+ self.screens.switchTo(.alternate);
+ }
+
+ // No more failures are allowed after this point because the screens have
+ // committed their new sizes and the remaining Terminal state must follow.
+ errdefer comptime unreachable;
+
+ // All fallible work is complete. Replace the old tabstop table only now.
+ if (new_tabstops) |v| {
+ self.tabstops.deinit(alloc);
+ self.tabstops = v;
+ new_tabstops = null;
+ }
// Whenever we resize we just mark it as a screen clear
self.flags.dirty.clear = true;
// Set our size
- self.cols = cols;
- self.rows = rows;
+ self.cols = opts.cols;
+ self.rows = opts.rows;
// Reset the scrolling region
self.scrolling_region = .{
.top = 0,
- .bottom = rows - 1,
+ .bottom = opts.rows - 1,
.left = 0,
- .right = cols - 1,
+ .right = opts.cols - 1,
};
}
+test "Terminal forwards optional scrollback limits" {
+ const max_lines: usize = 123;
+ var t = try init(testing.io, testing.allocator, .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback_bytes = null,
+ .max_scrollback_lines = max_lines,
+ });
+ defer t.deinit(testing.allocator);
+
+ try testing.expectEqual(
+ std.math.maxInt(usize),
+ t.screens.active.pages.limits.bytes.explicit,
+ );
+ try testing.expectEqual(
+ max_lines,
+ t.screens.active.pages.limits.lines.explicit,
+ );
+}
+
+test "Terminal setScrollbackMaxBytes" {
+ var t = try init(testing.io, testing.allocator, .{
+ .cols = 80,
+ .rows = 3,
+ .max_scrollback_bytes = null,
+ });
+ defer t.deinit(testing.allocator);
+
+ const primary = t.screens.get(.primary).?;
+ const page_rows: usize = primary.pages.pages.first.?.capacity().rows;
+
+ // Build several complete pages of history so lowering the byte limit has
+ // existing allocations to prune immediately.
+ for (0..4 * page_rows) |_| try t.linefeed();
+ const old_page_size = primary.pages.page_size;
+ t.setScrollbackMaxBytes(1);
+ try testing.expectEqual(@as(usize, 1), primary.pages.limits.bytes.explicit);
+ try testing.expect(primary.pages.page_size < old_page_size);
+ try testing.expect(
+ primary.pages.page_size <= primary.pages.limits.max(.bytes),
+ );
+ try testing.expect(!primary.no_scrollback);
+
+ // Zero switches Screen behavior as well as PageList accounting, discards
+ // all retained history, and prevents future linefeeds from recreating it.
+ t.setScrollbackMaxBytes(0);
+ try testing.expectEqual(@as(usize, 0), primary.pages.limits.bytes.explicit);
+ try testing.expect(primary.no_scrollback);
+ try testing.expectEqual(
+ @as(usize, primary.pages.rows),
+ primary.pages.total_rows,
+ );
+ try testing.expect(primary.pages.viewport == .active);
+
+ for (0..page_rows) |_| try t.linefeed();
+ try testing.expectEqual(
+ @as(usize, primary.pages.rows),
+ primary.pages.total_rows,
+ );
+
+ // Re-enabling unlimited scrollback affects subsequent output.
+ t.setScrollbackMaxBytes(null);
+ try testing.expectEqual(
+ std.math.maxInt(usize),
+ primary.pages.limits.bytes.explicit,
+ );
+ try testing.expect(!primary.no_scrollback);
+ for (0..page_rows) |_| try t.linefeed();
+ try testing.expect(primary.pages.total_rows > primary.pages.rows);
+}
+
+test "Terminal setScrollbackMaxLines" {
+ var t = try init(testing.io, testing.allocator, .{
+ .cols = 80,
+ .rows = 3,
+ .max_scrollback_bytes = null,
+ .max_scrollback_lines = null,
+ });
+ defer t.deinit(testing.allocator);
+
+ const primary = t.screens.get(.primary).?;
+ const page_rows: usize = primary.pages.pages.first.?.capacity().rows;
+
+ for (0..4 * page_rows) |_| try t.linefeed();
+ const old_total_rows = primary.pages.total_rows;
+ t.setScrollbackMaxLines(page_rows);
+ try testing.expectEqual(
+ page_rows,
+ primary.pages.limits.lines.explicit,
+ );
+ try testing.expect(primary.pages.total_rows < old_total_rows);
+ try testing.expect(
+ !primary.pages.limits.exceeded(&primary.pages, .lines),
+ );
+
+ const limited_total_rows = primary.pages.total_rows;
+ t.setScrollbackMaxLines(null);
+ try testing.expectEqual(
+ std.math.maxInt(usize),
+ primary.pages.limits.lines.explicit,
+ );
+ try testing.expectEqual(limited_total_rows, primary.pages.total_rows);
+ for (0..3 * page_rows) |_| try t.linefeed();
+ try testing.expect(
+ primary.pages.total_rows - primary.pages.rows > page_rows,
+ );
+}
+
+test "Terminal setScrollback only affects primary screen" {
+ var t = try init(testing.io, testing.allocator, .{
+ .cols = 80,
+ .rows = 3,
+ });
+ defer t.deinit(testing.allocator);
+
+ _ = try t.switchScreen(.alternate);
+ const primary = t.screens.get(.primary).?;
+ const alternate = t.screens.get(.alternate).?;
+
+ t.setScrollbackMaxBytes(123);
+ t.setScrollbackMaxLines(456);
+
+ try testing.expectEqual(
+ @as(usize, 123),
+ primary.pages.limits.bytes.explicit,
+ );
+ try testing.expectEqual(
+ @as(usize, 456),
+ primary.pages.limits.lines.explicit,
+ );
+ try testing.expect(!primary.no_scrollback);
+
+ try testing.expectEqual(
+ @as(usize, 0),
+ alternate.pages.limits.bytes.explicit,
+ );
+ try testing.expectEqual(
+ std.math.maxInt(usize),
+ alternate.pages.limits.lines.explicit,
+ );
+ try testing.expect(alternate.no_scrollback);
+ try testing.expectEqual(alternate, t.screens.active);
+}
+
+test "Terminal: resize resets synchronized output" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
+ defer t.deinit(alloc);
+
+ t.modes.set(.synchronized_output, true);
+ try t.resize(alloc, .{ .cols = 10, .rows = 5 });
+ try testing.expect(!t.modes.get(.synchronized_output));
+}
+
+test "Terminal: resize rejects zero dimensions before mutation" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
+ defer t.deinit(alloc);
+
+ t.width_px = 100;
+ t.height_px = 100;
+ t.flags.dirty.clear = false;
+
+ try testing.expectError(error.InvalidValue, t.resize(alloc, .{
+ .cols = 0,
+ .rows = 5,
+ .cell_size_px = .{ .width = 9, .height = 18 },
+ }));
+ try testing.expectError(error.InvalidValue, t.resize(alloc, .{
+ .cols = 10,
+ .rows = 0,
+ .cell_size_px = .{ .width = 9, .height = 18 },
+ }));
+
+ try testing.expectEqual(@as(size.CellCountInt, 10), t.cols);
+ try testing.expectEqual(@as(size.CellCountInt, 5), t.rows);
+ try testing.expectEqual(@as(u32, 100), t.width_px);
+ try testing.expectEqual(@as(u32, 100), t.height_px);
+ try testing.expect(!t.flags.dirty.clear);
+}
+
+test "Terminal: resize preserves pixel dimensions when omitted" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
+ defer t.deinit(alloc);
+
+ t.width_px = 90;
+ t.height_px = 90;
+ try t.resize(alloc, .{ .cols = 20, .rows = 10 });
+
+ try testing.expectEqual(@as(u32, 90), t.width_px);
+ try testing.expectEqual(@as(u32, 90), t.height_px);
+}
+
+test "Terminal: resize updates pixels without changing cell dimensions" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
+ defer t.deinit(alloc);
+
+ try t.resize(alloc, .{
+ .cols = 10,
+ .rows = 5,
+ .cell_size_px = .{ .width = 9, .height = 18 },
+ });
+
+ try testing.expectEqual(@as(u32, 90), t.width_px);
+ try testing.expectEqual(@as(u32, 90), t.height_px);
+}
+
+test "Terminal: resize pixel dimensions saturate" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 3 });
+ defer t.deinit(alloc);
+
+ try t.resize(alloc, .{
+ .cols = 2,
+ .rows = 3,
+ .cell_size_px = .{
+ .width = std.math.maxInt(u32),
+ .height = std.math.maxInt(u32),
+ },
+ });
+
+ try testing.expectEqual(std.math.maxInt(u32), t.width_px);
+ try testing.expectEqual(std.math.maxInt(u32), t.height_px);
+}
+
test "Terminal: resize preserves tabstops on allocation failure" {
var failing = testing.FailingAllocator.init(testing.allocator, .{});
const alloc = failing.allocator();
- var t = try init(alloc, .{ .cols = 10, .rows = 1 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 1 });
defer t.deinit(alloc);
failing.fail_index = failing.alloc_index;
- try testing.expectError(error.OutOfMemory, t.resize(alloc, 513, 1));
+ try testing.expectError(error.OutOfMemory, t.resize(alloc, .{
+ .cols = 513,
+ .rows = 1,
+ }));
try testing.expectEqual(@as(size.CellCountInt, 10), t.cols);
try testing.expect(t.tabstops.get(8));
}
+test "Terminal: resize failure paths preserve consistent state" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+
+ for ([_]resize_tw.FailPoint{
+ .tabstops,
+ .primary_screen,
+ .alternate_screen,
+ }) |tag| {
+ const tw = resize_tw;
+ defer tw.end(.reset) catch unreachable;
+
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
+ defer t.deinit(alloc);
+
+ try t.printString("primary");
+ _ = try t.switchScreen(.alternate);
+ try t.printString("alternate");
+ _ = try t.switchScreen(.primary);
+
+ t.width_px = 100;
+ t.height_px = 50;
+ t.modes.set(.synchronized_output, true);
+ t.flags.dirty.clear = false;
+ t.scrolling_region = .{
+ .top = 1,
+ .bottom = 2,
+ .left = 1,
+ .right = 8,
+ };
+ t.tabstops.unset(8);
+ t.tabstops.set(3);
+
+ const primary = t.screens.get(.primary).?;
+ const alternate = t.screens.get(.alternate).?;
+ const alternate_generation = t.screens.generation(.alternate);
+ try testing.expectEqual(primary.pages.pages.first, primary.pages.pages.last);
+ try testing.expectEqual(alternate.pages.pages.first, alternate.pages.pages.last);
+
+ const before = t;
+ const before_primary = primary.*;
+ const before_alternate = alternate.*;
+ const before_primary_page = try alloc.dupe(
+ u8,
+ primary.pages.pages.first.?.page().memory,
+ );
+ defer alloc.free(before_primary_page);
+ const before_alternate_page = try alloc.dupe(
+ u8,
+ alternate.pages.pages.first.?.page().memory,
+ );
+ defer alloc.free(before_alternate_page);
+ tw.errorAlways(tag, error.OutOfMemory);
+
+ // A failure after the primary screen has resized is recovered by
+ // dropping the inactive alternate and completing the resize. This
+ // also proves the alternate tripwire is after the primary resize
+ // rather than acting as a preflight check.
+ if (tag == .alternate_screen) {
+ try t.resize(alloc, .{
+ .cols = 513,
+ .rows = 4,
+ .cell_size_px = .{ .width = 9, .height = 18 },
+ });
+
+ try testing.expectEqual(@as(size.CellCountInt, 513), t.cols);
+ try testing.expectEqual(@as(size.CellCountInt, 4), t.rows);
+ try testing.expectEqual(@as(u32, 4617), t.width_px);
+ try testing.expectEqual(@as(u32, 72), t.height_px);
+ try testing.expect(!t.modes.get(.synchronized_output));
+ try testing.expect(t.flags.dirty.clear);
+ try testing.expectEqual(@as(size.CellCountInt, 513), primary.pages.cols);
+ try testing.expectEqual(@as(size.CellCountInt, 4), primary.pages.rows);
+ try testing.expectEqual(@as(?*Screen, null), t.screens.get(.alternate));
+ try testing.expectEqual(
+ alternate_generation +% 1,
+ t.screens.generation(.alternate),
+ );
+ try testing.expectEqual(.primary, t.screens.active_key);
+ try testing.expectEqual(primary, t.screens.active);
+ try testing.expect(t.tabstops.get(8));
+ try testing.expect(!t.tabstops.get(3));
+
+ // The alternate is recreated lazily at the terminal's new size.
+ _ = try t.switchScreen(.alternate);
+ const replacement = t.screens.get(.alternate).?;
+ try testing.expectEqual(@as(size.CellCountInt, 513), replacement.pages.cols);
+ try testing.expectEqual(@as(size.CellCountInt, 4), replacement.pages.rows);
+ try testing.expect(replacement.pages.getCell(.{ .active = .{} }).?.cell.isEmpty());
+ continue;
+ }
+
+ try testing.expectError(error.OutOfMemory, t.resize(alloc, .{
+ .cols = 513,
+ .rows = 4,
+ .cell_size_px = .{ .width = 9, .height = 18 },
+ }));
+
+ try testing.expectEqual(before.width_px, t.width_px);
+ try testing.expectEqual(before.height_px, t.height_px);
+ try testing.expect(std.meta.eql(before.modes, t.modes));
+ try testing.expectEqual(before.cols, t.cols);
+ try testing.expectEqual(before.rows, t.rows);
+ try testing.expectEqual(before.scrolling_region, t.scrolling_region);
+ try testing.expectEqual(before.flags, t.flags);
+ try testing.expect(std.meta.eql(before.tabstops, t.tabstops));
+ try testing.expectEqual(before.screens.active_key, t.screens.active_key);
+ try testing.expectEqual(before.screens.active, t.screens.active);
+
+ try testing.expectEqual(before_primary.pages.cols, primary.pages.cols);
+ try testing.expectEqual(before_primary.pages.rows, primary.pages.rows);
+ try testing.expectEqual(
+ before_primary.pages.total_rows,
+ primary.pages.total_rows,
+ );
+ try testing.expect(std.meta.eql(before_primary.cursor, primary.cursor));
+ try testing.expectEqualSlices(
+ u8,
+ before_primary_page,
+ primary.pages.pages.first.?.page().memory,
+ );
+
+ try testing.expectEqual(before_alternate.pages.cols, alternate.pages.cols);
+ try testing.expectEqual(before_alternate.pages.rows, alternate.pages.rows);
+ try testing.expectEqual(
+ before_alternate.pages.total_rows,
+ alternate.pages.total_rows,
+ );
+ try testing.expect(std.meta.eql(before_alternate.cursor, alternate.cursor));
+ try testing.expectEqualSlices(
+ u8,
+ before_alternate_page,
+ alternate.pages.pages.first.?.page().memory,
+ );
+ }
+}
+
+test "Terminal: alternate resize failure replaces active alternate screen" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ const tw = resize_tw;
+ defer tw.end(.reset) catch unreachable;
+
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
+ defer t.deinit(alloc);
+
+ _ = try t.switchScreen(.alternate);
+ try testing.expectEqual(.alternate, t.screens.active_key);
+ t.screens.active.charset.gl = .G1;
+ try t.printString("alternate");
+ const generation = t.screens.generation(.alternate);
+
+ tw.errorAlways(.alternate_screen, error.OutOfMemory);
+ try t.resize(alloc, .{ .cols = 20, .rows = 4 });
+
+ const alternate = t.screens.get(.alternate).?;
+ try testing.expectEqual(.alternate, t.screens.active_key);
+ try testing.expectEqual(alternate, t.screens.active);
+ try testing.expectEqual(@as(size.CellCountInt, 20), alternate.pages.cols);
+ try testing.expectEqual(@as(size.CellCountInt, 4), alternate.pages.rows);
+ try testing.expect(alternate.pages.getCell(.{ .active = .{} }).?.cell.isEmpty());
+ try testing.expectEqual(.G1, alternate.charset.gl);
+ try testing.expectEqual(generation +% 1, t.screens.generation(.alternate));
+}
+
+test "Terminal: alternate resize replacement failure falls back to primary" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ const tw = resize_tw;
+ defer tw.end(.reset) catch unreachable;
+
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
+ defer t.deinit(alloc);
+
+ _ = try t.switchScreen(.alternate);
+ tw.errorAlways(.alternate_screen, error.OutOfMemory);
+ tw.errorAlways(.alternate_screen_init, error.OutOfMemory);
+ try t.resize(alloc, .{ .cols = 20, .rows = 4 });
+
+ const primary = t.screens.get(.primary).?;
+ try testing.expectEqual(@as(?*Screen, null), t.screens.get(.alternate));
+ try testing.expectEqual(.primary, t.screens.active_key);
+ try testing.expectEqual(primary, t.screens.active);
+ try testing.expectEqual(@as(size.CellCountInt, 20), primary.pages.cols);
+ try testing.expectEqual(@as(size.CellCountInt, 4), primary.pages.rows);
+}
+
/// Set the pwd for the terminal.
pub fn setPwd(self: *Terminal, pwd: []const u8) !void {
if (pwd.len == 0) {
@@ -3625,7 +4647,8 @@ pub fn getPwd(self: *const Terminal) ?[:0]const u8 {
test "Terminal: setPwd preserves a sentinel on allocation failure" {
var failing = testing.FailingAllocator.init(testing.allocator, .{});
const alloc = failing.allocator();
- var t = try init(alloc, .{ .cols = 5, .rows = 1 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 1 });
defer t.deinit(alloc);
try t.pwd.ensureTotalCapacityPrecise(alloc, 3);
@@ -3635,7 +4658,7 @@ test "Terminal: setPwd preserves a sentinel on allocation failure" {
}
test "Terminal: setPwd accepts its current value" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 1 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 1 });
defer t.deinit(testing.allocator);
try t.setPwd("file:///tmp");
@@ -3669,7 +4692,8 @@ pub fn getTitle(self: *const Terminal) ?[:0]const u8 {
test "Terminal: setTitle preserves a sentinel on allocation failure" {
var failing = testing.FailingAllocator.init(testing.allocator, .{});
const alloc = failing.allocator();
- var t = try init(alloc, .{ .cols = 5, .rows = 1 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 1 });
defer t.deinit(alloc);
try t.title.ensureTotalCapacityPrecise(alloc, 5);
@@ -3679,7 +4703,7 @@ test "Terminal: setTitle preserves a sentinel on allocation failure" {
}
test "Terminal: setTitle accepts its current value" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 1 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 1 });
defer t.deinit(testing.allocator);
try t.setTitle("Ghostty");
@@ -3715,13 +4739,14 @@ pub fn switchScreen(self: *Terminal, key: ScreenSet.Key) !?*Screen {
const new = self.screens.get(key) orelse new: {
const primary = self.screens.get(.primary).?;
break :new try self.screens.getInit(
+ old.io,
old.alloc,
key,
.{
.cols = self.cols,
.rows = self.rows,
- .max_scrollback = switch (key) {
- .primary => primary.pages.explicit_max_size,
+ .max_scrollback_bytes = switch (key) {
+ .primary => primary.pages.limits.bytes.explicit,
.alternate => 0,
},
@@ -3894,13 +4919,21 @@ pub fn fullReset(self: *Terminal) void {
self.screens.active.reset();
// Rest our basic state
+ const visible = self.flags.visible;
self.modes.reset();
- self.flags = .{};
+ self.flags = .{
+ // Visibility belongs to the view rather than terminal state, so a
+ // terminal reset must not make a hidden view potentially visible.
+ .visible = visible,
+ };
self.tabstops.reset(TABSTOP_INTERVAL);
self.previous_char = null;
self.pwd.clearRetainingCapacity();
self.title.clearRetainingCapacity();
self.glyph_glossary.clearAndFree(self.gpa());
+ // A reset only interrupts an in-progress chunked OSC 72 command;
+ // drag and drop registration survives, matching kitty.
+ if (self.kitty_dnd) |dnd| dnd.chunking = .{};
self.status_display = .main;
self.scrolling_region = .{
.top = 0,
@@ -3908,6 +4941,7 @@ pub fn fullReset(self: *Terminal) void {
.left = 0,
.right = self.cols - 1,
};
+ self.setCursorStyle(.default);
// Always mark dirty so we redraw everything
self.flags.dirty.clear = true;
@@ -3925,7 +4959,8 @@ fn clearDirty(t: *Terminal) void {
test "Terminal: setCursorPos saturates overflowing origin offsets" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
t.scrolling_region = .{
@@ -3943,7 +4978,8 @@ test "Terminal: setCursorPos saturates overflowing origin offsets" {
test "Terminal: input with no control characters" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 40, .rows = 40 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 40, .rows = 40 });
defer t.deinit(alloc);
// Basic grid writing
@@ -3963,7 +4999,8 @@ test "Terminal: input with no control characters" {
test "Terminal: input with basic wraparound" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 40 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 40 });
defer t.deinit(alloc);
// Basic grid writing
@@ -3980,7 +5017,8 @@ test "Terminal: input with basic wraparound" {
test "Terminal: input with basic wraparound dirty" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 40 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 40 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
@@ -3995,7 +5033,8 @@ test "Terminal: input with basic wraparound dirty" {
test "Terminal: input that forces scroll" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 1, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 1, .rows = 5 });
defer t.deinit(alloc);
// Basic grid writing
@@ -4011,7 +5050,8 @@ test "Terminal: input that forces scroll" {
test "Terminal: input unique style per cell" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 30, .rows = 30 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
for (0..t.rows) |y| {
@@ -4030,7 +5070,8 @@ test "Terminal: input unique style per cell" {
test "Terminal: input glitch text" {
const glitch = @embedFile("res/glitch.txt");
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 30, .rows = 30 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
// Get our initial grapheme capacity.
@@ -4052,7 +5093,7 @@ test "Terminal: input glitch text" {
}
test "Terminal: zero-width character at start" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// This used to crash the terminal. This is not allowed so we should
@@ -4068,7 +5109,7 @@ test "Terminal: zero-width character at start" {
// https://github.com/ghostty-org/ghostty/pull/12581
test "Terminal: zero-width character attaches to pending wrap cell" {
- var t = try init(testing.allocator, .{ .cols = 2, .rows = 2 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 2, .rows = 2 });
defer t.deinit(testing.allocator);
// Disable grapheme clustering to exercise the fallback path.
@@ -4083,9 +5124,32 @@ test "Terminal: zero-width character attaches to pending wrap cell" {
try testing.expectEqualStrings("xå̲", str);
}
+test "Terminal: caps zero-width codepoints attached to one cell" {
+ var t = try init(testing.io, testing.allocator, .{ .cols = 2, .rows = 2 });
+ defer t.deinit(testing.allocator);
+
+ t.modes.set(.grapheme_cluster, false);
+ try t.print('A');
+
+ const initial_capacity = t.screens.active.cursor.page_pin.node.capacity().grapheme_bytes;
+ for (0..pagepkg.grapheme_max_len * 4) |_| try t.print(0x0301);
+
+ const list_cell = t.screens.active.pages.getCell(.{
+ .screen = .{ .x = 0, .y = 0 },
+ }).?;
+ try testing.expectEqual(
+ @as(usize, pagepkg.grapheme_max_len),
+ list_cell.node.page().lookupGrapheme(list_cell.cell).?.len,
+ );
+ try testing.expectEqual(
+ initial_capacity,
+ list_cell.node.capacity().grapheme_bytes,
+ );
+}
+
// https://github.com/mitchellh/ghostty/issues/1400
test "Terminal: print single very long line" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// This would crash for issue 1400. So the assertion here is
@@ -4094,7 +5158,7 @@ test "Terminal: print single very long line" {
}
test "Terminal: print wide char" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face
@@ -4104,7 +5168,7 @@ test "Terminal: print wide char" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
@@ -4117,7 +5181,7 @@ test "Terminal: print wide char" {
}
test "Terminal: print wide char at edge creates spacer head" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
t.setCursorPos(1, 10);
@@ -4133,7 +5197,7 @@ test "Terminal: print wide char at edge creates spacer head" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
@@ -4152,7 +5216,8 @@ test "Terminal: print wide char at edge creates spacer head" {
test "Terminal: print wide char with 1-column width" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 1, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 1, .rows = 2 });
defer t.deinit(alloc);
try t.print('😀'); // 0x1F600
@@ -4162,7 +5227,7 @@ test "Terminal: print wide char with 1-column width" {
}
test "Terminal: print wide char in single-width terminal" {
- var t = try init(testing.allocator, .{ .cols = 1, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 1, .rows = 80 });
defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face
@@ -4173,7 +5238,7 @@ test "Terminal: print wide char in single-width terminal" {
{
const list_cell = t.screens.active.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);
}
@@ -4181,7 +5246,7 @@ test "Terminal: print wide char in single-width terminal" {
}
test "Terminal: print over wide char at 0,0" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face
@@ -4194,13 +5259,13 @@ test "Terminal: print over wide char at 0,0" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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);
}
@@ -4216,7 +5281,8 @@ test "Terminal: print over wide char at col 0 corrupts previous row" {
// when that cell is a .spacer_tail rather than a .spacer_head. This
// orphans the .wide cell at cols-2.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
defer t.deinit(alloc);
// Fill rows 0 and 1 with wide chars (5 per row on a 10-col terminal).
@@ -4247,7 +5313,7 @@ test "Terminal: print over wide char at col 0 corrupts previous row" {
}
test "Terminal: print over wide spacer tail" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
try t.print('橋');
@@ -4257,13 +5323,13 @@ test "Terminal: print over wide spacer tail" {
{
const list_cell = t.screens.active.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);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'X'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'X'), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
@@ -4277,7 +5343,7 @@ test "Terminal: print over wide spacer tail" {
}
test "Terminal: print over wide char with bold" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.setAttribute(.{ .bold = {} });
@@ -4303,7 +5369,7 @@ test "Terminal: print over wide char with bold" {
}
test "Terminal: print over wide char with bg color" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_bg = .{
@@ -4333,7 +5399,7 @@ test "Terminal: print over wide char with bg color" {
}
test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// https://github.com/mitchellh/ghostty/issues/289
@@ -4353,7 +5419,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4362,7 +5428,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.node.page().lookupGrapheme(cell) == null);
@@ -4370,7 +5436,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F469), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F469), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4379,7 +5445,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.node.page().lookupGrapheme(cell) == null);
@@ -4387,7 +5453,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F467), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F467), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
try testing.expect(list_cell.node.page().lookupGrapheme(cell) == null);
@@ -4395,7 +5461,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 5, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.node.page().lookupGrapheme(cell) == null);
@@ -4405,7 +5471,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
}
test "Terminal: enabling grapheme mode handles stored breaks" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 1 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 1 });
defer t.deinit(testing.allocator);
t.modes.set(.grapheme_cluster, false);
@@ -4425,7 +5491,7 @@ test "Terminal: enabling grapheme mode handles stored breaks" {
// cluster or string end. This keeps the streaming printer's cursor advance
// in sync with the buffered measurement API for representative clusters.
fn expectGraphemeWidthParity(cps: []const u21) !void {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.grapheme_cluster, true);
@@ -4459,7 +5525,7 @@ test "Terminal: graphemeWidth parity" {
}
test "Terminal: VS16 doesn't make character with 2027 disabled" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Disable grapheme clustering
@@ -4477,7 +5543,7 @@ test "Terminal: VS16 doesn't make character with 2027 disabled" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4486,7 +5552,7 @@ test "Terminal: VS16 doesn't make character with 2027 disabled" {
}
test "Terminal: ignored VS16 doesn't mark dirty" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Disable grapheme clustering
@@ -4501,7 +5567,7 @@ test "Terminal: ignored VS16 doesn't mark dirty" {
}
test "Terminal: print invalid VS16 non-grapheme" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// https://github.com/mitchellh/ghostty/issues/1482
@@ -4517,19 +5583,19 @@ test "Terminal: print invalid VS16 non-grapheme" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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);
}
}
test "Terminal: invalid VS16 doesn't mark dirty" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Disable grapheme clustering
@@ -4545,7 +5611,7 @@ test "Terminal: invalid VS16 doesn't mark dirty" {
// https://github.com/ghostty-org/ghostty/pull/12596
test "Terminal: variation selectors apply to preceding codepoint" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4559,13 +5625,13 @@ test "Terminal: variation selectors apply to preceding codepoint" {
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F3F4), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F3F4), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{ 0x200D, 0x2620, 0xFE0F }, list_cell.node.page().lookupGrapheme(cell).?);
}
test "Terminal: print multicodepoint grapheme, mode 2027" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4591,7 +5657,7 @@ test "Terminal: print multicodepoint grapheme, mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4600,14 +5666,14 @@ test "Terminal: print multicodepoint grapheme, mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
}
}
test "Terminal: keypad sequence VS15" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4629,14 +5695,14 @@ test "Terminal: keypad sequence VS15" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x23), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x23), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: keypad sequence VS16" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4658,14 +5724,14 @@ test "Terminal: keypad sequence VS16" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x23), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x23), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
}
test "Terminal: Fitzpatrick skin tone next valid base" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4687,14 +5753,14 @@ test "Terminal: Fitzpatrick skin tone next valid base" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F44B), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F44B), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
}
test "Terminal: Fitzpatrick skin tone next to non-base" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4718,28 +5784,28 @@ test "Terminal: Fitzpatrick skin tone next to non-base" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F3FF), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F3FF), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: multicodepoint grapheme marks dirty on every codepoint" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4768,7 +5834,7 @@ test "Terminal: multicodepoint grapheme marks dirty on every codepoint" {
}
test "Terminal: VS15 to make narrow character" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4799,7 +5865,7 @@ test "Terminal: VS15 to make narrow character" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4808,7 +5874,7 @@ test "Terminal: VS15 to make narrow character" {
}
test "Terminal: VS15 on already narrow emoji" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4834,7 +5900,7 @@ test "Terminal: VS15 on already narrow emoji" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x26C8), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x26C8), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4843,7 +5909,7 @@ test "Terminal: VS15 on already narrow emoji" {
}
test "Terminal: print invalid VS15 following emoji is wide" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4861,20 +5927,20 @@ test "Terminal: print invalid VS15 following emoji is wide" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '\u{1F9E0}'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '\u{1F9E0}'), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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.spacer_tail, cell.wide);
}
}
test "Terminal: print invalid VS15 in emoji ZWJ sequence" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4894,7 +5960,7 @@ test "Terminal: print invalid VS15 in emoji ZWJ sequence" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '\u{1F469}'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '\u{1F469}'), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{ '\u{200D}', '\u{1F466}' }, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -4902,13 +5968,13 @@ test "Terminal: print invalid VS15 in emoji ZWJ sequence" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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.spacer_tail, cell.wide);
}
}
test "Terminal: VS15 to make narrow character with pending wrap" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 4 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 4 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4945,7 +6011,7 @@ test "Terminal: VS15 to make narrow character with pending wrap" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -4955,16 +6021,78 @@ test "Terminal: VS15 to make narrow character with pending wrap" {
// VS15 should not affect the previous grapheme
{
const lemon_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?.cell;
- try testing.expectEqual(@as(u21, 0x1F34B), lemon_cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F34B), lemon_cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, lemon_cell.wide);
const spacer_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?.cell;
- try testing.expectEqual(@as(u21, 0), spacer_cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0), spacer_cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.spacer_tail, spacer_cell.wide);
}
}
+test "Terminal: VS15 narrows wide cell under cursor with wraparound disabled" {
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
+ defer t.deinit(testing.allocator);
+
+ t.modes.set(.grapheme_cluster, true);
+ t.modes.set(.wraparound, false);
+
+ // First create a wide cell spanning columns 4 and 5.
+ t.setCursorPos(1, 4);
+ try t.print(0x2614);
+
+ // Make column 4 the right margin and put the cursor on the wide base.
+ // With wraparound disabled, grapheme lookup selects the cell under the
+ // cursor when it has content.
+ t.modes.set(.enable_left_and_right_margin, true);
+ t.setLeftAndRightMargin(1, 4);
+ t.setCursorPos(1, 4);
+ try t.print(0xFE0E);
+
+ try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x);
+ try testing.expect(!t.screens.active.cursor.pending_wrap);
+ const base = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?.cell;
+ try testing.expectEqual(Cell.Wide.narrow, base.wide);
+ try testing.expect(base.hasGrapheme());
+ const tail = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?.cell;
+ try testing.expectEqual(Cell.Wide.narrow, tail.wide);
+}
+
+test "Terminal: VS15 narrows wide cell under restored pending cursor" {
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
+ defer t.deinit(testing.allocator);
+
+ t.modes.set(.grapheme_cluster, true);
+ t.modes.set(.enable_left_and_right_margin, true);
+ t.setLeftAndRightMargin(1, 4);
+
+ // Save a pending-wrap cursor at column 4.
+ t.setCursorPos(1, 4);
+ try t.print('X');
+ try testing.expect(t.screens.active.cursor.pending_wrap);
+ t.saveCursor();
+
+ // Widen the margin and replace that cell with a wide character.
+ t.setLeftAndRightMargin(1, 5);
+ t.setCursorPos(1, 4);
+ try t.print(0x2614);
+
+ // Restoring also restores pending_wrap, so grapheme lookup selects the
+ // wide base under the cursor rather than its spacer tail.
+ t.restoreCursor();
+ try testing.expect(t.screens.active.cursor.pending_wrap);
+ try t.print(0xFE0E);
+
+ try testing.expectEqual(@as(usize, 4), t.screens.active.cursor.x);
+ try testing.expect(!t.screens.active.cursor.pending_wrap);
+ const base = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?.cell;
+ try testing.expectEqual(Cell.Wide.narrow, base.wide);
+ try testing.expect(base.hasGrapheme());
+ const tail = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?.cell;
+ try testing.expectEqual(Cell.Wide.narrow, tail.wide);
+}
+
test "Terminal: VS16 to make wide character on next line" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 3 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -4989,7 +6117,7 @@ test "Terminal: VS16 to make wide character on next line" {
// Previous cell turns into spacer_head
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_head, cell.wide);
}
@@ -4997,7 +6125,7 @@ test "Terminal: VS16 to make wide character on next line" {
// '#' cell is wide
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '#'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '#'), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -5006,7 +6134,7 @@ test "Terminal: VS16 to make wide character on next line" {
// spacer_tail
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?;
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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
}
@@ -5015,7 +6143,7 @@ test "Terminal: VS16 to make wide character on next line" {
test "Terminal: VS16 to make wide character on next line with hyperlink" {
// Regression test for the crash fixed in print's grapheme `.wide` path:
// writing a spacer_head at the screen edge before row.wrap was set.
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 3 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering and activate a hyperlink so printCell
@@ -5039,7 +6167,7 @@ test "Terminal: VS16 to make wide character on next line with hyperlink" {
// Previous cell turns into spacer_head and remains hyperlinked.
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .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.spacer_head, cell.wide);
try testing.expect(cell.hyperlink);
try testing.expect(list_cell.row.wrap);
@@ -5048,7 +6176,7 @@ test "Terminal: VS16 to make wide character on next line with hyperlink" {
// '#' cell is now wide and still hyperlinked.
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '#'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '#'), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -5058,14 +6186,114 @@ test "Terminal: VS16 to make wide character on next line with hyperlink" {
// spacer_tail inherits hyperlink as part of the same grapheme cell.
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?;
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.spacer_tail, cell.wide);
try testing.expect(cell.hyperlink);
}
}
+test "Terminal: VS16 widening when the spacer tail grows the page" {
+ // Regression test for a stale cell pointer in print's grapheme `.wide`
+ // path: writing the spacer tail can grow the page to fit the hyperlink,
+ // which replaces the page and invalidates the pointer to the wide cell.
+ var t = try init(testing.io, testing.allocator, .{ .rows = 10, .cols = 20 });
+ defer t.deinit(testing.allocator);
+
+ t.modes.set(.grapheme_cluster, true);
+ try t.screens.active.startHyperlink("http://example.com", null);
+
+ // Fill the page hyperlink map until a single slot is left. The '#' below
+ // takes that slot so the spacer tail is what forces the page to grow.
+ while (true) {
+ const page = t.screens.active.cursor.page_pin.node.page();
+ const map = page.hyperlink_map.map(page.memory);
+ if (map.maxLoad() - map.count() == 1) break;
+ try t.print('x');
+ }
+
+ const x = t.screens.active.cursor.x;
+ const y = t.screens.active.cursor.y;
+ try t.print('#');
+
+ // Without the fix this crashed appending to a freed page.
+ try t.print(0xFE0F);
+
+ {
+ // '#' is wide and carries the VS16 grapheme.
+ const list_cell = t.screens.active.pages.getCell(.{ .active = .{
+ .x = x,
+ .y = y,
+ } }).?;
+ const cell = list_cell.cell;
+ try testing.expectEqual(@as(u21, '#'), cell.content.codepoint.data);
+ try testing.expectEqual(Cell.Wide.wide, cell.wide);
+ try testing.expect(cell.hasGrapheme());
+ try testing.expectEqualSlices(
+ u21,
+ &.{0xFE0F},
+ list_cell.node.page().lookupGrapheme(cell).?,
+ );
+ }
+ {
+ const list_cell = t.screens.active.pages.getCell(.{ .active = .{
+ .x = x + 1,
+ .y = y,
+ } }).?;
+ try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide);
+ }
+}
+
+test "Terminal: grapheme transfer when widening wraps to the next line" {
+ // Covers print's grapheme `.wide` path where the previous cell already
+ // holds grapheme data and has to be moved to the wrapped row.
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 3 });
+ defer t.deinit(testing.allocator);
+
+ t.modes.set(.grapheme_cluster, true);
+ t.cursorRight(2);
+
+ // A narrow emoji, then ZWJ, then a second emoji. The ZWJ attaches
+ // without changing the width, so the cell has grapheme data by the time
+ // the second emoji widens it.
+ try t.print(0x263A);
+ try t.print(0x200D);
+ try t.print(0x2764);
+
+ {
+ // The old cell becomes a spacer head on the wrapped row.
+ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{
+ .x = 2,
+ .y = 0,
+ } }).?;
+ try testing.expectEqual(Cell.Wide.spacer_head, list_cell.cell.wide);
+ try testing.expect(list_cell.row.wrap);
+ }
+ {
+ // The grapheme moved with the base codepoint.
+ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{
+ .x = 0,
+ .y = 1,
+ } }).?;
+ const cell = list_cell.cell;
+ try testing.expectEqual(@as(u21, 0x263A), cell.content.codepoint.data);
+ try testing.expectEqual(Cell.Wide.wide, cell.wide);
+ try testing.expectEqualSlices(
+ u21,
+ &.{ 0x200D, 0x2764 },
+ list_cell.node.page().lookupGrapheme(cell).?,
+ );
+ }
+ {
+ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{
+ .x = 1,
+ .y = 1,
+ } }).?;
+ try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide);
+ }
+}
+
test "Terminal: VS16 to make wide character with pending wrap" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 3 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5086,7 +6314,7 @@ test "Terminal: VS16 to make wide character with pending wrap" {
// '#' cell is wide
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '#'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '#'), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -5095,14 +6323,14 @@ test "Terminal: VS16 to make wide character with pending wrap" {
// spacer_tail
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
}
}
test "Terminal: VS16 to make wide character with mode 2027" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5124,7 +6352,7 @@ test "Terminal: VS16 to make wide character with mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -5133,7 +6361,7 @@ test "Terminal: VS16 to make wide character with mode 2027" {
}
test "Terminal: VS16 repeated with mode 2027" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5155,7 +6383,7 @@ test "Terminal: VS16 repeated with mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -5164,7 +6392,7 @@ test "Terminal: VS16 repeated with mode 2027" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.node.page().lookupGrapheme(cell).?;
@@ -5173,7 +6401,7 @@ test "Terminal: VS16 repeated with mode 2027" {
}
test "Terminal: print invalid VS16 grapheme" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5192,20 +6420,20 @@ test "Terminal: print invalid VS16 grapheme" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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);
}
}
test "Terminal: print invalid VS16 with second char" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5225,7 +6453,7 @@ test "Terminal: print invalid VS16 with second char" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
@@ -5233,14 +6461,14 @@ test "Terminal: print invalid VS16 with second char" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'y'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'y'), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: print grapheme ò (o with nonspacing mark) should be narrow" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5258,7 +6486,7 @@ test "Terminal: print grapheme ò (o with nonspacing mark) should be narrow" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'o'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'o'), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{0x0300}, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
@@ -5266,7 +6494,7 @@ test "Terminal: print grapheme ò (o with nonspacing mark) should be narrow" {
}
test "Terminal: print Devanagari grapheme should be wide" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5287,7 +6515,7 @@ test "Terminal: print Devanagari grapheme should be wide" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -5300,7 +6528,7 @@ test "Terminal: print Devanagari grapheme should be wide" {
}
test "Terminal: print Devanagari grapheme should be wide on next line" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 3 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5327,7 +6555,7 @@ test "Terminal: print Devanagari grapheme should be wide on next line" {
// Previous cell turns into spacer_head
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_head, cell.wide);
}
@@ -5335,7 +6563,7 @@ test "Terminal: print Devanagari grapheme should be wide on next line" {
// Devanagari grapheme is wide
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -5350,7 +6578,7 @@ test "Terminal: print Devanagari grapheme should be wide on next line" {
test "Terminal: print Devanagari grapheme should be wide on next page" {
const rows = pagepkg.std_capacity.rows;
const cols = pagepkg.std_capacity.cols;
- var t = try init(testing.allocator, .{ .rows = rows, .cols = cols });
+ var t = try init(testing.io, testing.allocator, .{ .rows = rows, .cols = cols });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5386,7 +6614,7 @@ test "Terminal: print Devanagari grapheme should be wide on next page" {
// Previous cell turns into spacer_head
const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = cols - 1, .y = rows - 2 } }).?;
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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_head, cell.wide);
}
@@ -5394,7 +6622,7 @@ test "Terminal: print Devanagari grapheme should be wide on next page" {
// Devanagari grapheme is wide
const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = rows - 1 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
@@ -5407,7 +6635,7 @@ test "Terminal: print Devanagari grapheme should be wide on next page" {
}
test "Terminal: print invalid VS16 with second char (combining)" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5427,7 +6655,7 @@ test "Terminal: print invalid VS16 with second char (combining)" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'n'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'n'), cell.content.codepoint.data);
try testing.expect(cell.hasGrapheme());
try testing.expectEqualSlices(u21, &.{'\u{0303}'}, list_cell.node.page().lookupGrapheme(cell).?);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
@@ -5435,13 +6663,13 @@ test "Terminal: print invalid VS16 with second char (combining)" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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);
}
}
test "Terminal: overwrite grapheme should clear grapheme data" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5465,14 +6693,14 @@ test "Terminal: overwrite grapheme should clear grapheme data" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: overwrite multicodepoint grapheme clears grapheme data" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5512,7 +6740,7 @@ test "Terminal: overwrite multicodepoint grapheme clears grapheme data" {
}
test "Terminal: overwrite multicodepoint grapheme tail clears grapheme data" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
@@ -5551,7 +6779,8 @@ test "Terminal: overwrite multicodepoint grapheme tail clears grapheme data" {
test "Terminal: print breaks valid grapheme cluster with Prepend + ASCII for speed" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.grapheme_cluster, true);
@@ -5578,7 +6807,7 @@ test "Terminal: print breaks valid grapheme cluster with Prepend + ASCII for spe
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x0600), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x0600), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
// This is what we'd expect if we did break correctly:
//try testing.expect(cell.hasGrapheme());
@@ -5588,16 +6817,16 @@ test "Terminal: print breaks valid grapheme cluster with Prepend + ASCII for spe
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '1'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '1'), cell.content.codepoint.data);
// This is what we'd expect if we did break correctly:
- //try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
+ //try testing.expectEqual(@as(u21, 0), cell.content.codepoint.data);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: print writes to bottom if scrolled" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 2 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 2 });
defer t.deinit(testing.allocator);
// Basic grid writing
@@ -5639,7 +6868,7 @@ test "Terminal: print writes to bottom if scrolled" {
}
test "Terminal: print charset" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// G1 should have no effect
@@ -5668,7 +6897,7 @@ test "Terminal: print charset" {
}
test "Terminal: print charset outside of ASCII" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// G1 should have no effect
@@ -5693,7 +6922,7 @@ test "Terminal: print charset outside of ASCII" {
}
test "Terminal: print invoke charset" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
t.configureCharset(.G1, .dec_special);
@@ -5717,7 +6946,7 @@ test "Terminal: print invoke charset" {
}
test "Terminal: print invoke charset single" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
t.configureCharset(.G1, .dec_special);
@@ -5737,7 +6966,7 @@ test "Terminal: print invoke charset single" {
test "Terminal: print kitty unicode placeholder" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
try t.print(kitty.graphics.unicode.placeholder);
@@ -5747,7 +6976,7 @@ test "Terminal: print kitty unicode placeholder" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), cell.content.codepoint.data);
try testing.expect(list_cell.row.kitty_virtual_placeholder);
}
@@ -5755,7 +6984,7 @@ test "Terminal: print kitty unicode placeholder" {
}
test "Terminal: soft wrap" {
- var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 3, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
@@ -5770,7 +6999,7 @@ test "Terminal: soft wrap" {
}
test "Terminal: soft wrap with semantic prompt" {
- var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 3, .rows = 80 });
defer t.deinit(testing.allocator);
// Mark our prompt.
@@ -5791,7 +7020,7 @@ test "Terminal: soft wrap with semantic prompt" {
}
test "Terminal: disabled wraparound with wide char and one space" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.wraparound, false);
@@ -5814,7 +7043,7 @@ test "Terminal: disabled wraparound with wide char and one space" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .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);
}
@@ -5823,7 +7052,7 @@ test "Terminal: disabled wraparound with wide char and one space" {
}
test "Terminal: disabled wraparound with wide char and no space" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.wraparound, false);
@@ -5845,7 +7074,7 @@ test "Terminal: disabled wraparound with wide char and no space" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
@@ -5854,7 +7083,7 @@ test "Terminal: disabled wraparound with wide char and no space" {
}
test "Terminal: disabled wraparound with wide grapheme and half space" {
- var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.grapheme_cluster, true);
@@ -5878,7 +7107,7 @@ test "Terminal: disabled wraparound with wide grapheme and half space" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '❤'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '❤'), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
@@ -5887,7 +7116,7 @@ test "Terminal: disabled wraparound with wide grapheme and half space" {
}
test "Terminal: print right margin wrap" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
@@ -5910,7 +7139,7 @@ test "Terminal: print right margin wrap" {
}
test "Terminal: print right margin wrap dirty tracking" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
@@ -5939,7 +7168,7 @@ test "Terminal: print right margin wrap dirty tracking" {
}
test "Terminal: print right margin outside" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
@@ -5959,7 +7188,7 @@ test "Terminal: print right margin outside" {
}
test "Terminal: print right margin outside wrap" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
@@ -5976,7 +7205,7 @@ test "Terminal: print right margin outside wrap" {
}
test "Terminal: print wide char at right margin does not create spacer head" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
t.modes.set(.enable_left_and_right_margin, true);
@@ -5993,7 +7222,7 @@ test "Terminal: print wide char at right margin does not create spacer head" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .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);
const row = list_cell.row;
@@ -6002,7 +7231,7 @@ test "Terminal: print wide char at right margin does not create spacer head" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 1 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
@@ -6013,7 +7242,7 @@ test "Terminal: print wide char at right margin does not create spacer head" {
}
test "Terminal: print with hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Setup our hyperlink and print
@@ -6038,7 +7267,7 @@ test "Terminal: print with hyperlink" {
}
test "Terminal: print over cell with same hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Setup our hyperlink and print
@@ -6065,7 +7294,7 @@ test "Terminal: print over cell with same hyperlink" {
}
test "Terminal: print and end hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Setup our hyperlink and print
@@ -6102,7 +7331,7 @@ test "Terminal: print and end hyperlink" {
}
test "Terminal: print and change hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Setup our hyperlink and print
@@ -6137,7 +7366,7 @@ test "Terminal: print and change hyperlink" {
}
test "Terminal: overwrite hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Setup our hyperlink and print
@@ -6170,7 +7399,7 @@ test "Terminal: overwrite hyperlink" {
// flag. The integrity check inside setHyperlink (or increaseCapacity)
// sees the unwrapped spacer head and panics. Found via fuzzing.
test "Terminal: print wide char at right edge with hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -6197,7 +7426,7 @@ test "Terminal: print wide char at right edge with hyperlink" {
// Row 1, col 0: the wide char with hyperlink
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
- try testing.expectEqual(@as(u21, 0x4E2D), list_cell.cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x4E2D), list_cell.cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide);
try testing.expect(list_cell.cell.hyperlink);
}
@@ -6209,8 +7438,87 @@ test "Terminal: print wide char at right edge with hyperlink" {
}
}
+// A cursor style or hyperlink ID is an index into a set stored in the
+// page memory of the page the cursor pin points at. When scrollClear
+// (here via ED 22, kitty's scroll_complete) pushes the active area onto
+// a later page while the cursor pin is still on an earlier one,
+// cursorReload must migrate the cursor's style and hyperlink references
+// to the destination page. It previously replaced the pin directly,
+// leaving the cursor holding an ID that was dead or aliased an unrelated
+// entry on the new page, and the next print attached a live cell to it.
+// Found via fuzzing.
+test "Terminal: scrollClear across pages keeps cursor hyperlink refs page-local" {
+ const alloc = testing.allocator;
+
+ // Minimized from a 774-byte AFL fuzz input. Reading it:
+ //
+ // A print, so REP has something to repeat
+ // ESC [ 48111 b REP, filling the page and spilling onto a second
+ // ESC ] 8 ; ; 0x93 OSC 8; the C1 byte terminates the OSC and makes
+ // the URI non-empty, so a hyperlink starts
+ // ESC [ 11 A CUU, moving the cursor back onto the first page
+ // ESC [ 22 J ED 22, i.e. scroll_complete -> Screen.scrollClear
+ // B print, which attaches the cursor hyperlink
+ // ESC ] 8 ; ; ESC OSC 8 with an empty URI, ending the hyperlink
+ //
+ // The grid must be wide enough to fill a page from a single REP, so
+ // this does not reproduce at 80x24.
+ const input = "A\x1b[48111b\x1b]8;;\x93\x1b[11A\x1b[22JB\x1b]8;;\x1b";
+
+ var t = try init(testing.io, alloc, .{ .cols = 200, .rows = 50 });
+ defer t.deinit(alloc);
+
+ {
+ var s = t.vtStream();
+ defer s.deinit();
+ s.nextSlice(input);
+ }
+
+ // With slow runtime safety on, the page integrity checks during the
+ // stream above already catch the bug. Verify the ref counts explicitly
+ // as well so this test is meaningful with runtime safety off: every
+ // cell holding a hyperlink ID owns a reference, so a count below the
+ // number of holding cells means a live cell points at an entry that
+ // was already freed.
+ var node_ = t.screens.active.pages.pages.first;
+ while (node_) |node| : (node_ = node.next) {
+ const page = node.page();
+ const cap = page.hyperlink_set.layout.cap;
+ if (cap == 0) continue;
+
+ const holders = try alloc.alloc(u32, cap);
+ defer alloc.free(holders);
+ @memset(holders, 0);
+
+ for (page.rows.ptr(page.memory)[0..page.size.rows]) |*row| {
+ if (!row.hyperlink) continue;
+ for (row.cells.ptr(page.memory)[0..page.size.cols]) |*cell| {
+ if (!cell.hyperlink) continue;
+ const id = page.lookupHyperlink(cell) orelse continue;
+ if (id < cap) holders[id] += 1;
+ }
+ }
+
+ for (holders, 0..) |held, id| {
+ if (held == 0) continue;
+ const refs = page.hyperlink_set.refCount(page.memory, @intCast(id));
+ try testing.expect(refs >= held);
+ }
+ }
+
+ // If the cursor still has an active hyperlink, its own extra
+ // reference must live on the cursor's page.
+ const cursor = &t.screens.active.cursor;
+ if (cursor.hyperlink_id != 0) {
+ const page = cursor.page_pin.node.page();
+ try testing.expect(
+ page.hyperlink_set.refCount(page.memory, cursor.hyperlink_id) > 0,
+ );
+ }
+}
+
test "Terminal: linefeed and carriage return" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Print and CR.
@@ -6238,7 +7546,7 @@ test "Terminal: linefeed and carriage return" {
}
test "Terminal: linefeed unsets pending wrap" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
@@ -6252,7 +7560,7 @@ test "Terminal: linefeed unsets pending wrap" {
}
test "Terminal: linefeed mode automatic carriage return" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
// Basic grid writing
@@ -6268,7 +7576,7 @@ test "Terminal: linefeed mode automatic carriage return" {
}
test "Terminal: carriage return unsets pending wrap" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
@@ -6279,7 +7587,7 @@ test "Terminal: carriage return unsets pending wrap" {
}
test "Terminal: carriage return origin mode moves to left margin" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
t.modes.set(.origin, true);
@@ -6290,7 +7598,7 @@ test "Terminal: carriage return origin mode moves to left margin" {
}
test "Terminal: carriage return left of left margin moves to zero" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
t.screens.active.cursor.x = 1;
@@ -6300,7 +7608,7 @@ test "Terminal: carriage return left of left margin moves to zero" {
}
test "Terminal: carriage return right of left margin moves to left margin" {
- var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
t.screens.active.cursor.x = 3;
@@ -6310,7 +7618,7 @@ test "Terminal: carriage return right of left margin moves to left margin" {
}
test "Terminal: backspace" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// BS
@@ -6328,7 +7636,8 @@ test "Terminal: backspace" {
test "Terminal: horizontal tabs" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
// HT
@@ -6349,7 +7658,8 @@ test "Terminal: horizontal tabs" {
test "Terminal: horizontal tabs starting on tabstop" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(t.screens.active.cursor.y, 9);
@@ -6367,7 +7677,8 @@ test "Terminal: horizontal tabs starting on tabstop" {
test "Terminal: horizontal tabs with right margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.scrolling_region.left = 2;
@@ -6386,7 +7697,8 @@ test "Terminal: horizontal tabs with right margin" {
test "Terminal: horizontal tabs back" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
// Edge of screen
@@ -6409,7 +7721,8 @@ test "Terminal: horizontal tabs back" {
test "Terminal: horizontal tabs back starting on tabstop" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(t.screens.active.cursor.y, 9);
@@ -6427,7 +7740,8 @@ test "Terminal: horizontal tabs back starting on tabstop" {
test "Terminal: horizontal tabs with left margin in origin mode" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
@@ -6447,7 +7761,8 @@ test "Terminal: horizontal tabs with left margin in origin mode" {
test "Terminal: horizontal tab back with cursor before left margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 20, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
@@ -6467,7 +7782,8 @@ test "Terminal: horizontal tab back with cursor before left margin" {
test "Terminal: cursorPos resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -6485,7 +7801,8 @@ test "Terminal: cursorPos resets wrap" {
test "Terminal: cursorPos off the screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(500, 500);
@@ -6500,7 +7817,8 @@ test "Terminal: cursorPos off the screen" {
test "Terminal: cursorPos relative to origin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.top = 2;
@@ -6518,7 +7836,8 @@ test "Terminal: cursorPos relative to origin" {
test "Terminal: cursorPos relative to origin with left/right" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.top = 2;
@@ -6538,7 +7857,8 @@ test "Terminal: cursorPos relative to origin with left/right" {
test "Terminal: cursorPos limits with full scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.top = 2;
@@ -6558,7 +7878,7 @@ test "Terminal: cursorPos limits with full scroll region" {
// Probably outdated, but dates back to the original terminal implementation.
test "Terminal: setCursorPos (original test)" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.x);
@@ -6611,7 +7931,8 @@ test "Terminal: setCursorPos (original test)" {
test "Terminal: setTopAndBottomMargin simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6641,7 +7962,8 @@ test "Terminal: setTopAndBottomMargin simple" {
test "Terminal: setTopAndBottomMargin top only" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6671,7 +7993,8 @@ test "Terminal: setTopAndBottomMargin top only" {
test "Terminal: setTopAndBottomMargin top and bottom" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6699,7 +8022,8 @@ test "Terminal: setTopAndBottomMargin top and bottom" {
test "Terminal: setTopAndBottomMargin top equal to bottom" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6728,7 +8052,8 @@ test "Terminal: setTopAndBottomMargin top equal to bottom" {
test "Terminal: setLeftAndRightMargin simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6756,7 +8081,8 @@ test "Terminal: setLeftAndRightMargin simple" {
test "Terminal: setLeftAndRightMargin left only" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6789,7 +8115,8 @@ test "Terminal: setLeftAndRightMargin left only" {
test "Terminal: setLeftAndRightMargin left and right" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6820,7 +8147,8 @@ test "Terminal: setLeftAndRightMargin left and right" {
test "Terminal: setLeftAndRightMargin left equal right" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6851,7 +8179,8 @@ test "Terminal: setLeftAndRightMargin left equal right" {
test "Terminal: setLeftAndRightMargin mode 69 unset" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6882,7 +8211,8 @@ test "Terminal: setLeftAndRightMargin mode 69 unset" {
test "Terminal: insertLines simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6914,7 +8244,8 @@ test "Terminal: insertLines simple" {
test "Terminal: insertLines colors with bg color" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6955,7 +8286,8 @@ test "Terminal: insertLines colors with bg color" {
test "Terminal: insertLines handles style refs" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -6989,7 +8321,8 @@ test "Terminal: insertLines handles style refs" {
test "Terminal: insertLines outside of scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7018,7 +8351,8 @@ test "Terminal: insertLines outside of scroll region" {
test "Terminal: insertLines top/bottom scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7051,7 +8385,8 @@ test "Terminal: insertLines top/bottom scroll region" {
test "Terminal: insertLines across page boundary marks all shifted rows dirty" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 10, .max_scrollback_bytes = 1024 });
defer t.deinit(alloc);
const first_page = t.screens.active.pages.pages.first.?;
@@ -7099,9 +8434,96 @@ test "Terminal: insertLines across page boundary marks all shifted rows dirty" {
}
}
+test "Terminal: insertLines hyperlink-dense row crosses page boundary" {
+ // Regression test for the cross-page copy of insertLines: when the
+ // shifted row carries more unique hyperlinks than the destination
+ // page's hyperlink capacity, the copy must increase the destination
+ // page's capacity and retry rather than corrupting the page list.
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 10, .max_scrollback_bytes = 1024 });
+ defer t.deinit(alloc);
+
+ const pages = &t.screens.active.pages;
+
+ // Fill the first page so it is exactly full, then two more rows so
+ // the second page holds the last two active rows (y=3 and y=4).
+ const first_page_rows = pages.pages.first.?.capacity().rows;
+ for (0..first_page_rows + 1) |_| try t.linefeed();
+ try testing.expect(pages.pages.first != pages.pages.last);
+ try testing.expectEqual(@as(usize, 2), pages.pages.last.?.rows());
+
+ // Marker rows so we can verify the shift afterwards.
+ t.setCursorPos(1, 1);
+ try t.printString("0");
+ t.setCursorPos(2, 1);
+ try t.printString("1");
+ t.setCursorPos(4, 1);
+ try t.printString("3");
+ t.setCursorPos(5, 1);
+ try t.printString("4");
+
+ // Fill the last row of the first page (active y=2) with unique
+ // hyperlinks: more than the second page can hold with its default
+ // hyperlink capacity. Writing them grows the first page's capacity
+ // as needed; the second page keeps its default capacity.
+ t.setCursorPos(3, 1);
+ for (0..10) |i| {
+ var buf: [64]u8 = undefined;
+ const uri = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{i});
+ try t.screens.active.startHyperlink(uri, null);
+ try t.print(@intCast('A' + i));
+ t.screens.active.endHyperlink();
+ }
+ {
+ const pin = pages.pin(.{ .active = .{ .y = 2 } }).?;
+ try testing.expectEqual(pages.pages.first.?, pin.node);
+ try testing.expectEqual(pin.node.rows() - 1, @as(usize, pin.y));
+ }
+ try testing.expect(pages.pages.last.?.page().hyperlink_set.layout.cap < 10);
+
+ // Insert a line at the top: every row shifts down by one and the
+ // dense row crosses the page boundary into the second page.
+ t.setCursorPos(1, 1);
+ t.insertLines(1);
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("\n0\n1\nABCDEFGHIJ\n3", str);
+ }
+
+ // The second page's hyperlink capacity had to grow to receive the
+ // row, proving the capacity-retry path ran.
+ try testing.expect(pages.pages.last.?.page().hyperlink_set.layout.cap >= 10);
+
+ // Every cell of the dense row must still resolve to a real
+ // hyperlink entry with the correct URI. A half-applied shift
+ // leaves cells whose hyperlink flag is set but that have no map
+ // entry, which aborts in clearCells later.
+ for (0..10) |x| {
+ const list_cell = pages.getCell(.{ .active = .{
+ .x = @intCast(x),
+ .y = 3,
+ } }).?;
+ 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 uri = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{x});
+ try testing.expectEqualStrings(uri, link.uri.slice(page.memory));
+ }
+
+ // All pages must pass integrity checks.
+ var node_: ?*PageList.List.Node = pages.pages.first;
+ while (node_) |node| : (node_ = node.next) node.page().assertIntegrity();
+}
+
test "Terminal: insertLines (legacy test)" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// Initial value
@@ -7134,7 +8556,8 @@ test "Terminal: insertLines (legacy test)" {
test "Terminal: insertLines zero" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// This should do nothing
@@ -7144,7 +8567,8 @@ test "Terminal: insertLines zero" {
test "Terminal: insertLines with scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 6 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 6 });
defer t.deinit(alloc);
// Initial value
@@ -7183,7 +8607,8 @@ test "Terminal: insertLines with scroll region" {
test "Terminal: insertLines more than remaining" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// Initial value
@@ -7221,7 +8646,8 @@ test "Terminal: insertLines more than remaining" {
test "Terminal: insertLines resets pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -7239,7 +8665,8 @@ test "Terminal: insertLines resets pending wrap" {
test "Terminal: insertLines resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 3, .cols = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
try t.print('1');
@@ -7265,7 +8692,8 @@ test "Terminal: insertLines resets wrap" {
test "Terminal: insertLines multi-codepoint graphemes" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Disable grapheme clustering
@@ -7297,7 +8725,8 @@ test "Terminal: insertLines multi-codepoint graphemes" {
test "Terminal: insertLines left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -7328,7 +8757,8 @@ test "Terminal: insertLines left/right scroll region" {
test "Terminal: scrollUp simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7361,7 +8791,8 @@ test "Terminal: scrollUp simple" {
test "Terminal: scrollUp moves hyperlink" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7412,7 +8843,8 @@ test "Terminal: scrollUp moves hyperlink" {
test "Terminal: scrollUp clears hyperlink" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -7449,7 +8881,8 @@ test "Terminal: scrollUp clears hyperlink" {
test "Terminal: scrollUp top/bottom scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7479,7 +8912,8 @@ test "Terminal: scrollUp top/bottom scroll region" {
test "Terminal: scrollUp left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -7512,7 +8946,8 @@ test "Terminal: scrollUp left/right scroll region" {
test "Terminal: scrollUp left/right scroll region hyperlink" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -7618,7 +9053,8 @@ test "Terminal: scrollUp left/right scroll region hyperlink" {
test "Terminal: scrollUp preserves pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
@@ -7639,7 +9075,8 @@ test "Terminal: scrollUp preserves pending wrap" {
test "Terminal: scrollUp full top/bottom region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("top");
@@ -7663,7 +9100,8 @@ test "Terminal: scrollUp full top/bottom region" {
test "Terminal: scrollUp full top/bottomleft/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("top");
@@ -7694,7 +9132,8 @@ test "Terminal: scrollUp creates scrollback in primary screen" {
// When in primary screen with full-width scroll region at top,
// scrollUp (CSI S) should push lines into scrollback like xterm.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5, .max_scrollback_bytes = 10 });
defer t.deinit(alloc);
// Fill the screen with content
@@ -7737,10 +9176,11 @@ test "Terminal: scrollUp creates scrollback in primary screen" {
}
}
-test "Terminal: scrollUp with max_scrollback zero" {
- // When max_scrollback is 0, scrollUp should still work but not retain history
+test "Terminal: scrollUp with max_scrollback_bytes zero" {
+ // When max_scrollback_bytes is 0, scrollUp should still work but not retain history
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5, .max_scrollback_bytes = 0 });
defer t.deinit(alloc);
try t.printString("AAAAA");
@@ -7769,10 +9209,11 @@ test "Terminal: scrollUp with max_scrollback zero" {
}
}
-test "Terminal: scrollUp with max_scrollback zero and top margin" {
- // When max_scrollback is 0 and top margin is set, should use deleteLines path
+test "Terminal: scrollUp with max_scrollback_bytes zero and top margin" {
+ // When max_scrollback_bytes is 0 and top margin is set, should use deleteLines path
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5, .max_scrollback_bytes = 0 });
defer t.deinit(alloc);
try t.printString("AAAAA");
@@ -7799,10 +9240,11 @@ test "Terminal: scrollUp with max_scrollback zero and top margin" {
}
}
-test "Terminal: scrollUp with max_scrollback zero and left/right margin" {
- // When max_scrollback is 0 with left/right margins, uses deleteLines path
+test "Terminal: scrollUp with max_scrollback_bytes zero and left/right margin" {
+ // When max_scrollback_bytes is 0 with left/right margins, uses deleteLines path
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 0 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 10, .max_scrollback_bytes = 0 });
defer t.deinit(alloc);
try t.printString("AAAAABBBBB");
@@ -7829,7 +9271,8 @@ test "Terminal: scrollUp with max_scrollback zero and left/right margin" {
test "Terminal: scrollDown simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7861,7 +9304,8 @@ test "Terminal: scrollDown simple" {
test "Terminal: scrollDown hyperlink moves" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -7912,7 +9356,8 @@ test "Terminal: scrollDown hyperlink moves" {
test "Terminal: scrollDown outside of scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -7947,7 +9392,8 @@ test "Terminal: scrollDown outside of scroll region" {
test "Terminal: scrollDown left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -7981,7 +9427,8 @@ test "Terminal: scrollDown left/right scroll region" {
test "Terminal: scrollDown left/right scroll region hyperlink" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -8087,7 +9534,8 @@ test "Terminal: scrollDown left/right scroll region hyperlink" {
test "Terminal: scrollDown outside of left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -8121,7 +9569,8 @@ test "Terminal: scrollDown outside of left/right scroll region" {
test "Terminal: scrollDown preserves pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
@@ -8142,7 +9591,8 @@ test "Terminal: scrollDown preserves pending wrap" {
test "Terminal: eraseChars simple operation" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -8163,7 +9613,8 @@ test "Terminal: eraseChars simple operation" {
test "Terminal: eraseChars minimum one" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -8182,7 +9633,8 @@ test "Terminal: eraseChars minimum one" {
test "Terminal: eraseChars beyond screen edge" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for (" ABC") |c| try t.print(c);
@@ -8198,7 +9650,8 @@ test "Terminal: eraseChars beyond screen edge" {
test "Terminal: eraseChars wide character" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('橋');
@@ -8216,7 +9669,8 @@ test "Terminal: eraseChars wide character" {
test "Terminal: eraseChars resets pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -8234,7 +9688,8 @@ test "Terminal: eraseChars resets pending wrap" {
test "Terminal: eraseChars resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c);
@@ -8264,7 +9719,8 @@ test "Terminal: eraseChars resets wrap" {
test "Terminal: eraseChars preserves background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -8303,7 +9759,8 @@ test "Terminal: eraseChars preserves background sgr" {
test "Terminal: eraseChars handles refcounted styles" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
@@ -8325,7 +9782,8 @@ test "Terminal: eraseChars handles refcounted styles" {
test "Terminal: eraseChars protected attributes respected with iso" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -8342,7 +9800,8 @@ test "Terminal: eraseChars protected attributes respected with iso" {
test "Terminal: eraseChars protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -8361,7 +9820,8 @@ test "Terminal: eraseChars protected attributes ignored with dec most recent" {
test "Terminal: eraseChars protected attributes ignored with dec set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -8378,7 +9838,8 @@ test "Terminal: eraseChars protected attributes ignored with dec set" {
test "Terminal: eraseChars wide char boundary conditions" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 1, .cols = 8 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 1, .cols = 8 });
defer t.deinit(alloc);
try t.printString("😀a😀b😀");
@@ -8401,7 +9862,8 @@ test "Terminal: eraseChars wide char boundary conditions" {
test "Terminal: eraseChars wide char splits proper cell boundaries" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 1, .cols = 30 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 1, .cols = 30 });
defer t.deinit(alloc);
// This is a test for a bug: https://github.com/ghostty-org/ghostty/issues/2817
@@ -8431,7 +9893,8 @@ test "Terminal: eraseChars wide char splits proper cell boundaries" {
test "Terminal: eraseChars wide char wrap boundary conditions" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 3, .cols = 8 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 8 });
defer t.deinit(alloc);
try t.printString(".......😀abcde😀......");
@@ -8460,9 +9923,42 @@ test "Terminal: eraseChars wide char wrap boundary conditions" {
}
}
+test "Terminal: eraseChars clearing wrapped wide char marks spacer head row dirty" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 5 });
+ defer t.deinit(alloc);
+
+ // The wide char doesn't fit so it wraps, leaving a spacer head at
+ // the end of the first row.
+ try t.printString("ABCD字");
+ {
+ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
+ try testing.expectEqual(Cell.Wide.spacer_head, list_cell.cell.wide);
+ try testing.expect(list_cell.row.wrap);
+ }
+
+ t.setCursorPos(2, 1);
+ t.clearDirty();
+ t.eraseChars(1);
+ t.screens.active.cursor.page_pin.node.page().assertIntegrity();
+
+ // Erasing the wide char also clears the spacer head on the previous
+ // row, so that row must be dirty too.
+ try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
+ try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));
+ try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 2 } }));
+
+ {
+ const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
+ try testing.expectEqual(Cell.Wide.narrow, list_cell.cell.wide);
+ }
+}
+
test "Terminal: reverseIndex" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// Initial value
@@ -8489,7 +9985,8 @@ test "Terminal: reverseIndex" {
test "Terminal: reverseIndex from the top" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -8522,7 +10019,8 @@ test "Terminal: reverseIndex from the top" {
test "Terminal: reverseIndex top of scrolling region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 10 });
defer t.deinit(alloc);
// Initial value
@@ -8555,7 +10053,8 @@ test "Terminal: reverseIndex top of scrolling region" {
test "Terminal: reverseIndex top of screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -8576,7 +10075,8 @@ test "Terminal: reverseIndex top of screen" {
test "Terminal: reverseIndex not top of screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -8597,7 +10097,8 @@ test "Terminal: reverseIndex not top of screen" {
test "Terminal: reverseIndex top/bottom margins" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -8618,7 +10119,8 @@ test "Terminal: reverseIndex top/bottom margins" {
test "Terminal: reverseIndex outside top/bottom margins" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -8639,7 +10141,8 @@ test "Terminal: reverseIndex outside top/bottom margins" {
test "Terminal: reverseIndex left/right margins" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -8661,7 +10164,8 @@ test "Terminal: reverseIndex left/right margins" {
test "Terminal: reverseIndex outside left/right margins" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -8683,7 +10187,8 @@ test "Terminal: reverseIndex outside left/right margins" {
test "Terminal: index" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
try t.index();
@@ -8701,7 +10206,8 @@ test "Terminal: index" {
test "Terminal: index from the bottom" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
@@ -8724,7 +10230,8 @@ test "Terminal: index from the bottom" {
test "Terminal: index scrolling with hyperlink" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
@@ -8769,7 +10276,8 @@ test "Terminal: index scrolling with hyperlink" {
test "Terminal: index outside of scrolling region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
@@ -8780,7 +10288,8 @@ test "Terminal: index outside of scrolling region" {
test "Terminal: index from the bottom outside of scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 2);
@@ -8800,7 +10309,8 @@ test "Terminal: index from the bottom outside of scroll region" {
test "Terminal: index no scroll region, top of screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -8820,7 +10330,8 @@ test "Terminal: index no scroll region, top of screen" {
test "Terminal: index bottom of primary screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
@@ -8841,7 +10352,8 @@ test "Terminal: index bottom of primary screen" {
test "Terminal: index bottom of primary screen background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
@@ -8874,7 +10386,8 @@ test "Terminal: index bottom of primary screen background sgr" {
test "Terminal: index inside scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -8895,7 +10408,8 @@ test "Terminal: index inside scroll region" {
test "Terminal: index bottom of scroll region with hyperlinks" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 2);
@@ -8943,7 +10457,8 @@ test "Terminal: index bottom of scroll region with hyperlinks" {
test "Terminal: index bottom of scroll region clear hyperlinks" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5, .max_scrollback_bytes = 0 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(2, 3);
@@ -8982,7 +10497,8 @@ test "Terminal: index bottom of scroll region clear hyperlinks" {
test "Terminal: index bottom of scroll region with background SGR" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9019,7 +10535,8 @@ test "Terminal: index bottom of scroll region with background SGR" {
test "Terminal: index bottom of primary screen with scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9047,7 +10564,8 @@ test "Terminal: index bottom of primary screen with scroll region" {
test "Terminal: index outside left/right margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9071,7 +10589,8 @@ test "Terminal: index outside left/right margin" {
test "Terminal: index inside left/right margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.printString("AAAAAA");
@@ -9105,7 +10624,8 @@ test "Terminal: index inside left/right margin" {
test "Terminal: index bottom of scroll region creates scrollback" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9130,7 +10650,8 @@ test "Terminal: index bottom of scroll region creates scrollback" {
test "Terminal: index bottom of scroll region no scrollback" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5, .max_scrollback_bytes = 0 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9151,7 +10672,8 @@ test "Terminal: index bottom of scroll region no scrollback" {
test "Terminal: index bottom of scroll region blank line preserves SGR" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9192,7 +10714,8 @@ test "Terminal: index bottom of scroll region blank line preserves SGR" {
test "Terminal: index bottom of scroll region with top margin and background SGR" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("1\n2\n3\n4\n5");
@@ -9232,7 +10755,8 @@ test "Terminal: index bottom of scroll region with top margin and background SGR
test "Terminal: index bottom of alt screen full region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 3, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 5 });
defer t.deinit(alloc);
try t.switchScreenMode(.@"1049", true);
@@ -9265,7 +10789,8 @@ test "Terminal: index bottom of alt screen full region" {
test "Terminal: index bottom of alt screen top region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.switchScreenMode(.@"1049", true);
@@ -9292,7 +10817,8 @@ test "Terminal: index bottom of alt screen top region" {
test "Terminal: scrollUp top region no scrollback" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5, .max_scrollback_bytes = 0 });
defer t.deinit(alloc);
try t.printString("A\nB\nC\nD\nE");
@@ -9314,7 +10840,8 @@ test "Terminal: scrollUp top region no scrollback" {
test "Terminal: cursorUp basic" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(3, 1);
@@ -9331,7 +10858,8 @@ test "Terminal: cursorUp basic" {
test "Terminal: cursorUp below top scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(2, 4);
@@ -9349,7 +10877,8 @@ test "Terminal: cursorUp below top scroll margin" {
test "Terminal: cursorUp above top scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(3, 5);
@@ -9368,7 +10897,8 @@ test "Terminal: cursorUp above top scroll margin" {
test "Terminal: cursorUp resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -9386,7 +10916,8 @@ test "Terminal: cursorUp resets wrap" {
test "Terminal: cursorLeft no wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -9404,7 +10935,8 @@ test "Terminal: cursorLeft no wrap" {
test "Terminal: cursorLeft unsets pending wrap state" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -9422,7 +10954,8 @@ test "Terminal: cursorLeft unsets pending wrap state" {
test "Terminal: cursorLeft unsets pending wrap state with longer jump" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -9440,7 +10973,8 @@ test "Terminal: cursorLeft unsets pending wrap state with longer jump" {
test "Terminal: cursorLeft reverse wrap with pending wrap state" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9461,7 +10995,8 @@ test "Terminal: cursorLeft reverse wrap with pending wrap state" {
test "Terminal: cursorLeft reverse wrap extended with pending wrap state" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9482,7 +11017,8 @@ test "Terminal: cursorLeft reverse wrap extended with pending wrap state" {
test "Terminal: cursorLeft reverse wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9502,7 +11038,8 @@ test "Terminal: cursorLeft reverse wrap" {
test "Terminal: cursorLeft reverse wrap with no soft wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9524,7 +11061,8 @@ test "Terminal: cursorLeft reverse wrap with no soft wrap" {
test "Terminal: cursorLeft reverse wrap before left margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9542,7 +11080,8 @@ test "Terminal: cursorLeft reverse wrap before left margin" {
test "Terminal: cursorLeft extended reverse wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9564,7 +11103,8 @@ test "Terminal: cursorLeft extended reverse wrap" {
test "Terminal: cursorLeft extended reverse wrap bottom wraparound" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9586,7 +11126,8 @@ test "Terminal: cursorLeft extended reverse wrap bottom wraparound" {
test "Terminal: cursorLeft extended reverse wrap is priority if both set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9609,7 +11150,8 @@ test "Terminal: cursorLeft extended reverse wrap is priority if both set" {
test "Terminal: cursorLeft extended reverse wrap above top scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9625,7 +11167,8 @@ test "Terminal: cursorLeft extended reverse wrap above top scroll region" {
test "Terminal: cursorLeft reverse wrap on first row" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -9641,7 +11184,8 @@ test "Terminal: cursorLeft reverse wrap on first row" {
test "Terminal: cursorDown basic" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -9657,7 +11201,8 @@ test "Terminal: cursorDown basic" {
test "Terminal: cursorDown above bottom scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9674,7 +11219,8 @@ test "Terminal: cursorDown above bottom scroll margin" {
test "Terminal: cursorDown below bottom scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
@@ -9692,7 +11238,8 @@ test "Terminal: cursorDown below bottom scroll margin" {
test "Terminal: cursorDown resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -9710,7 +11257,8 @@ test "Terminal: cursorDown resets wrap" {
test "Terminal: cursorRight resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -9728,7 +11276,8 @@ test "Terminal: cursorRight resets wrap" {
test "Terminal: cursorRight to the edge of screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.cursorRight(100);
@@ -9743,7 +11292,8 @@ test "Terminal: cursorRight to the edge of screen" {
test "Terminal: cursorRight left of right margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.right = 2;
@@ -9759,7 +11309,8 @@ test "Terminal: cursorRight left of right margin" {
test "Terminal: cursorRight right of right margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.right = 2;
@@ -9776,7 +11327,8 @@ test "Terminal: cursorRight right of right margin" {
test "Terminal: deleteLines simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -9808,7 +11360,8 @@ test "Terminal: deleteLines simple" {
test "Terminal: deleteLines colors with bg color" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
@@ -9849,7 +11402,8 @@ test "Terminal: deleteLines colors with bg color" {
test "Terminal: deleteLines across page boundary marks all shifted rows dirty" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 10, .max_scrollback_bytes = 1024 });
defer t.deinit(alloc);
const first_page = t.screens.active.pages.pages.first.?;
@@ -9897,9 +11451,101 @@ test "Terminal: deleteLines across page boundary marks all shifted rows dirty" {
}
}
+test "Terminal: deleteLines hyperlink-dense row crosses page boundary" {
+ // Regression test for the cross-page copy of deleteLines: when the
+ // shifted row carries more unique hyperlinks than the destination
+ // page's hyperlink capacity, the copy must increase the destination
+ // page's capacity and retry rather than corrupting the page list.
+ //
+ // This is the mirror of the insertLines variant: the dense row
+ // starts as the first row of the second page and is pulled up into
+ // the first page, which also happens to be the cursor's page so
+ // this exercises the cursor accounting of the capacity increase.
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 10, .max_scrollback_bytes = 1024 });
+ defer t.deinit(alloc);
+
+ const pages = &t.screens.active.pages;
+
+ // Fill the first page so it is exactly full, then two more rows so
+ // the second page holds the last two active rows (y=3 and y=4).
+ const first_page_rows = pages.pages.first.?.capacity().rows;
+ for (0..first_page_rows + 1) |_| try t.linefeed();
+ try testing.expect(pages.pages.first != pages.pages.last);
+ try testing.expectEqual(@as(usize, 2), pages.pages.last.?.rows());
+
+ // Marker rows so we can verify the shift afterwards.
+ t.setCursorPos(1, 1);
+ try t.printString("0");
+ t.setCursorPos(2, 1);
+ try t.printString("1");
+ t.setCursorPos(3, 1);
+ try t.printString("2");
+ t.setCursorPos(5, 1);
+ try t.printString("4");
+
+ // Fill the first row of the second page (active y=3) with unique
+ // hyperlinks: more than the first page can hold with its default
+ // hyperlink capacity. Writing them grows the second page's
+ // capacity as needed; the first page keeps its default capacity.
+ t.setCursorPos(4, 1);
+ for (0..10) |i| {
+ var buf: [64]u8 = undefined;
+ const uri = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{i});
+ try t.screens.active.startHyperlink(uri, null);
+ try t.print(@intCast('A' + i));
+ t.screens.active.endHyperlink();
+ }
+ {
+ const pin = pages.pin(.{ .active = .{ .y = 3 } }).?;
+ try testing.expectEqual(pages.pages.last.?, pin.node);
+ try testing.expectEqual(@as(usize, 0), @as(usize, pin.y));
+ }
+ try testing.expect(pages.pages.first.?.page().hyperlink_set.layout.cap < 10);
+
+ // Delete the top line: every row shifts up by one and the dense
+ // row crosses the page boundary into the first page.
+ t.setCursorPos(1, 1);
+ t.deleteLines(1);
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("1\n2\nABCDEFGHIJ\n4", str);
+ }
+
+ // The first page's hyperlink capacity had to grow to receive the
+ // row, proving the capacity-retry path ran.
+ try testing.expect(pages.pages.first.?.page().hyperlink_set.layout.cap >= 10);
+
+ // Every cell of the dense row must still resolve to a real
+ // hyperlink entry with the correct URI. A half-applied shift
+ // leaves cells whose hyperlink flag is set but that have no map
+ // entry, which aborts in clearCells later.
+ for (0..10) |x| {
+ const list_cell = pages.getCell(.{ .active = .{
+ .x = @intCast(x),
+ .y = 2,
+ } }).?;
+ 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 uri = try std.fmt.bufPrint(&buf, "http://example.com/{d}", .{x});
+ try testing.expectEqualStrings(uri, link.uri.slice(page.memory));
+ }
+
+ // All pages must pass integrity checks.
+ var node_: ?*PageList.List.Node = pages.pages.first;
+ while (node_) |node| : (node_ = node.next) node.page().assertIntegrity();
+}
+
test "Terminal: deleteLines (legacy)" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 80, .rows = 80 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
@@ -9934,7 +11580,8 @@ test "Terminal: deleteLines (legacy)" {
test "Terminal: deleteLines with scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 80, .rows = 80 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
@@ -9977,7 +11624,8 @@ test "Terminal: deleteLines with scroll region" {
test "Terminal: deleteLines with scroll region, large count" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 80, .rows = 80 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
@@ -10020,7 +11668,8 @@ test "Terminal: deleteLines with scroll region, large count" {
test "Terminal: deleteLines with scroll region, cursor outside of region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 80, .rows = 80 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
@@ -10055,7 +11704,8 @@ test "Terminal: deleteLines with scroll region, cursor outside of region" {
test "Terminal: deleteLines resets pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -10073,7 +11723,8 @@ test "Terminal: deleteLines resets pending wrap" {
test "Terminal: deleteLines resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 3, .cols = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 3 });
defer t.deinit(alloc);
try t.print('1');
@@ -10104,7 +11755,8 @@ test "Terminal: deleteLines resets wrap" {
test "Terminal: deleteLines left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -10136,7 +11788,8 @@ test "Terminal: deleteLines left/right scroll region" {
test "Terminal: deleteLines left/right scroll region from top" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -10167,7 +11820,8 @@ test "Terminal: deleteLines left/right scroll region from top" {
test "Terminal: deleteLines left/right scroll region high count" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -10199,7 +11853,8 @@ test "Terminal: deleteLines left/right scroll region high count" {
test "Terminal: deleteLines wide character spacer head" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10235,7 +11890,8 @@ test "Terminal: deleteLines wide character spacer head" {
test "Terminal: deleteLines wide character spacer head left scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10276,7 +11932,8 @@ test "Terminal: deleteLines wide character spacer head left scroll margin" {
test "Terminal: deleteLines wide character spacer head right scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10317,7 +11974,8 @@ test "Terminal: deleteLines wide character spacer head right scroll margin" {
test "Terminal: deleteLines wide character spacer head left and right scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10359,7 +12017,8 @@ test "Terminal: deleteLines wide character spacer head left and right scroll mar
test "Terminal: deleteLines wide character spacer head left (< 2) and right scroll margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10401,7 +12060,8 @@ test "Terminal: deleteLines wide character spacer head left (< 2) and right scro
test "Terminal: deleteLines wide characters split by left/right scroll region boundaries" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
// Initial value
@@ -10436,7 +12096,8 @@ test "Terminal: deleteLines wide characters split by left/right scroll region bo
test "Terminal: deleteLines zero" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// This should do nothing
@@ -10446,7 +12107,8 @@ test "Terminal: deleteLines zero" {
test "Terminal: default style is empty" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -10454,14 +12116,15 @@ test "Terminal: default style is empty" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint.data);
try testing.expectEqual(@as(style.Id, 0), cell.style_id);
}
}
test "Terminal: bold style" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
@@ -10470,7 +12133,7 @@ test "Terminal: bold style" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint.data);
try testing.expect(cell.style_id != 0);
const page = t.screens.active.cursor.page_pin.node.page();
try testing.expect(page.styles.refCount(page.memory, t.screens.active.cursor.style_id) > 1);
@@ -10479,7 +12142,8 @@ test "Terminal: bold style" {
test "Terminal: garbage collect overwritten" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
@@ -10491,7 +12155,7 @@ test "Terminal: garbage collect overwritten" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint.data);
try testing.expect(cell.style_id == 0);
}
@@ -10502,7 +12166,8 @@ test "Terminal: garbage collect overwritten" {
test "Terminal: do not garbage collect old styles in use" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
@@ -10513,7 +12178,7 @@ test "Terminal: do not garbage collect old styles in use" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint.data);
try testing.expect(cell.style_id == 0);
}
@@ -10524,7 +12189,8 @@ test "Terminal: do not garbage collect old styles in use" {
test "Terminal: print with style marks the row as styled" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
@@ -10540,7 +12206,8 @@ test "Terminal: print with style marks the row as styled" {
test "Terminal: DECALN" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 2 });
defer t.deinit(alloc);
// Initial value
@@ -10567,7 +12234,8 @@ test "Terminal: DECALN" {
test "Terminal: decaln reset margins" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10585,7 +12253,8 @@ test "Terminal: decaln reset margins" {
test "Terminal: decaln preserves color" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
// Initial value
@@ -10614,7 +12283,8 @@ test "Terminal: decaln preserves color" {
test "Terminal: DECALN resets graphemes with protected mode" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
// Add protected mode. A previous version of DECALN accidentally preserved
@@ -10650,7 +12320,8 @@ test "Terminal: DECALN resets graphemes with protected mode" {
test "Terminal: insertBlanks zero" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
try t.print('A');
@@ -10671,7 +12342,8 @@ test "Terminal: insertBlanks" {
// NOTE: this is not verified with conformance tests, so these
// tests might actually be verifying wrong behavior.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
try t.print('A');
@@ -10695,7 +12367,8 @@ test "Terminal: insertBlanks pushes off end" {
// NOTE: this is not verified with conformance tests, so these
// tests might actually be verifying wrong behavior.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 2 });
defer t.deinit(alloc);
try t.print('A');
@@ -10718,7 +12391,8 @@ test "Terminal: insertBlanks more than size" {
// NOTE: this is not verified with conformance tests, so these
// tests might actually be verifying wrong behavior.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 2 });
defer t.deinit(alloc);
try t.print('A');
@@ -10739,7 +12413,8 @@ test "Terminal: insertBlanks more than size" {
test "Terminal: insertBlanks no scroll region, fits" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -10758,7 +12433,8 @@ test "Terminal: insertBlanks no scroll region, fits" {
test "Terminal: insertBlanks preserves background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -10788,7 +12464,8 @@ test "Terminal: insertBlanks preserves background sgr" {
test "Terminal: insertBlanks shift off screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
for (" ABC") |c| try t.print(c);
@@ -10807,7 +12484,8 @@ test "Terminal: insertBlanks shift off screen" {
test "Terminal: insertBlanks split multi-cell character" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
for ("123") |c| try t.print(c);
@@ -10826,7 +12504,8 @@ test "Terminal: insertBlanks split multi-cell character" {
test "Terminal: insertBlanks inside left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
t.scrolling_region.left = 2;
@@ -10849,7 +12528,8 @@ test "Terminal: insertBlanks inside left/right scroll region" {
test "Terminal: insertBlanks outside left/right scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 6, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
t.setCursorPos(1, 4);
@@ -10872,7 +12552,8 @@ test "Terminal: insertBlanks outside left/right scroll region" {
test "Terminal: insertBlanks left/right scroll region large count" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
@@ -10893,7 +12574,8 @@ test "Terminal: insertBlanks left/right scroll region large count" {
test "Terminal: insertBlanks deleting graphemes" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Disable grapheme clustering
@@ -10929,7 +12611,8 @@ test "Terminal: insertBlanks deleting graphemes" {
test "Terminal: insertBlanks shift graphemes" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Enable grapheme clustering
@@ -10965,7 +12648,8 @@ test "Terminal: insertBlanks shift graphemes" {
test "Terminal: insertBlanks split multi-cell character from tail" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
try t.printString("橋123");
@@ -10989,7 +12673,8 @@ test "Terminal: insertBlanks shifts hyperlinks" {
// link should be preserved, blanks should not be linked
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 2 });
defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -11030,7 +12715,8 @@ test "Terminal: insertBlanks shifts hyperlinks" {
test "Terminal: insertBlanks pushes hyperlink off end completely" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 2 });
defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -11066,7 +12752,8 @@ test "Terminal: insertBlanks wide char straddling right margin" {
// away via swapCells but leaves the orphaned spacer_tail in place,
// causing a page integrity violation.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Fill row: A B C D 橋 _ _ _ _ _
@@ -11101,7 +12788,8 @@ test "Terminal: insertBlanks wide char spacer_tail orphaned beyond right margin"
// is left behind, causing a page integrity violation:
// "spacer tail not following wide"
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Fill cols 0–9 with wide chars: 中中中中中
@@ -11131,7 +12819,8 @@ test "Terminal: insertBlanks wide char spacer_tail orphaned beyond right margin"
test "Terminal: insert mode with space" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
@@ -11148,7 +12837,8 @@ test "Terminal: insert mode with space" {
test "Terminal: insert mode doesn't wrap pushed characters" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
@@ -11165,7 +12855,8 @@ test "Terminal: insert mode doesn't wrap pushed characters" {
test "Terminal: insert mode does nothing at the end of the line" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
@@ -11181,7 +12872,8 @@ test "Terminal: insert mode does nothing at the end of the line" {
test "Terminal: insert mode with wide characters" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
@@ -11198,7 +12890,8 @@ test "Terminal: insert mode with wide characters" {
test "Terminal: insert mode with wide characters at end" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("well") |c| try t.print(c);
@@ -11214,7 +12907,8 @@ test "Terminal: insert mode with wide characters at end" {
test "Terminal: insert mode pushing off wide character" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("123") |c| try t.print(c);
@@ -11232,7 +12926,8 @@ test "Terminal: insert mode pushing off wide character" {
test "Terminal: deleteChars" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11251,7 +12946,8 @@ test "Terminal: deleteChars" {
test "Terminal: deleteChars zero count" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11270,7 +12966,8 @@ test "Terminal: deleteChars zero count" {
test "Terminal: deleteChars more than half" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11289,7 +12986,8 @@ test "Terminal: deleteChars more than half" {
test "Terminal: deleteChars more than line width" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11308,7 +13006,8 @@ test "Terminal: deleteChars more than line width" {
test "Terminal: deleteChars should shift left" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11327,7 +13026,8 @@ test "Terminal: deleteChars should shift left" {
test "Terminal: deleteChars resets pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11345,7 +13045,8 @@ test "Terminal: deleteChars resets pending wrap" {
test "Terminal: deleteChars resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c);
@@ -11374,7 +13075,8 @@ test "Terminal: deleteChars resets wrap" {
test "Terminal: deleteChars simple operation" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -11393,7 +13095,8 @@ test "Terminal: deleteChars simple operation" {
test "Terminal: deleteChars preserves background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC123") |c| try t.print(c);
@@ -11426,7 +13129,8 @@ test "Terminal: deleteChars preserves background sgr" {
test "Terminal: deleteChars outside scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 6, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -11447,7 +13151,8 @@ test "Terminal: deleteChars outside scroll region" {
test "Terminal: deleteChars inside scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 6, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
@@ -11468,7 +13173,8 @@ test "Terminal: deleteChars inside scroll region" {
test "Terminal: deleteChars split wide character from spacer tail" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 6, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("A橋123");
@@ -11484,7 +13190,8 @@ test "Terminal: deleteChars split wide character from spacer tail" {
test "Terminal: deleteChars split wide character from wide" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 6, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("橋123");
@@ -11494,20 +13201,21 @@ test "Terminal: deleteChars split wide character from wide" {
{
const list_cell = t.screens.active.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);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, '1'), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, '1'), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: deleteChars split wide character from end" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 6, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("A橋123");
@@ -11517,20 +13225,21 @@ test "Terminal: deleteChars split wide character from end" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u21, 0x6A4B), cell.content.codepoint);
+ try testing.expectEqual(@as(u21, 0x6A4B), cell.content.codepoint.data);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .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.spacer_tail, cell.wide);
}
}
test "Terminal: deleteChars with a spacer head at the end" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 10 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
try t.printString("0123橋123");
@@ -11548,14 +13257,15 @@ test "Terminal: deleteChars with a spacer head at the end" {
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 3, .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);
}
}
test "Terminal: deleteChars split wide character tail" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, t.cols - 1);
@@ -11573,7 +13283,8 @@ test "Terminal: deleteChars split wide character tail" {
test "Terminal: deleteChars wide char boundary conditions" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 1, .cols = 8 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 1, .cols = 8 });
defer t.deinit(alloc);
// EXPLANATION(qwerasd):
@@ -11637,7 +13348,8 @@ test "Terminal: deleteChars wide char boundary conditions" {
test "Terminal: deleteChars wide char wrap boundary conditions" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 3, .cols = 8 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 8 });
defer t.deinit(alloc);
// EXPLANATION(qwerasd):
@@ -11677,9 +13389,16 @@ test "Terminal: deleteChars wide char wrap boundary conditions" {
}
t.setCursorPos(2, 2);
+ t.clearDirty();
t.deleteChars(3);
t.screens.active.cursor.page_pin.node.page().assertIntegrity();
+ // Deleting the wide char also clears the spacer head on the previous
+ // row, so that row must be dirty too.
+ try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
+ try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));
+ try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 2 } }));
+
{
const str = try t.plainString(alloc);
defer testing.allocator.free(str);
@@ -11693,7 +13412,8 @@ test "Terminal: deleteChars wide char wrap boundary conditions" {
test "Terminal: deleteChars wide char across right margin" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 3, .cols = 8 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 3, .cols = 8 });
defer t.deinit(alloc);
// scroll region
@@ -11736,7 +13456,8 @@ test "Terminal: deleteChars wide char across right margin" {
test "Terminal: saveCursor" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
@@ -11754,7 +13475,8 @@ test "Terminal: saveCursor" {
test "Terminal: saveCursor position" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
@@ -11774,7 +13496,8 @@ test "Terminal: saveCursor position" {
test "Terminal: saveCursor pending wrap state" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
@@ -11794,7 +13517,8 @@ test "Terminal: saveCursor pending wrap state" {
test "Terminal: saveCursor origin mode" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
@@ -11814,12 +13538,13 @@ test "Terminal: saveCursor origin mode" {
test "Terminal: saveCursor resize" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 10);
t.saveCursor();
- try t.resize(alloc, 5, 5);
+ try t.resize(alloc, .{ .cols = 5, .rows = 5 });
t.restoreCursor();
try t.print('X');
@@ -11832,7 +13557,8 @@ test "Terminal: saveCursor resize" {
test "Terminal: saveCursor protected pen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -11847,7 +13573,8 @@ test "Terminal: saveCursor protected pen" {
test "Terminal: saveCursor doesn't modify hyperlink state" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -11863,9 +13590,10 @@ test "Terminal: restoreCursor uses default style on OutOfSpace" {
// manualStyleUpdate fails with OutOfSpace (can't split a 1-row page
// and styles are at max capacity).
const alloc = testing.allocator;
+ const io_impl = testing.io;
// Use a single row so the page can't be split
- var t = try init(alloc, .{ .cols = 10, .rows = 1 });
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 1 });
defer t.deinit(alloc);
// Set a style and save the cursor
@@ -11917,7 +13645,8 @@ test "Terminal: restoreCursor uses default style on OutOfSpace" {
test "Terminal: setProtectedMode" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try testing.expect(!t.screens.active.cursor.protected);
@@ -11933,7 +13662,8 @@ test "Terminal: setProtectedMode" {
test "Terminal: eraseLine simple erase right" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11951,7 +13681,8 @@ test "Terminal: eraseLine simple erase right" {
test "Terminal: eraseLine resets pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -11969,7 +13700,8 @@ test "Terminal: eraseLine resets pending wrap" {
test "Terminal: eraseLine resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c);
@@ -11996,7 +13728,8 @@ test "Terminal: eraseLine resets wrap" {
test "Terminal: eraseLine right preserves background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -12029,7 +13762,8 @@ test "Terminal: eraseLine right preserves background sgr" {
test "Terminal: eraseLine right wide character" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("AB") |c| try t.print(c);
@@ -12049,7 +13783,8 @@ test "Terminal: eraseLine right wide character" {
test "Terminal: eraseLine right protected attributes respected with iso" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12068,7 +13803,8 @@ test "Terminal: eraseLine right protected attributes respected with iso" {
test "Terminal: eraseLine right protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12089,7 +13825,8 @@ test "Terminal: eraseLine right protected attributes ignored with dec most recen
test "Terminal: eraseLine right protected attributes ignored with dec set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -12108,7 +13845,8 @@ test "Terminal: eraseLine right protected attributes ignored with dec set" {
test "Terminal: eraseLine right protected requested" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("12345678") |c| try t.print(c);
@@ -12129,7 +13867,8 @@ test "Terminal: eraseLine right protected requested" {
test "Terminal: eraseLine simple erase left" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -12147,7 +13886,8 @@ test "Terminal: eraseLine simple erase left" {
test "Terminal: eraseLine left resets wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -12167,7 +13907,8 @@ test "Terminal: eraseLine left resets wrap" {
test "Terminal: eraseLine left preserves background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -12200,7 +13941,8 @@ test "Terminal: eraseLine left preserves background sgr" {
test "Terminal: eraseLine left wide character" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("AB") |c| try t.print(c);
@@ -12220,7 +13962,8 @@ test "Terminal: eraseLine left wide character" {
test "Terminal: eraseLine left protected attributes respected with iso" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12239,7 +13982,8 @@ test "Terminal: eraseLine left protected attributes respected with iso" {
test "Terminal: eraseLine left protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12260,7 +14004,8 @@ test "Terminal: eraseLine left protected attributes ignored with dec most recent
test "Terminal: eraseLine left protected attributes ignored with dec set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -12279,7 +14024,8 @@ test "Terminal: eraseLine left protected attributes ignored with dec set" {
test "Terminal: eraseLine left protected requested" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("123456789") |c| try t.print(c);
@@ -12300,7 +14046,8 @@ test "Terminal: eraseLine left protected requested" {
test "Terminal: eraseLine complete preserves background sgr" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -12331,9 +14078,39 @@ test "Terminal: eraseLine complete preserves background sgr" {
}
}
+test "Terminal: eraseLine complete resets wrap" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
+ defer t.deinit(alloc);
+
+ for ("ABCDE123") |c| try t.print(c);
+ {
+ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
+ try testing.expect(list_cell.row.wrap);
+ }
+
+ t.setCursorPos(1, 1);
+ t.eraseLine(.complete, false);
+
+ {
+ const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
+ try testing.expect(!list_cell.row.wrap);
+ }
+ try t.print('X');
+ try t.resize(alloc, .{ .rows = 5, .cols = 10 });
+
+ {
+ const str = try t.plainString(testing.allocator);
+ defer testing.allocator.free(str);
+ try testing.expectEqualStrings("X\n123", str);
+ }
+}
+
test "Terminal: eraseLine complete protected attributes respected with iso" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12352,7 +14129,8 @@ test "Terminal: eraseLine complete protected attributes respected with iso" {
test "Terminal: eraseLine complete protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12373,7 +14151,8 @@ test "Terminal: eraseLine complete protected attributes ignored with dec most re
test "Terminal: eraseLine complete protected attributes ignored with dec set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -12392,7 +14171,8 @@ test "Terminal: eraseLine complete protected attributes ignored with dec set" {
test "Terminal: eraseLine complete protected requested" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("123456789") |c| try t.print(c);
@@ -12413,7 +14193,8 @@ test "Terminal: eraseLine complete protected requested" {
test "Terminal: tabClear single" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 30, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 30, .rows = 5 });
defer t.deinit(alloc);
t.horizontalTab();
@@ -12426,7 +14207,8 @@ test "Terminal: tabClear single" {
test "Terminal: tabClear all" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 30, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 30, .rows = 5 });
defer t.deinit(alloc);
t.tabClear(.all);
@@ -12438,7 +14220,8 @@ test "Terminal: tabClear all" {
test "Terminal: printRepeat simple" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("A");
@@ -12454,7 +14237,8 @@ test "Terminal: printRepeat simple" {
test "Terminal: printRepeat wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString(" A");
@@ -12470,7 +14254,8 @@ test "Terminal: printRepeat wrap" {
test "Terminal: printRepeat no previous character" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printRepeat(1);
@@ -12485,7 +14270,8 @@ test "Terminal: printRepeat no previous character" {
test "Terminal: printSlice simple ascii" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
defer t.deinit(alloc);
try t.printSlice(&.{ 'h', 'e', 'l', 'l', 'o' });
@@ -12502,7 +14288,8 @@ test "Terminal: printSlice simple ascii" {
test "Terminal: printSlice wraps and scrolls" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
// 12 chars: fills row 1 (5), row 2 (5), wraps+scrolls, 2 more.
@@ -12519,7 +14306,8 @@ test "Terminal: printSlice wraps and scrolls" {
test "Terminal: printSlice pending wrap state" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 5, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
try t.printSlice(&.{ 'a', 'b', 'c', 'd', 'e' });
@@ -12538,18 +14326,19 @@ test "Terminal: printSlice pending wrap state" {
/// the other using printSlice() with random chunking, verifying that
/// the results are identical.
fn testPrintSliceDifferential(
+ io_impl: std.Io,
alloc: Allocator,
rand: std.Random,
ops: usize,
cols: size.CellCountInt,
rows: size.CellCountInt,
) !void {
- var t1 = try init(alloc, .{
+ var t1 = try init(io_impl, alloc, .{
.cols = cols,
.rows = rows,
});
defer t1.deinit(alloc);
- var t2 = try init(alloc, .{
+ var t2 = try init(io_impl, alloc, .{
.cols = cols,
.rows = rows,
});
@@ -12558,11 +14347,12 @@ fn testPrintSliceDifferential(
// Alphabet of interesting codepoints: ascii, latin-1, combining
// marks, CJK (wide), emoji (wide), ZWJ, variation selectors.
const alphabet = [_]u21{
- 'a', 'b', 'Z', '0', ' ', 0x10, 0x1F, 0x7F,
- 'é', 0xFF, 0x301, 0x4E00, 0x4E01, 0x1F600, 0x200D, 0xFE0F,
- 'x', 'y', 0x1F9D1, 0x0308, 0xAD, 0x3042, 0xAC00, 'q',
- 'r', 's', 't', 'u', 'v', 'w', '1', '2',
- 0x1F1E6, 0x1F1E7, 0x1100, 0x1161, 0x11A8, 0x200C, 0x0430, 0x03B1,
+ 'a', 'b', 'Z', '0', ' ', 0x10, 0x1F, 0x7F,
+ 'é',
+ 0xFF, 0x301, 0x4E00, 0x4E01, 0x1F600, 0x200D, 0xFE0F, 'x',
+ 'y', 0x1F9D1, 0x0308, 0xAD, 0x3042, 0xAC00, 'q', 'r',
+ 's', 't', 'u', 'v', 'w', '1', '2', 0x1F1E6,
+ 0x1F1E7, 0x1100, 0x1161, 0x11A8, 0x200C, 0x0430, 0x03B1,
};
var cps_buf: [64]u32 = undefined;
@@ -12700,20 +14490,22 @@ fn testPrintSliceDifferential(
test "Terminal: printSlice differential fuzz vs print" {
const alloc = testing.allocator;
+ const io_impl = testing.io;
// Multiple seeds and terminal sizes for coverage, including a
// tiny terminal to stress wrap/scroll edge cases.
var prng = std.Random.DefaultPrng.init(0xC0FFEE);
const rand = prng.random();
- try testPrintSliceDifferential(alloc, rand, 500, 80, 24);
- try testPrintSliceDifferential(alloc, rand, 500, 10, 4);
- try testPrintSliceDifferential(alloc, rand, 500, 5, 2);
- try testPrintSliceDifferential(alloc, rand, 200, 2, 2);
+ try testPrintSliceDifferential(io_impl, alloc, rand, 500, 80, 24);
+ try testPrintSliceDifferential(io_impl, alloc, rand, 500, 10, 4);
+ try testPrintSliceDifferential(io_impl, alloc, rand, 500, 5, 2);
+ try testPrintSliceDifferential(io_impl, alloc, rand, 200, 2, 2);
}
test "Terminal: printAttributes" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
var storage: [64]u8 = undefined;
@@ -12742,20 +14534,31 @@ test "Terminal: printAttributes" {
try t.setAttribute(.inverse);
try t.setAttribute(.invisible);
try t.setAttribute(.strikethrough);
+ try t.setAttribute(.overline);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 100, .g = 200, .b = 255 } });
try t.setAttribute(.{ .direct_color_bg = .{ .r = 101, .g = 102, .b = 103 } });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
- try testing.expectEqualStrings("0;1;2;3;4;5;7;8;9;38:2::100:200:255;48:2::101:102:103", buf);
+ try testing.expectEqualStrings("0;1;2;3;4;53;5;7;8;9;38:2::100:200:255;48:2::101:102:103", buf);
}
- {
- try t.setAttribute(.{ .underline = .single });
- defer t.setAttribute(.unset) catch unreachable;
+ const Case = struct {
+ underline: sgr.Attribute.Underline,
+ expected: []const u8,
+ };
+ for ([_]Case{
+ .{ .underline = .single, .expected = "0;4" },
+ .{ .underline = .double, .expected = "0;4:2" },
+ .{ .underline = .curly, .expected = "0;4:3" },
+ .{ .underline = .dotted, .expected = "0;4:4" },
+ .{ .underline = .dashed, .expected = "0;4:5" },
+ }) |case| {
+ try t.setAttribute(.{ .underline = case.underline });
const buf = try t.printAttributes(&storage);
- try testing.expectEqualStrings("0;4", buf);
+ try testing.expectEqualStrings(case.expected, buf);
}
+ try t.setAttribute(.unset);
{
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0", buf);
@@ -12764,7 +14567,8 @@ test "Terminal: printAttributes" {
test "Terminal: eraseDisplay simple erase below" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -12792,7 +14596,8 @@ test "Terminal: eraseDisplay simple erase below" {
test "Terminal: eraseDisplay erase below preserves SGR bg" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -12832,7 +14637,8 @@ test "Terminal: eraseDisplay erase below preserves SGR bg" {
test "Terminal: eraseDisplay below split multi-cell" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("AB橋C");
@@ -12854,7 +14660,8 @@ test "Terminal: eraseDisplay below split multi-cell" {
test "Terminal: eraseDisplay below protected attributes respected with iso" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12877,7 +14684,8 @@ test "Terminal: eraseDisplay below protected attributes respected with iso" {
test "Terminal: eraseDisplay below protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -12902,7 +14710,8 @@ test "Terminal: eraseDisplay below protected attributes ignored with dec most re
test "Terminal: eraseDisplay below protected attributes ignored with dec set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -12925,7 +14734,8 @@ test "Terminal: eraseDisplay below protected attributes ignored with dec set" {
test "Terminal: eraseDisplay below protected attributes respected with force" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -12948,7 +14758,8 @@ test "Terminal: eraseDisplay below protected attributes respected with force" {
test "Terminal: eraseDisplay simple erase above" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -12975,7 +14786,8 @@ test "Terminal: eraseDisplay simple erase above" {
test "Terminal: eraseDisplay erase above preserves SGR bg" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
@@ -13015,7 +14827,8 @@ test "Terminal: eraseDisplay erase above preserves SGR bg" {
test "Terminal: eraseDisplay above split multi-cell" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("AB橋C");
@@ -13037,7 +14850,8 @@ test "Terminal: eraseDisplay above split multi-cell" {
test "Terminal: eraseDisplay above protected attributes respected with iso" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -13060,7 +14874,8 @@ test "Terminal: eraseDisplay above protected attributes respected with iso" {
test "Terminal: eraseDisplay above protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
@@ -13085,7 +14900,8 @@ test "Terminal: eraseDisplay above protected attributes ignored with dec most re
test "Terminal: eraseDisplay above protected attributes ignored with dec set" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -13108,7 +14924,8 @@ test "Terminal: eraseDisplay above protected attributes ignored with dec set" {
test "Terminal: eraseDisplay above protected attributes respected with force" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
@@ -13131,7 +14948,8 @@ test "Terminal: eraseDisplay above protected attributes respected with force" {
test "Terminal: eraseDisplay protected complete" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -13159,7 +14977,8 @@ test "Terminal: eraseDisplay protected complete" {
test "Terminal: eraseDisplay protected below" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -13181,7 +15000,8 @@ test "Terminal: eraseDisplay protected below" {
test "Terminal: eraseDisplay scroll complete" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
@@ -13198,7 +15018,8 @@ test "Terminal: eraseDisplay scroll complete" {
test "Terminal: eraseDisplay protected above" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
defer t.deinit(alloc);
try t.print('A');
@@ -13220,7 +15041,8 @@ test "Terminal: eraseDisplay protected above" {
test "Terminal: eraseDisplay complete preserves cursor" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Set our cursur
@@ -13237,7 +15059,8 @@ test "Terminal: eraseDisplay complete preserves cursor" {
test "Terminal: semantic prompt" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Prompt
@@ -13281,7 +15104,8 @@ test "Terminal: semantic prompt" {
test "Terminal: semantic prompt continuations" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Prompt
@@ -13331,7 +15155,8 @@ test "Terminal: index in prompt mode marks new row as prompt continuation" {
// a newline, assume the new row is a prompt continuation (since Fish
// doesn't emit OSC133 k=s markers for continuation lines).
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Start a prompt
@@ -13368,7 +15193,8 @@ test "Terminal: index in input mode does not mark new row as prompt" {
// Input mode should NOT trigger prompt continuation on newline
// (only prompt mode does, not input mode)
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Start a prompt then switch to input
@@ -13397,7 +15223,8 @@ test "Terminal: index in input mode does not mark new row as prompt" {
test "Terminal: index in output mode does not mark new row as prompt" {
// Output mode should NOT trigger prompt continuation
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Complete prompt cycle: prompt -> input -> output
@@ -13426,7 +15253,8 @@ test "Terminal: OSC133C at x=0 on prompt row clears prompt mark" {
// then immediately sends OSC133C (start output) at column 0, we
// should clear the prompt continuation mark we just set.
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Start a prompt
@@ -13463,7 +15291,8 @@ test "Terminal: OSC133C at x=0 on prompt row clears prompt mark" {
test "Terminal: OSC133C at x>0 on prompt row does not clear prompt mark" {
// If we're not at column 0, we shouldn't clear the prompt mark
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Start a prompt on a row
@@ -13505,7 +15334,8 @@ test "Terminal: OSC133C at x>0 on prompt row does not clear prompt mark" {
test "Terminal: multiple newlines in prompt mode marks all rows" {
// Multiple newlines should each mark their row as prompt continuation
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Start a prompt
@@ -13548,7 +15378,8 @@ test "Terminal: multiple newlines in prompt mode marks all rows" {
test "Terminal: OSC133A click_events=1 sets click to click_events" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Verify default state is none
@@ -13565,7 +15396,8 @@ test "Terminal: OSC133A click_events=1 sets click to click_events" {
test "Terminal: OSC133A click_events=2 sets click to click_events (relative)" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// Verify default state is none
@@ -13582,7 +15414,8 @@ test "Terminal: OSC133A click_events=2 sets click to click_events (relative)" {
test "Terminal: OSC133A click_events=0 does not set click_events" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// OSC 133;A with click_events=0
@@ -13597,7 +15430,8 @@ test "Terminal: OSC133A click_events=0 does not set click_events" {
test "Terminal: OSC133A cl option sets click to cl value" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// OSC 133;A with cl=m (multiple)
@@ -13611,7 +15445,8 @@ test "Terminal: OSC133A cl option sets click to cl value" {
test "Terminal: OSC133A cl=line sets click to line" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.semanticPrompt(.{
@@ -13624,7 +15459,8 @@ test "Terminal: OSC133A cl=line sets click to line" {
test "Terminal: OSC133A click_events=1 takes priority over cl" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// OSC 133;A with both click_events=1 and cl=m
@@ -13639,7 +15475,8 @@ test "Terminal: OSC133A click_events=1 takes priority over cl" {
test "Terminal: OSC133A click_events=0 falls back to cl" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// OSC 133;A with click_events=0 and cl=v
@@ -13654,7 +15491,8 @@ test "Terminal: OSC133A click_events=0 falls back to cl" {
test "Terminal: OSC133A no click options leaves click as none" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
// OSC 133;A with no click-related options
@@ -13668,7 +15506,8 @@ test "Terminal: OSC133A no click options leaves click as none" {
test "Terminal: cursorIsAtPrompt" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 10, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
defer t.deinit(alloc);
try testing.expect(!t.cursorIsAtPrompt());
@@ -13697,7 +15536,8 @@ test "Terminal: cursorIsAtPrompt" {
test "Terminal: cursorIsAtPrompt alternate screen" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 2 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 2 });
defer t.deinit(alloc);
try testing.expect(!t.cursorIsAtPrompt());
@@ -13711,8 +15551,66 @@ test "Terminal: cursorIsAtPrompt alternate screen" {
try testing.expect(!t.cursorIsAtPrompt());
}
+test "Terminal: cursor defaults update current default cursor" {
+ var t = try init(testing.io, testing.allocator, .{
+ .cols = 10,
+ .rows = 10,
+ .default_cursor_style = .bar,
+ .default_cursor_blink = true,
+ });
+ defer t.deinit(testing.allocator);
+
+ // Initialization applies the configured defaults.
+ try testing.expect(t.cursor.is_default);
+ try testing.expectEqual(.bar, t.screens.active.cursor.cursor_style);
+ try testing.expect(t.modes.get(.cursor_blinking));
+
+ // Configuration changes are immediately visible while the cursor still
+ // follows its defaults.
+ t.setDefaultCursorStyle(.underline);
+ t.setDefaultCursorBlink(false);
+ try testing.expect(t.cursor.is_default);
+ try testing.expectEqual(.underline, t.screens.active.cursor.cursor_style);
+ try testing.expect(!t.modes.get(.cursor_blinking));
+
+ // Null restores the terminal emulator's blinking default.
+ t.setDefaultCursorBlink(null);
+ try testing.expect(t.modes.get(.cursor_blinking));
+}
+
+test "Terminal: cursor defaults do not override explicit cursor" {
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
+ defer t.deinit(testing.allocator);
+
+ t.setCursorStyle(.blinking_bar);
+ try testing.expect(!t.cursor.is_default);
+ try testing.expectEqual(.bar, t.screens.active.cursor.cursor_style);
+ try testing.expect(t.modes.get(.cursor_blinking));
+
+ // New defaults are retained without replacing the explicit appearance.
+ t.setDefaultCursorStyle(.underline);
+ t.setDefaultCursorBlink(false);
+ try testing.expectEqual(.underline, t.cursor.default_style);
+ try testing.expectEqual(false, t.cursor.default_blink);
+ try testing.expectEqual(.bar, t.screens.active.cursor.cursor_style);
+ try testing.expect(t.modes.get(.cursor_blinking));
+
+ // Selecting the default applies the values that changed above.
+ t.setCursorStyle(.default);
+ try testing.expect(t.cursor.is_default);
+ try testing.expectEqual(.underline, t.screens.active.cursor.cursor_style);
+ try testing.expect(!t.modes.get(.cursor_blinking));
+
+ // A full reset also leaves the cursor on the configured defaults.
+ t.setCursorStyle(.steady_block);
+ t.fullReset();
+ try testing.expect(t.cursor.is_default);
+ try testing.expectEqual(.underline, t.screens.active.cursor.cursor_style);
+ try testing.expect(!t.modes.get(.cursor_blinking));
+}
+
test "Terminal: fullReset with a non-empty pen" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
@@ -13734,7 +15632,7 @@ test "Terminal: fullReset with a non-empty pen" {
}
test "Terminal: fullReset hyperlink" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.screens.active.startHyperlink("http://example.com", null);
@@ -13743,7 +15641,7 @@ test "Terminal: fullReset hyperlink" {
}
test "Terminal: fullReset with a non-empty saved cursor" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
@@ -13764,7 +15662,7 @@ test "Terminal: fullReset with a non-empty saved cursor" {
}
test "Terminal: fullReset origin mode" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
t.setCursorPos(3, 5);
@@ -13778,7 +15676,7 @@ test "Terminal: fullReset origin mode" {
}
test "Terminal: fullReset status display" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
t.status_display = .status_line;
@@ -13786,9 +15684,37 @@ test "Terminal: fullReset status display" {
try testing.expect(t.status_display == .main);
}
+test "Terminal: fullReset preserves kitty graphics limits" {
+ if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
+
+ const alloc = testing.allocator;
+ const temp_dir = "/tmp/ghostty-kitty-images";
+
+ var t = try init(testing.io, alloc, .{ .cols = 10, .rows = 10 });
+ defer t.deinit(alloc);
+
+ t.setKittyGraphicsLoadingLimits(.allWithTempDir(temp_dir));
+ for ([_]usize{ 1234, 0 }) |total_limit| {
+ t.setKittyGraphicsSizeLimit(alloc, total_limit);
+ t.fullReset();
+
+ const storage = &t.screens.active.kitty_images;
+ try testing.expectEqual(total_limit, storage.total_limit);
+ try testing.expect(storage.image_limits.file);
+ try testing.expect(storage.image_limits.shared_memory);
+ switch (storage.image_limits.temporary_file) {
+ .enabled => |value| try testing.expectEqualStrings(
+ temp_dir,
+ value.directory,
+ ),
+ .disabled => return error.TestUnexpectedResult,
+ }
+ }
+}
+
// https://github.com/mitchellh/ghostty/issues/1607
test "Terminal: fullReset clears alt screen kitty keyboard state" {
- var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
try t.switchScreenMode(.@"1049", true);
@@ -13806,7 +15732,7 @@ test "Terminal: fullReset clears alt screen kitty keyboard state" {
}
test "Terminal: fullReset default modes" {
- var t = try init(testing.allocator, .{
+ var t = try init(testing.io, testing.allocator, .{
.cols = 10,
.rows = 10,
.default_modes = .{ .grapheme_cluster = true },
@@ -13818,7 +15744,7 @@ test "Terminal: fullReset default modes" {
}
test "Terminal: fullReset tracked pins" {
- var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
+ var t = try init(testing.io, testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Create a tracked pin
@@ -13832,12 +15758,13 @@ test "Terminal: fullReset tracked pins" {
// this test around to ensure we don't regress at multiple layers.
test "Terminal: resize less cols with wide char then print" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 3, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try t.print('x');
try t.print('😀'); // 0x1F600
- try t.resize(alloc, 2, 3);
+ try t.resize(alloc, .{ .cols = 2, .rows = 3 });
t.setCursorPos(1, 2);
try t.print('😀'); // 0x1F600
}
@@ -13846,27 +15773,29 @@ test "Terminal: resize less cols with wide char then print" {
// This was found via fuzzing so its highly specific.
test "Terminal: resize with left and right margin set" {
const alloc = testing.allocator;
+ const io_impl = testing.io;
const cols = 70;
const rows = 23;
- var t = try init(alloc, .{ .cols = cols, .rows = rows });
+ var t = try init(io_impl, alloc, .{ .cols = cols, .rows = rows });
defer t.deinit(alloc);
t.modes.set(.enable_left_and_right_margin, true);
try t.print('0');
t.modes.set(.enable_mode_3, true);
- try t.resize(alloc, cols, rows);
+ try t.resize(alloc, .{ .cols = cols, .rows = rows });
t.setLeftAndRightMargin(2, 0);
try t.printRepeat(1850);
_ = t.modes.restore(.enable_mode_3);
- try t.resize(alloc, cols, rows);
+ try t.resize(alloc, .{ .cols = cols, .rows = rows });
}
// https://github.com/mitchellh/ghostty/issues/1343
test "Terminal: resize with wraparound off" {
const alloc = testing.allocator;
+ const io_impl = testing.io;
const cols = 4;
const rows = 2;
- var t = try init(alloc, .{ .cols = cols, .rows = rows });
+ var t = try init(io_impl, alloc, .{ .cols = cols, .rows = rows });
defer t.deinit(alloc);
t.modes.set(.wraparound, false);
@@ -13875,7 +15804,7 @@ test "Terminal: resize with wraparound off" {
try t.print('2');
try t.print('3');
const new_cols = 2;
- try t.resize(alloc, new_cols, rows);
+ try t.resize(alloc, .{ .cols = new_cols, .rows = rows });
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
@@ -13884,9 +15813,10 @@ test "Terminal: resize with wraparound off" {
test "Terminal: resize with wraparound on" {
const alloc = testing.allocator;
+ const io_impl = testing.io;
const cols = 4;
const rows = 2;
- var t = try init(alloc, .{ .cols = cols, .rows = rows });
+ var t = try init(io_impl, alloc, .{ .cols = cols, .rows = rows });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
@@ -13895,7 +15825,7 @@ test "Terminal: resize with wraparound on" {
try t.print('2');
try t.print('3');
const new_cols = 2;
- try t.resize(alloc, new_cols, rows);
+ try t.resize(alloc, .{ .cols = new_cols, .rows = rows });
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
@@ -13904,7 +15834,8 @@ test "Terminal: resize with wraparound on" {
test "Terminal: resize with high unique style per cell" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 30, .rows = 30 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
for (0..t.rows) |y| {
@@ -13919,12 +15850,13 @@ test "Terminal: resize with high unique style per cell" {
}
}
- try t.resize(alloc, 60, 30);
+ try t.resize(alloc, .{ .cols = 60, .rows = 30 });
}
test "Terminal: resize with high unique style per cell with wrapping" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 30, .rows = 30 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
const cell_count: u16 = @intCast(t.rows * t.cols);
@@ -13940,12 +15872,13 @@ test "Terminal: resize with high unique style per cell with wrapping" {
try t.print('x');
}
- try t.resize(alloc, 60, 30);
+ try t.resize(alloc, .{ .cols = 60, .rows = 30 });
}
test "Terminal: resize with reflow and saved cursor" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 3 });
defer t.deinit(alloc);
try t.printString("1A2B");
t.setCursorPos(2, 2);
@@ -13955,7 +15888,7 @@ test "Terminal: resize with reflow and saved cursor" {
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint.data);
}
{
@@ -13965,7 +15898,7 @@ test "Terminal: resize with reflow and saved cursor" {
}
t.saveCursor();
- try t.resize(alloc, 5, 3);
+ try t.resize(alloc, .{ .cols = 5, .rows = 3 });
t.restoreCursor();
{
@@ -13981,13 +15914,14 @@ test "Terminal: resize with reflow and saved cursor" {
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint.data);
}
}
test "Terminal: resize with reflow and saved cursor pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 2, .rows = 3 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 2, .rows = 3 });
defer t.deinit(alloc);
try t.printString("1A2B");
{
@@ -13996,7 +15930,7 @@ test "Terminal: resize with reflow and saved cursor pending wrap" {
.y = t.screens.active.cursor.y,
} }).?;
const cell = list_cell.cell;
- try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint);
+ try testing.expectEqual(@as(u32, 'B'), cell.content.codepoint.data);
}
{
@@ -14006,7 +15940,7 @@ test "Terminal: resize with reflow and saved cursor pending wrap" {
}
t.saveCursor();
- try t.resize(alloc, 5, 3);
+ try t.resize(alloc, .{ .cols = 5, .rows = 3 });
t.restoreCursor();
{
@@ -14026,7 +15960,8 @@ test "Terminal: resize with reflow and saved cursor pending wrap" {
test "Terminal: DECCOLM without DEC mode 40" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.@"132_column", true);
@@ -14038,7 +15973,8 @@ test "Terminal: DECCOLM without DEC mode 40" {
test "Terminal: DECCOLM unset" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.enable_mode_3, true);
@@ -14049,7 +15985,8 @@ test "Terminal: DECCOLM unset" {
test "Terminal: DECCOLM resets pending wrap" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
@@ -14064,7 +16001,8 @@ test "Terminal: DECCOLM resets pending wrap" {
test "Terminal: DECCOLM preserves SGR bg" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .direct_color_bg = .{
@@ -14088,7 +16026,8 @@ test "Terminal: DECCOLM preserves SGR bg" {
test "Terminal: DECCOLM resets scroll region" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.enable_left_and_right_margin, true);
@@ -14107,7 +16046,8 @@ test "Terminal: DECCOLM resets scroll region" {
test "Terminal: mode 47 alt screen plain" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Print on primary screen
@@ -14158,7 +16098,8 @@ test "Terminal: mode 47 alt screen plain" {
test "Terminal: mode 47 copies cursor both directions" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Color our cursor red
@@ -14194,7 +16135,8 @@ test "Terminal: mode 47 copies cursor both directions" {
test "Terminal: mode 1047 alt screen plain" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Print on primary screen
@@ -14245,7 +16187,8 @@ test "Terminal: mode 1047 alt screen plain" {
test "Terminal: mode 1047 copies cursor both directions" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Color our cursor red
@@ -14281,7 +16224,8 @@ test "Terminal: mode 1047 copies cursor both directions" {
test "Terminal: mode 1049 alt screen plain" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .rows = 5, .cols = 5 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Print on primary screen
@@ -14346,7 +16290,8 @@ test "Terminal: mode 1049 alt screen plain" {
// characters straddling the right margin boundary leave orphaned spacer_tails.
test "Terminal: deleteLines wide char at right margin with full clear" {
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 80, .rows = 24 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 24 });
defer t.deinit(alloc);
// Place a wide character at col 39 (1-indexed) on several rows.
@@ -14368,8 +16313,11 @@ test "Terminal: deleteLines wide char at right margin with full clear" {
}
test "Terminal: glyph APC stores session glossary entries" {
+ if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
+
const alloc = testing.allocator;
- var t = try init(alloc, .{ .cols = 80, .rows = 24 });
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 80, .rows = 24 });
defer t.deinit(alloc);
var register_parser = glyph.CommandParser.init(alloc, 1024 * 1024);
@@ -14398,3 +16346,161 @@ test "Terminal: glyph APC stores session glossary entries" {
t.fullReset();
try testing.expect(!t.glyph_glossary.contains(0xE0A0));
}
+
+test "Terminal: scroll region linefeed recycled row has default metadata" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(alloc);
+
+ // A soft-wrapped line across rows 0-2 so that row 1 has both wrap
+ // flags set. Mark row 1 as a prompt as well (OSC 133 A would).
+ for (0..12) |_| try t.print('A');
+ t.screens.active.pages.getCell(
+ .{ .active = .{ .y = 1 } },
+ ).?.row.semantic_prompt = .prompt;
+
+ // DECSTBM rows 2-4, cursor to the region bottom, and linefeed:
+ // row 1 is discarded and its Row storage recycled as the new
+ // blank region-bottom row.
+ t.setTopAndBottomMargin(2, 4);
+ t.setCursorPos(4, 1);
+ try t.linefeed();
+
+ {
+ const rac = t.screens.active.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);
+ }
+}
+
+test "Terminal: alt screen scroll up recycled row has default metadata" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
+ defer t.deinit(alloc);
+
+ try t.switchScreenMode(.@"1049", true);
+
+ // A soft-wrapped line across rows 0-1 and a prompt mark on row 0.
+ for (0..7) |_| try t.print('A');
+ t.screens.active.pages.getCell(
+ .{ .active = .{} },
+ ).?.row.semantic_prompt = .prompt;
+
+ // Scroll up: with no scrollback, row 0 is discarded and its Row
+ // storage recycled as the new blank bottom row.
+ try t.scrollUp(1);
+
+ {
+ const rac = t.screens.active.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 "Terminal: insertLines count over region blanks row metadata" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(alloc);
+
+ // A soft-wrapped line across rows 0-2 so that row 1 has both wrap
+ // flags set, plus a prompt mark on row 1.
+ for (0..12) |_| try t.print('A');
+ t.screens.active.pages.getCell(
+ .{ .active = .{ .y = 1 } },
+ ).?.row.semantic_prompt = .prompt;
+
+ // Insert more lines than remain in the region: every row from the
+ // cursor to the region bottom is blanked in place, with no shifts.
+ t.setCursorPos(2, 1);
+ t.insertLines(10);
+
+ for (1..5) |y| {
+ const rac = t.screens.active.pages.getCell(
+ .{ .active = .{ .y = @intCast(y) } },
+ ).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+}
+
+test "Terminal: deleteLines count over region blanks row metadata" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 5 });
+ defer t.deinit(alloc);
+
+ for (0..12) |_| try t.print('A');
+ t.screens.active.pages.getCell(
+ .{ .active = .{ .y = 1 } },
+ ).?.row.semantic_prompt = .prompt;
+
+ t.setCursorPos(2, 1);
+ t.deleteLines(10);
+
+ for (1..5) |y| {
+ const rac = t.screens.active.pages.getCell(
+ .{ .active = .{ .y = @intCast(y) } },
+ ).?;
+ try testing.expect(!rac.row.wrap);
+ try testing.expect(!rac.row.wrap_continuation);
+ try testing.expectEqual(.none, rac.row.semantic_prompt);
+ }
+}
+
+test "Terminal: deleteLines blank row does not retain semantic prompt" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 5, .rows = 3 });
+ defer t.deinit(alloc);
+
+ // Mark row 0 as a prompt row, then delete it. The blank row that
+ // appears at the region bottom reuses the deleted row's storage
+ // and must not read as a prompt (e.g. for prompt navigation).
+ try t.print('$');
+ t.screens.active.pages.getCell(
+ .{ .active = .{} },
+ ).?.row.semantic_prompt = .prompt;
+
+ t.setCursorPos(1, 1);
+ t.deleteLines(1);
+
+ {
+ const rac = t.screens.active.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 "Terminal: eraseDisplay complete ignores stale prompt on recycled row" {
+ const alloc = testing.allocator;
+ const io_impl = testing.io;
+ var t = try init(io_impl, alloc, .{ .cols = 10, .rows = 3 });
+ defer t.deinit(alloc);
+
+ // Screen content that must NOT enter the scrollback on a clear.
+ try t.printString("hello");
+
+ // Mark row 1 as a prompt row and then discard it with a region
+ // scroll, recycling its storage as the blank bottom row.
+ t.screens.active.pages.getCell(
+ .{ .active = .{ .y = 1 } },
+ ).?.row.semantic_prompt = .prompt;
+ t.setTopAndBottomMargin(2, 3);
+ t.setCursorPos(3, 1);
+ try t.linefeed();
+ t.setTopAndBottomMargin(0, 0);
+
+ // ED2: since no prompt is on screen, this must NOT take the
+ // scroll-and-clear path that pushes content into scrollback. A
+ // stale prompt flag on the recycled blank bottom row would.
+ t.eraseDisplay(.complete, false);
+
+ try testing.expectEqual(t.screens.active.pages.rows, t.screens.active.pages.total_rows);
+}