ghostty/src/terminal/Terminal.zig
2024-03-22 20:28:00 -07:00

7849 lines
240 KiB
Zig
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The primary terminal emulation structure. This represents a single
//! "terminal" containing a grid of characters and exposes various operations
//! on that grid. This also maintains the scrollback buffer.
const Terminal = @This();
// TODO on new terminal branch:
// - page splitting
// - resize tests when multiple pages are required
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const unicode = @import("../unicode/main.zig");
const ansi = @import("ansi.zig");
const modes = @import("modes.zig");
const charsets = @import("charsets.zig");
const csi = @import("csi.zig");
const kitty = @import("kitty.zig");
const sgr = @import("sgr.zig");
const Tabstops = @import("Tabstops.zig");
const color = @import("color.zig");
const mouse_shape = @import("mouse_shape.zig");
const size = @import("size.zig");
const pagepkg = @import("page.zig");
const style = @import("style.zig");
const Screen = @import("Screen.zig");
const Page = pagepkg.Page;
const Cell = pagepkg.Cell;
const Row = pagepkg.Row;
const log = std.log.scoped(.terminal);
/// Default tabstop interval
const TABSTOP_INTERVAL = 8;
/// Screen type is an enum that tracks whether a screen is primary or alternate.
pub const ScreenType = enum {
primary,
alternate,
};
/// Screen is the current screen state. The "active_screen" field says what
/// the current screen is. The backup screen is the opposite of the active
/// screen.
active_screen: ScreenType,
screen: Screen,
secondary_screen: Screen,
/// Whether we're currently writing to the status line (DECSASD and DECSSDT).
/// We don't support a status line currently so we just black hole this
/// data so that it doesn't mess up our main display.
status_display: ansi.StatusDisplay = .main,
/// Where the tabstops are.
tabstops: Tabstops,
/// The size of the terminal.
rows: size.CellCountInt,
cols: size.CellCountInt,
/// The size of the screen in pixels. This is used for pty events and images
width_px: u32 = 0,
height_px: u32 = 0,
/// The current scrolling region.
scrolling_region: ScrollingRegion,
/// The last reported pwd, if any.
pwd: std.ArrayList(u8),
/// The default color palette. This is only modified by changing the config file
/// and is used to reset the palette when receiving an OSC 104 command.
default_palette: color.Palette = color.default,
/// The color palette to use. The mask indicates which palette indices have been
/// modified with OSC 4
color_palette: struct {
const Mask = std.StaticBitSet(@typeInfo(color.Palette).Array.len);
colors: color.Palette = color.default,
mask: Mask = Mask.initEmpty(),
} = .{},
/// The previous printed character. This is used for the repeat previous
/// char CSI (ESC [ <n> b).
previous_char: ?u21 = null,
/// The modes that this terminal currently has active.
modes: modes.ModeState = .{},
/// The most recently set mouse shape for the terminal.
mouse_shape: mouse_shape.MouseShape = .text,
/// These are just a packed set of flags we may set on the terminal.
flags: packed struct {
// This isn't a mode, this is set by OSC 133 using the "A" event.
// If this is true, it tells us that the shell supports redrawing
// the prompt and that when we resize, if the cursor is at a prompt,
// then we should clear the screen below and allow the shell to redraw.
shell_redraws_prompt: bool = false,
// This is set via ESC[4;2m. Any other modify key mode just sets
// this to false and we act in mode 1 by default.
modify_other_keys_2: bool = false,
/// The mouse event mode and format. These are set to the last
/// set mode in modes. You can't get the right event/format to use
/// based on modes alone because modes don't show you what order
/// this was called so we have to track it separately.
mouse_event: MouseEvents = .none,
mouse_format: MouseFormat = .x10,
/// Set via the XTSHIFTESCAPE sequence. If true (XTSHIFTESCAPE = 1)
/// then we want to capture the shift key for the mouse protocol
/// if the configuration allows it.
mouse_shift_capture: enum { null, false, true } = .null,
} = .{},
/// The event types that can be reported for mouse-related activities.
/// These are all mutually exclusive (hence in a single enum).
pub const MouseEvents = enum(u3) {
none = 0,
x10 = 1, // 9
normal = 2, // 1000
button = 3, // 1002
any = 4, // 1003
/// Returns true if this event sends motion events.
pub fn motion(self: MouseEvents) bool {
return self == .button or self == .any;
}
};
/// The format of mouse events when enabled.
/// These are all mutually exclusive (hence in a single enum).
pub const MouseFormat = enum(u3) {
x10 = 0,
utf8 = 1, // 1005
sgr = 2, // 1006
urxvt = 3, // 1015
sgr_pixels = 4, // 1016
};
/// Scrolling region is the area of the screen designated where scrolling
/// occurs. When scrolling the screen, only this viewport is scrolled.
pub const ScrollingRegion = struct {
// Top and bottom of the scroll region (0-indexed)
// Precondition: top < bottom
top: size.CellCountInt,
bottom: size.CellCountInt,
// Left/right scroll regions.
// Precondition: right > left
// Precondition: right <= cols - 1
left: size.CellCountInt,
right: size.CellCountInt,
};
pub const Options = struct {
cols: size.CellCountInt,
rows: size.CellCountInt,
max_scrollback: usize = 10_000,
};
/// Initialize a new terminal.
pub fn init(
alloc: Allocator,
opts: Options,
) !Terminal {
const cols = opts.cols;
const rows = opts.rows;
return Terminal{
.cols = cols,
.rows = rows,
.active_screen = .primary,
.screen = try Screen.init(alloc, cols, rows, opts.max_scrollback),
.secondary_screen = try Screen.init(alloc, cols, rows, 0),
.tabstops = try Tabstops.init(alloc, cols, TABSTOP_INTERVAL),
.scrolling_region = .{
.top = 0,
.bottom = rows - 1,
.left = 0,
.right = cols - 1,
},
.pwd = std.ArrayList(u8).init(alloc),
};
}
pub fn deinit(self: *Terminal, alloc: Allocator) void {
self.tabstops.deinit(alloc);
self.screen.deinit();
self.secondary_screen.deinit();
self.pwd.deinit();
self.* = undefined;
}
/// 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);
var it = view.iterator();
while (it.nextCodepoint()) |cp| {
switch (cp) {
'\n' => {
self.carriageReturn();
try self.linefeed();
},
else => try self.print(cp),
}
}
}
/// 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);
}
}
pub fn print(self: *Terminal, c: u21) !void {
// log.debug("print={x} y={} x={}", .{ c, self.screen.cursor.y, self.screen.cursor.x });
// If we're not on the main display, do nothing for now
if (self.status_display != .main) return;
// Our right margin depends where our cursor is now.
const right_limit = if (self.screen.cursor.x > self.scrolling_region.right)
self.cols
else
self.scrolling_region.right + 1;
// Perform grapheme clustering if grapheme support is enabled (mode 2027).
// This is MUCH slower than the normal path so the conditional below is
// purposely ordered in least-likely to most-likely so we can drop out
// as quickly as possible.
if (c > 255 and
self.modes.get(.grapheme_cluster) and
self.screen.cursor.x > 0)
grapheme: {
// We need the previous cell to determine if we're at a grapheme
// break or not. If we are NOT, then we are still combining the
// same grapheme. Otherwise, we can stay in this cell.
const Prev = struct { cell: *Cell, left: size.CellCountInt };
const prev: Prev = prev: {
const left: size.CellCountInt = left: {
// If we have wraparound, then we always use the prev col
if (self.modes.get(.wraparound)) break :left 1;
// If we do not have wraparound, the logic is trickier. If
// we're not on the last column, then we just use the previous
// column. Otherwise, we need to check if there is text to
// figure out if we're attaching to the prev or current.
if (self.screen.cursor.x != right_limit - 1) break :left 1;
break :left @intFromBool(!self.screen.cursor.page_cell.hasText());
};
// If the previous cell is a wide spacer tail, then we actually
// want to use the cell before that because that has the actual
// content.
const immediate = self.screen.cursorCellLeft(left);
break :prev switch (immediate.wide) {
else => .{ .cell = immediate, .left = left },
.spacer_tail => .{
.cell = self.screen.cursorCellLeft(left + 1),
.left = left + 1,
},
};
};
// If our cell has no content, then this is a new cell and
// necessarily a grapheme break.
if (!prev.cell.hasText()) break :grapheme;
const grapheme_break = brk: {
var state: unicode.GraphemeBreakState = .{};
var cp1: u21 = prev.cell.content.codepoint;
if (prev.cell.hasGrapheme()) {
const cps = self.screen.cursor.page_pin.page.data.lookupGrapheme(prev.cell).?;
for (cps) |cp2| {
// log.debug("cp1={x} cp2={x}", .{ cp1, cp2 });
assert(!unicode.graphemeBreak(cp1, cp2, &state));
cp1 = cp2;
}
}
// log.debug("cp1={x} cp2={x} end", .{ cp1, c });
break :brk unicode.graphemeBreak(cp1, c, &state);
};
// If we can NOT break, this means that "c" is part of a grapheme
// with the previous char.
if (!grapheme_break) {
// If this is an emoji variation selector then we need to modify
// the cell width accordingly. VS16 makes the character wide and
// VS15 makes it narrow.
if (c == 0xFE0F or c == 0xFE0E) {
// This only applies to emoji
const prev_props = unicode.getProperties(prev.cell.content.codepoint);
const emoji = prev_props.grapheme_boundary_class == .extended_pictographic;
if (!emoji) return;
switch (c) {
0xFE0F => wide: {
if (prev.cell.wide == .wide) break :wide;
// Move our cursor back to the previous. We'll move
// the cursor within this block to the proper location.
self.screen.cursorLeft(prev.left);
// If we don't have space for the wide char, we need
// to insert spacers and wrap. Then we just print the wide
// char as normal.
if (self.screen.cursor.x == right_limit - 1) {
if (!self.modes.get(.wraparound)) return;
self.printCell(' ', .spacer_head);
try self.printWrap();
}
self.printCell(prev.cell.content.codepoint, .wide);
// Write our spacer
self.screen.cursorRight(1);
self.printCell(' ', .spacer_tail);
// Move the cursor again so we're beyond our spacer
if (self.screen.cursor.x == right_limit - 1) {
self.screen.cursor.pending_wrap = true;
} else {
self.screen.cursorRight(1);
}
},
0xFE0E => narrow: {
// Prev cell is no longer wide
if (prev.cell.wide != .wide) break :narrow;
prev.cell.wide = .narrow;
// Remove the wide spacer tail
const cell = self.screen.cursorCellLeft(prev.left - 1);
cell.wide = .narrow;
break :narrow;
},
else => unreachable,
}
}
log.debug("c={x} grapheme attach to left={}", .{ c, prev.left });
try self.screen.cursor.page_pin.page.data.appendGrapheme(
self.screen.cursor.page_row,
prev.cell,
c,
);
return;
}
}
// Determine the width of this character so we can handle
// non-single-width characters properly. We have a fast-path for
// byte-sized characters since they're so common. We can ignore
// control characters because they're always filtered prior.
const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width);
// Note: it is possible to have a width of "3" and a width of "-1"
// from ziglyph. We should look into those cases and handle them
// appropriately.
assert(width <= 2);
// log.debug("c={x} width={}", .{ c, width });
// Attach zero-width characters to our cell as grapheme data.
if (width == 0) {
// If we have grapheme clustering enabled, we don't blindly attach
// any zero width character to our cells and we instead just ignore
// it.
if (self.modes.get(.grapheme_cluster)) return;
// If we're at cell zero, then this is malformed data and we don't
// print anything or even store this. Zero-width characters are ALWAYS
// attached to some other non-zero-width character at the time of
// writing.
if (self.screen.cursor.x == 0) {
log.warn("zero-width character with no prior character, ignoring", .{});
return;
}
// Find our previous cell
const prev = prev: {
const immediate = self.screen.cursorCellLeft(1);
if (immediate.wide != .spacer_tail) break :prev immediate;
break :prev self.screen.cursorCellLeft(2);
};
// If our previous cell has no text, just ignore the zero-width character
if (!prev.hasText()) {
log.warn("zero-width character with no prior character, ignoring", .{});
return;
}
// If this is a emoji variation selector, prev must be an emoji
if (c == 0xFE0F or c == 0xFE0E) {
const prev_props = unicode.getProperties(prev.content.codepoint);
const emoji = prev_props.grapheme_boundary_class == .extended_pictographic;
if (!emoji) return;
}
try self.screen.cursor.page_pin.page.data.appendGrapheme(
self.screen.cursor.page_row,
prev,
c,
);
return;
}
// We have a printable character, save it
self.previous_char = c;
// If we're soft-wrapping, then handle that first.
if (self.screen.cursor.pending_wrap and self.modes.get(.wraparound)) {
try self.printWrap();
}
// If we have insert mode enabled then we need to handle that. We
// only do insert mode if we're not at the end of the line.
if (self.modes.get(.insert) and
self.screen.cursor.x + width < self.cols)
{
self.insertBlanks(width);
}
switch (width) {
// Single cell is very easy: just write in the cell
1 => @call(.always_inline, printCell, .{ self, c, .narrow }),
// Wide character requires a spacer. We print this by
// using two cells: the first is flagged "wide" and has the
// wide char. The second is guaranteed to be a spacer if
// we're not at the end of the line.
2 => if ((right_limit - self.scrolling_region.left) > 1) {
// If we don't have space for the wide char, we need
// to insert spacers and wrap. Then we just print the wide
// char as normal.
if (self.screen.cursor.x == right_limit - 1) {
// If we don't have wraparound enabled then we don't print
// this character at all and don't move the cursor. This is
// how xterm behaves.
if (!self.modes.get(.wraparound)) return;
self.printCell(' ', .spacer_head);
try self.printWrap();
}
self.printCell(c, .wide);
self.screen.cursorRight(1);
self.printCell(' ', .spacer_tail);
} else {
// This is pretty broken, terminals should never be only 1-wide.
// We sould prevent this downstream.
self.printCell(' ', .narrow);
},
else => unreachable,
}
// If we're at the column limit, then we need to wrap the next time.
// In this case, we don't move the cursor.
if (self.screen.cursor.x == right_limit - 1) {
self.screen.cursor.pending_wrap = true;
return;
}
// Move the cursor
self.screen.cursorRight(1);
}
fn printCell(
self: *Terminal,
unmapped_c: u21,
wide: Cell.Wide,
) void {
// TODO: spacers should use a bgcolor only cell
const c: u21 = c: {
// TODO: non-utf8 handling, gr
// If we're single shifting, then we use the key exactly once.
const key = if (self.screen.charset.single_shift) |key_once| blk: {
self.screen.charset.single_shift = null;
break :blk key_once;
} else self.screen.charset.gl;
const set = self.screen.charset.charsets.get(key);
// UTF-8 or ASCII is used as-is
if (set == .utf8 or set == .ascii) break :c unmapped_c;
// If we're outside of ASCII range this is an invalid value in
// this table so we just return space.
if (unmapped_c > std.math.maxInt(u8)) break :c ' ';
// Get our lookup table and map it
const table = set.table();
break :c @intCast(table[@intCast(unmapped_c)]);
};
const cell = self.screen.cursor.page_cell;
// If the wide property of this cell is the same, then we don't
// need to do the special handling here because the structure will
// be the same. If it is NOT the same, then we may need to clear some
// cells.
if (cell.wide != wide) {
switch (cell.wide) {
// Previous cell was narrow. Do nothing.
.narrow => {},
// Previous cell was wide. We need to clear the tail and head.
.wide => wide: {
if (self.screen.cursor.x >= self.cols - 1) break :wide;
const spacer_cell = self.screen.cursorCellRight(1);
spacer_cell.* = .{ .style_id = self.screen.cursor.style_id };
if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) {
const head_cell = self.screen.cursorCellEndOfPrev();
head_cell.wide = .narrow;
}
},
.spacer_tail => {
assert(self.screen.cursor.x > 0);
const wide_cell = self.screen.cursorCellLeft(1);
wide_cell.* = .{ .style_id = self.screen.cursor.style_id };
if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) {
const head_cell = self.screen.cursorCellEndOfPrev();
head_cell.wide = .narrow;
}
},
// TODO: this case was not handled in the old terminal implementation
// but it feels like we should do something. investigate other
// terminals (xterm mainly) and see whats up.
.spacer_head => {},
}
}
// If the prior value had graphemes, clear those
if (cell.hasGrapheme()) {
self.screen.cursor.page_pin.page.data.clearGrapheme(
self.screen.cursor.page_row,
cell,
);
}
// Keep track of the previous style so we can decrement the ref count
const prev_style_id = cell.style_id;
// Write
cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = c },
.style_id = self.screen.cursor.style_id,
.wide = wide,
.protected = self.screen.cursor.protected,
};
// Handle the style ref count handling
style_ref: {
if (prev_style_id != style.default_id) {
const row = self.screen.cursor.page_row;
assert(row.styled);
// If our previous cell had the same style ID as us currently,
// then we don't bother with any ref counts because we're the same.
if (prev_style_id == self.screen.cursor.style_id) break :style_ref;
// Slow path: we need to lookup this style so we can decrement
// the ref count. Since we've already loaded everything, we also
// just go ahead and GC it if it reaches zero, too.
var page = &self.screen.cursor.page_pin.page.data;
if (page.styles.lookupId(page.memory, prev_style_id)) |prev_style| {
// Below upsert can't fail because it should already be present
const md = page.styles.upsert(page.memory, prev_style.*) catch unreachable;
assert(md.ref > 0);
md.ref -= 1;
if (md.ref == 0) page.styles.remove(page.memory, prev_style_id);
}
}
// If we have a ref-counted style, increase.
if (self.screen.cursor.style_ref) |ref| {
ref.* += 1;
self.screen.cursor.page_row.styled = true;
}
}
}
fn printWrap(self: *Terminal) !void {
self.screen.cursor.page_row.wrap = true;
// Get the old semantic prompt so we can extend it to the next
// line. We need to do this before we index() because we may
// modify memory.
const old_prompt = self.screen.cursor.page_row.semantic_prompt;
// Move to the next line
try self.index();
self.screen.cursorHorizontalAbsolute(self.scrolling_region.left);
// New line must inherit semantic prompt of the old line
self.screen.cursor.page_row.semantic_prompt = old_prompt;
self.screen.cursor.page_row.wrap_continuation = true;
}
/// Set the charset into the given slot.
pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void {
self.screen.charset.charsets.set(slot, set);
}
/// Invoke the charset in slot into the active slot. If single is true,
/// then this will only be invoked for a single character.
pub fn invokeCharset(
self: *Terminal,
active: charsets.ActiveSlot,
slot: charsets.Slots,
single: bool,
) void {
if (single) {
assert(active == .GL);
self.screen.charset.single_shift = slot;
return;
}
switch (active) {
.GL => self.screen.charset.gl = slot,
.GR => self.screen.charset.gr = slot,
}
}
/// Carriage return moves the cursor to the first column.
pub fn carriageReturn(self: *Terminal) void {
// Always reset pending wrap state
self.screen.cursor.pending_wrap = false;
// In origin mode we always move to the left margin
self.screen.cursorHorizontalAbsolute(if (self.modes.get(.origin))
self.scrolling_region.left
else if (self.screen.cursor.x >= self.scrolling_region.left)
self.scrolling_region.left
else
0);
}
/// Linefeed moves the cursor to the next line.
pub fn linefeed(self: *Terminal) !void {
try self.index();
if (self.modes.get(.linefeed)) self.carriageReturn();
}
/// Backspace moves the cursor back a column (but not less than 0).
pub fn backspace(self: *Terminal) void {
self.cursorLeft(1);
}
/// Move the cursor up amount lines. If amount is greater than the maximum
/// move distance then it is internally adjusted to the maximum. If amount is
/// 0, adjust it to 1.
pub fn cursorUp(self: *Terminal, count_req: usize) void {
// Always resets pending wrap
self.screen.cursor.pending_wrap = false;
// The maximum amount the cursor can move up depends on scrolling regions
const max = if (self.screen.cursor.y >= self.scrolling_region.top)
self.screen.cursor.y - self.scrolling_region.top
else
self.screen.cursor.y;
const count = @min(max, @max(count_req, 1));
// We can safely intCast below because of the min/max clamping we did above.
self.screen.cursorUp(@intCast(count));
}
/// Move the cursor down amount lines. If amount is greater than the maximum
/// move distance then it is internally adjusted to the maximum. This sequence
/// will not scroll the screen or scroll region. If amount is 0, adjust it to 1.
pub fn cursorDown(self: *Terminal, count_req: usize) void {
// Always resets pending wrap
self.screen.cursor.pending_wrap = false;
// The max the cursor can move to depends where the cursor currently is
const max = if (self.screen.cursor.y <= self.scrolling_region.bottom)
self.scrolling_region.bottom - self.screen.cursor.y
else
self.rows - self.screen.cursor.y - 1;
const count = @min(max, @max(count_req, 1));
self.screen.cursorDown(@intCast(count));
}
/// Move the cursor right amount columns. If amount is greater than the
/// maximum move distance then it is internally adjusted to the maximum.
/// This sequence will not scroll the screen or scroll region. If amount is
/// 0, adjust it to 1.
pub fn cursorRight(self: *Terminal, count_req: usize) void {
// Always resets pending wrap
self.screen.cursor.pending_wrap = false;
// The max the cursor can move to depends where the cursor currently is
const max = if (self.screen.cursor.x <= self.scrolling_region.right)
self.scrolling_region.right - self.screen.cursor.x
else
self.cols - self.screen.cursor.x - 1;
const count = @min(max, @max(count_req, 1));
self.screen.cursorRight(@intCast(count));
}
/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1.
pub fn cursorLeft(self: *Terminal, count_req: usize) void {
// Wrapping behavior depends on various terminal modes
const WrapMode = enum { none, reverse, reverse_extended };
const wrap_mode: WrapMode = wrap_mode: {
if (!self.modes.get(.wraparound)) break :wrap_mode .none;
if (self.modes.get(.reverse_wrap_extended)) break :wrap_mode .reverse_extended;
if (self.modes.get(.reverse_wrap)) break :wrap_mode .reverse;
break :wrap_mode .none;
};
var count = @max(count_req, 1);
// If we are in no wrap mode, then we move the cursor left and exit
// since this is the fastest and most typical path.
if (wrap_mode == .none) {
self.screen.cursorLeft(@min(count, self.screen.cursor.x));
self.screen.cursor.pending_wrap = false;
return;
}
// If we have a pending wrap state and we are in either reverse wrap
// modes then we decrement the amount we move by one to match xterm.
if (self.screen.cursor.pending_wrap) {
count -= 1;
self.screen.cursor.pending_wrap = false;
}
// The margins we can move to.
const top = self.scrolling_region.top;
const bottom = self.scrolling_region.bottom;
const right_margin = self.scrolling_region.right;
const left_margin = if (self.screen.cursor.x < self.scrolling_region.left)
0
else
self.scrolling_region.left;
// Handle some edge cases when our cursor is already on the left margin.
if (self.screen.cursor.x == left_margin) {
switch (wrap_mode) {
// In reverse mode, if we're already before the top margin
// then we just set our cursor to the top-left and we're done.
.reverse => if (self.screen.cursor.y <= top) {
self.screen.cursorAbsolute(left_margin, top);
return;
},
// Handled in while loop
.reverse_extended => {},
// Handled above
.none => unreachable,
}
}
while (true) {
// We can move at most to the left margin.
const max = self.screen.cursor.x - left_margin;
// We want to move at most the number of columns we have left
// or our remaining count. Do the move.
const amount = @min(max, count);
count -= amount;
self.screen.cursorLeft(amount);
// If we have no more to move, then we're done.
if (count == 0) break;
// If we are at the top, then we are done.
if (self.screen.cursor.y == top) {
if (wrap_mode != .reverse_extended) break;
self.screen.cursorAbsolute(right_margin, bottom);
count -= 1;
continue;
}
// UNDEFINED TERMINAL BEHAVIOR. This situation is not handled in xterm
// and currently results in a crash in xterm. Given no other known
// terminal [to me] implements XTREVWRAP2, I decided to just mimick
// the behavior of xterm up and not including the crash by wrapping
// up to the (0, 0) and stopping there. My reasoning is that for an
// appropriately sized value of "count" this is the behavior that xterm
// would have. This is unit tested.
if (self.screen.cursor.y == 0) {
assert(self.screen.cursor.x == left_margin);
break;
}
// If our previous line is not wrapped then we are done.
if (wrap_mode != .reverse_extended) {
const prev_row = self.screen.cursorRowUp(1);
if (!prev_row.wrap) break;
}
self.screen.cursorAbsolute(right_margin, self.screen.cursor.y - 1);
count -= 1;
}
}
/// Save cursor position and further state.
///
/// The primary and alternate screen have distinct save state. One saved state
/// is kept per screen (main / alternative). If for the current screen state
/// was already saved it is overwritten.
pub fn saveCursor(self: *Terminal) void {
self.screen.saved_cursor = .{
.x = self.screen.cursor.x,
.y = self.screen.cursor.y,
.style = self.screen.cursor.style,
.protected = self.screen.cursor.protected,
.pending_wrap = self.screen.cursor.pending_wrap,
.origin = self.modes.get(.origin),
.charset = self.screen.charset,
};
}
/// Restore cursor position and other state.
///
/// The primary and alternate screen have distinct save state.
/// If no save was done before values are reset to their initial values.
pub fn restoreCursor(self: *Terminal) !void {
const saved: Screen.SavedCursor = self.screen.saved_cursor orelse .{
.x = 0,
.y = 0,
.style = .{},
.protected = false,
.pending_wrap = false,
.origin = false,
.charset = .{},
};
// Set the style first because it can fail
const old_style = self.screen.cursor.style;
self.screen.cursor.style = saved.style;
errdefer self.screen.cursor.style = old_style;
try self.screen.manualStyleUpdate();
self.screen.charset = saved.charset;
self.modes.set(.origin, saved.origin);
self.screen.cursor.pending_wrap = saved.pending_wrap;
self.screen.cursor.protected = saved.protected;
self.screen.cursorAbsolute(
@min(saved.x, self.cols - 1),
@min(saved.y, self.rows - 1),
);
}
/// Set the character protection mode for the terminal.
pub fn setProtectedMode(self: *Terminal, mode: ansi.ProtectedMode) void {
switch (mode) {
.off => {
self.screen.cursor.protected = false;
// screen.protected_mode is NEVER reset to ".off" because
// logic such as eraseChars depends on knowing what the
// _most recent_ mode was.
},
.iso => {
self.screen.cursor.protected = true;
self.screen.protected_mode = .iso;
},
.dec => {
self.screen.cursor.protected = true;
self.screen.protected_mode = .dec;
},
}
}
/// The semantic prompt type. This is used when tracking a line type and
/// requires integration with the shell. By default, we mark a line as "none"
/// meaning we don't know what type it is.
///
/// See: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
pub const SemanticPrompt = enum {
prompt,
prompt_continuation,
input,
command,
};
/// Mark the current semantic prompt information. Current escape sequences
/// (OSC 133) only allow setting this for wherever the current active cursor
/// is located.
pub fn markSemanticPrompt(self: *Terminal, p: SemanticPrompt) void {
//log.debug("semantic_prompt y={} p={}", .{ self.screen.cursor.y, p });
self.screen.cursor.page_row.semantic_prompt = switch (p) {
.prompt => .prompt,
.prompt_continuation => .prompt_continuation,
.input => .input,
.command => .command,
};
}
/// Returns true if the cursor is currently at a prompt. Another way to look
/// at this is it returns false if the shell is currently outputting something.
/// This requires shell integration (semantic prompt integration).
///
/// If the shell integration doesn't exist, this will always return false.
pub fn cursorIsAtPrompt(self: *Terminal) bool {
// If we're on the secondary screen, we're never at a prompt.
if (self.active_screen == .alternate) return false;
// Reverse through the active
const start_x, const start_y = .{ self.screen.cursor.x, self.screen.cursor.y };
defer self.screen.cursorAbsolute(start_x, start_y);
for (0..start_y + 1) |i| {
if (i > 0) self.screen.cursorUp(1);
switch (self.screen.cursor.page_row.semantic_prompt) {
// If we're at a prompt or input area, then we are at a prompt.
.prompt,
.prompt_continuation,
.input,
=> return true,
// If we have command output, then we're most certainly not
// at a prompt.
.command => return false,
// If we don't know, we keep searching.
.unknown => {},
}
}
return false;
}
/// Horizontal tab moves the cursor to the next tabstop, clearing
/// the screen to the left the tabstop.
pub fn horizontalTab(self: *Terminal) !void {
while (self.screen.cursor.x < self.scrolling_region.right) {
// Move the cursor right
self.screen.cursorRight(1);
// If the last cursor position was a tabstop we return. We do
// "last cursor position" because we want a space to be written
// at the tabstop unless we're at the end (the while condition).
if (self.tabstops.get(self.screen.cursor.x)) return;
}
}
// Same as horizontalTab but moves to the previous tabstop instead of the next.
pub fn horizontalTabBack(self: *Terminal) !void {
// With origin mode enabled, our leftmost limit is the left margin.
const left_limit = if (self.modes.get(.origin)) self.scrolling_region.left else 0;
while (true) {
// If we're already at the edge of the screen, then we're done.
if (self.screen.cursor.x <= left_limit) return;
// Move the cursor left
self.screen.cursorLeft(1);
if (self.tabstops.get(self.screen.cursor.x)) return;
}
}
/// Clear tab stops.
pub fn tabClear(self: *Terminal, cmd: csi.TabClear) void {
switch (cmd) {
.current => self.tabstops.unset(self.screen.cursor.x),
.all => self.tabstops.reset(0),
else => log.warn("invalid or unknown tab clear setting: {}", .{cmd}),
}
}
/// Set a tab stop on the current cursor.
/// TODO: test
pub fn tabSet(self: *Terminal) void {
self.tabstops.set(self.screen.cursor.x);
}
/// TODO: test
pub fn tabReset(self: *Terminal) void {
self.tabstops.reset(TABSTOP_INTERVAL);
}
/// Move the cursor to the next line in the scrolling region, possibly scrolling.
///
/// If the cursor is outside of the scrolling region: move the cursor one line
/// down if it is not on the bottom-most line of the screen.
///
/// If the cursor is inside the scrolling region:
/// If the cursor is on the bottom-most line of the scrolling region:
/// invoke scroll up with amount=1
/// If the cursor is not on the bottom-most line of the scrolling region:
/// move the cursor one line down
///
/// This unsets the pending wrap state without wrapping.
pub fn index(self: *Terminal) !void {
// Unset pending wrap state
self.screen.cursor.pending_wrap = false;
// Outside of the scroll region we move the cursor one line down.
if (self.screen.cursor.y < self.scrolling_region.top or
self.screen.cursor.y > self.scrolling_region.bottom)
{
// We only move down if we're not already at the bottom of
// the screen.
if (self.screen.cursor.y < self.rows - 1) {
self.screen.cursorDown(1);
}
return;
}
// If the cursor is inside the scrolling region and on the bottom-most
// line, then we scroll up. If our scrolling region is the full screen
// we create scrollback.
if (self.screen.cursor.y == self.scrolling_region.bottom and
self.screen.cursor.x >= self.scrolling_region.left and
self.screen.cursor.x <= self.scrolling_region.right)
{
// If our scrolling region is the full screen, we create scrollback.
// Otherwise, we simply scroll the region.
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)
{
try self.screen.cursorDownScroll();
} else {
self.scrollUp(1);
}
return;
}
// Increase cursor by 1, maximum to bottom of scroll region
if (self.screen.cursor.y < self.scrolling_region.bottom) {
self.screen.cursorDown(1);
}
}
/// Move the cursor to the previous line in the scrolling region, possibly
/// scrolling.
///
/// If the cursor is outside of the scrolling region, move the cursor one
/// line up if it is not on the top-most line of the screen.
///
/// If the cursor is inside the scrolling region:
///
/// * If the cursor is on the top-most line of the scrolling region:
/// invoke scroll down with amount=1
/// * If the cursor is not on the top-most line of the scrolling region:
/// move the cursor one line up
pub fn reverseIndex(self: *Terminal) void {
if (self.screen.cursor.y != self.scrolling_region.top or
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right)
{
self.cursorUp(1);
return;
}
self.scrollDown(1);
}
// Set Cursor Position. Move cursor to the position indicated
// by row and column (1-indexed). If column is 0, it is adjusted to 1.
// If column is greater than the right-most column it is adjusted to
// the right-most column. If row is 0, it is adjusted to 1. If row is
// greater than the bottom-most row it is adjusted to the bottom-most
// row.
pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void {
// If cursor origin mode is set the cursor row will be moved relative to
// the top margin row and adjusted to be above or at bottom-most row in
// the current scroll region.
//
// If origin mode is set and left and right margin mode is set the cursor
// will be moved relative to the left margin column and adjusted to be on
// or left of the right margin column.
const params: struct {
x_offset: size.CellCountInt = 0,
y_offset: size.CellCountInt = 0,
x_max: size.CellCountInt,
y_max: size.CellCountInt,
} = if (self.modes.get(.origin)) .{
.x_offset = self.scrolling_region.left,
.y_offset = self.scrolling_region.top,
.x_max = self.scrolling_region.right + 1, // We need this 1-indexed
.y_max = self.scrolling_region.bottom + 1, // We need this 1-indexed
} else .{
.x_max = self.cols,
.y_max = self.rows,
};
// Unset pending wrap state
self.screen.cursor.pending_wrap = false;
// Calculate our new x/y
const row = if (row_req == 0) 1 else row_req;
const col = if (col_req == 0) 1 else col_req;
const x = @min(params.x_max, col + params.x_offset) -| 1;
const y = @min(params.y_max, row + params.y_offset) -| 1;
// If the y is unchanged then this is fast pointer math
if (y == self.screen.cursor.y) {
if (x > self.screen.cursor.x) {
self.screen.cursorRight(x - self.screen.cursor.x);
} else {
self.screen.cursorLeft(self.screen.cursor.x - x);
}
return;
}
// If everything changed we do an absolute change which is slightly slower
self.screen.cursorAbsolute(x, y);
// log.info("set cursor position: col={} row={}", .{ self.screen.cursor.x, self.screen.cursor.y });
}
/// Set Top and Bottom Margins If bottom is not specified, 0 or bigger than
/// the number of the bottom-most row, it is adjusted to the number of the
/// bottom most row.
///
/// If top < bottom set the top and bottom row of the scroll region according
/// to top and bottom and move the cursor to the top-left cell of the display
/// (when in cursor origin mode is set to the top-left cell of the scroll region).
///
/// Otherwise: Set the top and bottom row of the scroll region to the top-most
/// and bottom-most line of the screen.
///
/// Top and bottom are 1-indexed.
pub fn setTopAndBottomMargin(self: *Terminal, top_req: usize, bottom_req: usize) void {
const top = @max(1, top_req);
const bottom = @min(self.rows, if (bottom_req == 0) self.rows else bottom_req);
if (top >= bottom) return;
self.scrolling_region.top = @intCast(top - 1);
self.scrolling_region.bottom = @intCast(bottom - 1);
self.setCursorPos(1, 1);
}
/// DECSLRM
pub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize) void {
// We must have this mode enabled to do anything
if (!self.modes.get(.enable_left_and_right_margin)) return;
const left = @max(1, left_req);
const right = @min(self.cols, if (right_req == 0) self.cols else right_req);
if (left >= right) return;
self.scrolling_region.left = @intCast(left - 1);
self.scrolling_region.right = @intCast(right - 1);
self.setCursorPos(1, 1);
}
/// Scroll the text down by one row.
pub fn scrollDown(self: *Terminal, count: usize) void {
// Preserve our x/y to restore.
const old_x = self.screen.cursor.x;
const old_y = self.screen.cursor.y;
const old_wrap = self.screen.cursor.pending_wrap;
defer {
self.screen.cursorAbsolute(old_x, old_y);
self.screen.cursor.pending_wrap = old_wrap;
}
// Move to the top of the scroll region
self.screen.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top);
self.insertLines(count);
}
/// Removes amount lines from the top of the scroll region. The remaining lines
/// to the bottom margin are shifted up and space from the bottom margin up
/// is filled with empty lines.
///
/// The new lines are created according to the current SGR state.
///
/// Does not change the (absolute) cursor position.
pub fn scrollUp(self: *Terminal, count: usize) void {
// Preserve our x/y to restore.
const old_x = self.screen.cursor.x;
const old_y = self.screen.cursor.y;
const old_wrap = self.screen.cursor.pending_wrap;
defer {
self.screen.cursorAbsolute(old_x, old_y);
self.screen.cursor.pending_wrap = old_wrap;
}
// Move to the top of the scroll region
self.screen.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top);
self.deleteLines(count);
}
/// Options for scrolling the viewport of the terminal grid.
pub const ScrollViewport = union(enum) {
/// Scroll to the top of the scrollback
top: void,
/// Scroll to the bottom, i.e. the top of the active area
bottom: void,
/// Scroll by some delta amount, up is negative.
delta: isize,
};
/// Scroll the viewport of the terminal grid.
pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void {
self.screen.scroll(switch (behavior) {
.top => .{ .top = {} },
.bottom => .{ .active = {} },
.delta => |delta| .{ .delta_row = delta },
});
}
/// Insert amount lines at the current cursor row. The contents of the line
/// at the current cursor row and below (to the bottom-most line in the
/// scrolling region) are shifted down by amount lines. The contents of the
/// amount bottom-most lines in the scroll region are lost.
///
/// This unsets the pending wrap state without wrapping. If the current cursor
/// position is outside of the current scroll region it does nothing.
///
/// If amount is greater than the remaining number of lines in the scrolling
/// region it is adjusted down (still allowing for scrolling out every remaining
/// line in the scrolling region)
///
/// In left and right margin mode the margins are respected; lines are only
/// scrolled in the scroll region.
///
/// All cleared space is colored according to the current SGR state.
///
/// Moves the cursor to the left margin.
pub fn insertLines(self: *Terminal, count: usize) void {
// Rare, but happens
if (count == 0) return;
// If the cursor is outside the scroll region we do nothing.
if (self.screen.cursor.y < self.scrolling_region.top or
self.screen.cursor.y > self.scrolling_region.bottom or
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// Remaining rows from our cursor to the bottom of the scroll region.
const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1;
// We can only insert lines up to our remaining lines in the scroll
// region. So we take whichever is smaller.
const adjusted_count = @min(count, rem);
// top is just the cursor position. insertLines starts at the cursor
// so this is our top. We want to shift lines down, down to the bottom
// of the scroll region.
const top: [*]Row = @ptrCast(self.screen.cursor.page_row);
// This is the amount of space at the bottom of the scroll region
// that will NOT be blank, so we need to shift the correct lines down.
// "scroll_amount" is the number of such lines.
const scroll_amount = rem - adjusted_count;
if (scroll_amount > 0) {
var y: [*]Row = top + (scroll_amount - 1);
// TODO: detect active area split across multiple pages
// If we have left/right scroll margins we have a slower path.
const left_right = self.scrolling_region.left > 0 or
self.scrolling_region.right < self.cols - 1;
// We work backwards so we don't overwrite data.
while (@intFromPtr(y) >= @intFromPtr(top)) : (y -= 1) {
const src: *Row = @ptrCast(y);
const dst: *Row = @ptrCast(y + adjusted_count);
if (!left_right) {
// Swap the src/dst cells. This ensures that our dst gets the proper
// shifted rows and src gets non-garbage cell data that we can clear.
const dst_row = dst.*;
dst.* = src.*;
src.* = dst_row;
continue;
}
// Left/right scroll margins we have to copy cells, which is much slower...
var page = &self.screen.cursor.page_pin.page.data;
page.moveCells(
src,
self.scrolling_region.left,
dst,
self.scrolling_region.left,
(self.scrolling_region.right - self.scrolling_region.left) + 1,
);
}
}
// Inserted lines should keep our bg color
for (0..adjusted_count) |i| {
const row: *Row = @ptrCast(top + i);
// Clear the src row.
var page = &self.screen.cursor.page_pin.page.data;
const cells = page.getCells(row);
const cells_write = cells[self.scrolling_region.left .. self.scrolling_region.right + 1];
self.screen.clearCells(page, row, cells_write);
}
// Move the cursor to the left margin. But importantly this also
// forces screen.cursor.page_cell to reload because the rows above
// shifted cell ofsets so this will ensure the cursor is pointing
// to the correct cell.
self.screen.cursorAbsolute(
self.scrolling_region.left,
self.screen.cursor.y,
);
// Always unset pending wrap
self.screen.cursor.pending_wrap = false;
}
/// Removes amount lines from the current cursor row down. The remaining lines
/// to the bottom margin are shifted up and space from the bottom margin up is
/// filled with empty lines.
///
/// If the current cursor position is outside of the current scroll region it
/// does nothing. If amount is greater than the remaining number of lines in the
/// scrolling region it is adjusted down.
///
/// In left and right margin mode the margins are respected; lines are only
/// scrolled in the scroll region.
///
/// If the cell movement splits a multi cell character that character cleared,
/// by replacing it by spaces, keeping its current attributes. All other
/// cleared space is colored according to the current SGR state.
///
/// Moves the cursor to the left margin.
pub fn deleteLines(self: *Terminal, count_req: usize) void {
// If the cursor is outside the scroll region we do nothing.
if (self.screen.cursor.y < self.scrolling_region.top or
self.screen.cursor.y > self.scrolling_region.bottom or
self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// top is just the cursor position. insertLines starts at the cursor
// so this is our top. We want to shift lines down, down to the bottom
// of the scroll region.
const top: [*]Row = @ptrCast(self.screen.cursor.page_row);
var y: [*]Row = top;
// Remaining rows from our cursor to the bottom of the scroll region.
const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1;
// The maximum we can delete is the remaining lines in the scroll region.
const count = @min(count_req, rem);
// This is the amount of space at the bottom of the scroll region
// that will NOT be blank, so we need to shift the correct lines down.
// "scroll_amount" is the number of such lines.
const scroll_amount = rem - count;
if (scroll_amount > 0) {
// If we have left/right scroll margins we have a slower path.
const left_right = self.scrolling_region.left > 0 or
self.scrolling_region.right < self.cols - 1;
const bottom: [*]Row = top + (scroll_amount - 1);
while (@intFromPtr(y) <= @intFromPtr(bottom)) : (y += 1) {
const src: *Row = @ptrCast(y + count);
const dst: *Row = @ptrCast(y);
if (!left_right) {
// Swap the src/dst cells. This ensures that our dst gets the proper
// shifted rows and src gets non-garbage cell data that we can clear.
const dst_row = dst.*;
dst.* = src.*;
src.* = dst_row;
continue;
}
// Left/right scroll margins we have to copy cells, which is much slower...
var page = &self.screen.cursor.page_pin.page.data;
page.moveCells(
src,
self.scrolling_region.left,
dst,
self.scrolling_region.left,
(self.scrolling_region.right - self.scrolling_region.left) + 1,
);
}
}
const bottom: [*]Row = top + (rem - 1);
while (@intFromPtr(y) <= @intFromPtr(bottom)) : (y += 1) {
const row: *Row = @ptrCast(y);
// Clear the src row.
var page = &self.screen.cursor.page_pin.page.data;
const cells = page.getCells(row);
const cells_write = cells[self.scrolling_region.left .. self.scrolling_region.right + 1];
self.screen.clearCells(page, row, cells_write);
}
// Move the cursor to the left margin. But importantly this also
// forces screen.cursor.page_cell to reload because the rows above
// shifted cell ofsets so this will ensure the cursor is pointing
// to the correct cell.
self.screen.cursorAbsolute(
self.scrolling_region.left,
self.screen.cursor.y,
);
// Always unset pending wrap
self.screen.cursor.pending_wrap = false;
}
/// Inserts spaces at current cursor position moving existing cell contents
/// to the right. The contents of the count right-most columns in the scroll
/// region are lost. The cursor position is not changed.
///
/// This unsets the pending wrap state without wrapping.
///
/// The inserted cells are colored according to the current SGR state.
pub fn insertBlanks(self: *Terminal, count: usize) void {
// Unset pending wrap state without wrapping. Note: this purposely
// happens BEFORE the scroll region check below, because that's what
// xterm does.
self.screen.cursor.pending_wrap = false;
// If our cursor is outside the margins then do nothing. We DO reset
// wrap state still so this must remain below the above logic.
if (self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// If our count is larger than the remaining amount, we just erase right.
// We only do this if we can erase the entire line (no right margin).
// if (right_limit == self.cols and
// count > right_limit - self.screen.cursor.x)
// {
// self.eraseLine(.right, false);
// return;
// }
// left is just the cursor position but as a multi-pointer
const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell);
var page = &self.screen.cursor.page_pin.page.data;
// Remaining cols from our cursor to the right margin.
const rem = self.scrolling_region.right - self.screen.cursor.x + 1;
// We can only insert blanks up to our remaining cols
const adjusted_count = @min(count, rem);
// This is the amount of space at the right of the scroll region
// that will NOT be blank, so we need to shift the correct cols right.
// "scroll_amount" is the number of such cols.
const scroll_amount = rem - adjusted_count;
if (scroll_amount > 0) {
var x: [*]Cell = left + (scroll_amount - 1);
// If our last cell we're shifting is wide, then we need to clear
// it to be empty so we don't split the multi-cell char.
const end: *Cell = @ptrCast(x);
if (end.wide == .wide) {
self.screen.clearCells(page, self.screen.cursor.page_row, end[0..1]);
}
// We work backwards so we don't overwrite data.
while (@intFromPtr(x) >= @intFromPtr(left)) : (x -= 1) {
const src: *Cell = @ptrCast(x);
const dst: *Cell = @ptrCast(x + adjusted_count);
// If the destination has graphemes we need to delete them.
// Graphemes are stored by cell offset so we have to do this
// now before we move.
if (dst.hasGrapheme()) {
page.clearGrapheme(self.screen.cursor.page_row, dst);
}
// Copy our src to our dst
const old_dst = dst.*;
dst.* = src.*;
src.* = old_dst;
// If the original source (now copied to dst) had graphemes,
// we have to move them since they're stored by cell offset.
if (dst.hasGrapheme()) {
assert(!src.hasGrapheme());
page.moveGraphemeWithinRow(src, dst);
}
}
}
// Insert blanks. The blanks preserve the background color.
self.screen.clearCells(page, self.screen.cursor.page_row, left[0..adjusted_count]);
}
/// Removes amount characters from the current cursor position to the right.
/// The remaining characters are shifted to the left and space from the right
/// margin is filled with spaces.
///
/// If amount is greater than the remaining number of characters in the
/// scrolling region, it is adjusted down.
///
/// Does not change the cursor position.
pub fn deleteChars(self: *Terminal, count: usize) void {
if (count == 0) return;
// If our cursor is outside the margins then do nothing. We DO reset
// wrap state still so this must remain below the above logic.
if (self.screen.cursor.x < self.scrolling_region.left or
self.screen.cursor.x > self.scrolling_region.right) return;
// This resets the pending wrap state
self.screen.cursor.pending_wrap = false;
// left is just the cursor position but as a multi-pointer
const left: [*]Cell = @ptrCast(self.screen.cursor.page_cell);
var page = &self.screen.cursor.page_pin.page.data;
// If our X is a wide spacer tail then we need to erase the
// previous cell too so we don't split a multi-cell character.
if (self.screen.cursor.page_cell.wide == .spacer_tail) {
assert(self.screen.cursor.x > 0);
self.screen.clearCells(page, self.screen.cursor.page_row, (left - 1)[0..2]);
}
// Remaining cols from our cursor to the right margin.
const rem = self.scrolling_region.right - self.screen.cursor.x + 1;
// We can only insert blanks up to our remaining cols
const adjusted_count = @min(count, rem);
// This is the amount of space at the right of the scroll region
// that will NOT be blank, so we need to shift the correct cols right.
// "scroll_amount" is the number of such cols.
const scroll_amount = rem - adjusted_count;
var x: [*]Cell = left;
if (scroll_amount > 0) {
const right: [*]Cell = left + (scroll_amount - 1);
// If our last cell we're shifting is wide, then we need to clear
// it to be empty so we don't split the multi-cell char.
const end: *Cell = @ptrCast(right + count);
if (end.wide == .spacer_tail) {
const wide: [*]Cell = right + count - 1;
assert(wide[0].wide == .wide);
self.screen.clearCells(page, self.screen.cursor.page_row, wide[0..2]);
}
while (@intFromPtr(x) <= @intFromPtr(right)) : (x += 1) {
const src: *Cell = @ptrCast(x + count);
const dst: *Cell = @ptrCast(x);
// If the destination has graphemes we need to delete them.
// Graphemes are stored by cell offset so we have to do this
// now before we move.
if (dst.hasGrapheme()) {
page.clearGrapheme(self.screen.cursor.page_row, dst);
}
// Copy our src to our dst
const old_dst = dst.*;
dst.* = src.*;
src.* = old_dst;
// If the original source (now copied to dst) had graphemes,
// we have to move them since they're stored by cell offset.
if (dst.hasGrapheme()) {
assert(!src.hasGrapheme());
page.moveGraphemeWithinRow(src, dst);
}
}
}
// Insert blanks. The blanks preserve the background color.
self.screen.clearCells(page, self.screen.cursor.page_row, x[0 .. rem - scroll_amount]);
}
pub fn eraseChars(self: *Terminal, count_req: usize) void {
const count = @max(count_req, 1);
// This resets the soft-wrap of this line
self.screen.cursor.page_row.wrap = false;
// This resets the pending wrap state
self.screen.cursor.pending_wrap = false;
// Our last index is at most the end of the number of chars we have
// in the current line.
const end = end: {
const remaining = self.cols - self.screen.cursor.x;
var end = @min(remaining, count);
// If our last cell is a wide char then we need to also clear the
// cell beyond it since we can't just split a wide char.
if (end != remaining) {
const last = self.screen.cursorCellRight(end - 1);
if (last.wide == .wide) end += 1;
}
break :end end;
};
// Clear the cells
const cells: [*]Cell = @ptrCast(self.screen.cursor.page_cell);
// If we never had a protection mode, then we can assume no cells
// are protected and go with the fast path. If the last protection
// mode was not ISO we also always ignore protection attributes.
if (self.screen.protected_mode != .iso) {
self.screen.clearCells(
&self.screen.cursor.page_pin.page.data,
self.screen.cursor.page_row,
cells[0..end],
);
return;
}
// SLOW PATH
// We had a protection mode at some point. We must go through each
// cell and check its protection attribute.
for (0..end) |x| {
const cell_multi: [*]Cell = @ptrCast(cells + x);
const cell: *Cell = @ptrCast(&cell_multi[0]);
if (cell.protected) continue;
self.screen.clearCells(
&self.screen.cursor.page_pin.page.data,
self.screen.cursor.page_row,
cell_multi[0..1],
);
}
}
/// Erase the line.
pub fn eraseLine(
self: *Terminal,
mode: csi.EraseLine,
protected_req: bool,
) void {
// Get our start/end positions depending on mode.
const start, const end = switch (mode) {
.right => right: {
var x = self.screen.cursor.x;
// If our X is a wide spacer tail then we need to erase the
// previous cell too so we don't split a multi-cell character.
if (x > 0 and self.screen.cursor.page_cell.wide == .spacer_tail) {
x -= 1;
}
// This resets the soft-wrap of this line
self.screen.cursor.page_row.wrap = false;
break :right .{ x, self.cols };
},
.left => left: {
var x = self.screen.cursor.x;
// If our x is a wide char we need to delete the tail too.
if (self.screen.cursor.page_cell.wide == .wide) {
x += 1;
}
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 },
else => {
log.err("unimplemented erase line mode: {}", .{mode});
return;
},
};
// All modes will clear the pending wrap state and we know we have
// a valid mode at this point.
self.screen.cursor.pending_wrap = false;
// Start of our cells
const cells: [*]Cell = cells: {
const cells: [*]Cell = @ptrCast(self.screen.cursor.page_cell);
break :cells cells - self.screen.cursor.x;
};
// We respect protected attributes if explicitly requested (probably
// a DECSEL sequence) or if our last protected mode was ISO even if its
// not currently set.
const protected = self.screen.protected_mode == .iso or protected_req;
// If we're not respecting protected attributes, we can use a fast-path
// to fill the entire line.
if (!protected) {
self.screen.clearCells(
&self.screen.cursor.page_pin.page.data,
self.screen.cursor.page_row,
cells[start..end],
);
return;
}
for (start..end) |x| {
const cell_multi: [*]Cell = @ptrCast(cells + x);
const cell: *Cell = @ptrCast(&cell_multi[0]);
if (cell.protected) continue;
self.screen.clearCells(
&self.screen.cursor.page_pin.page.data,
self.screen.cursor.page_row,
cell_multi[0..1],
);
}
}
/// Erase the display.
pub fn eraseDisplay(
self: *Terminal,
mode: csi.EraseDisplay,
protected_req: bool,
) void {
// We respect protected attributes if explicitly requested (probably
// a DECSEL sequence) or if our last protected mode was ISO even if its
// not currently set.
const protected = self.screen.protected_mode == .iso or protected_req;
switch (mode) {
.scroll_complete => {
self.screen.scrollClear() catch |err| {
log.warn("scroll clear failed, doing a normal clear err={}", .{err});
self.eraseDisplay(.complete, protected_req);
return;
};
// Unsets pending wrap state
self.screen.cursor.pending_wrap = false;
// Clear all Kitty graphics state for this screen
// TODO
// self.screen.kitty_images.delete(alloc, self, .{ .all = true });
},
.complete => {
// If we're on the primary screen and our last non-empty row is
// a prompt, then we do a scroll_complete instead. This is a
// heuristic to get the generally desirable behavior that ^L
// at a prompt scrolls the screen contents prior to clearing.
// Most shells send `ESC [ H ESC [ 2 J` so we can't just check
// our current cursor position. See #905
// if (self.active_screen == .primary) at_prompt: {
// // Go from the bottom of the viewport up and see if we're
// // at a prompt.
// const viewport_max = Screen.RowIndexTag.viewport.maxLen(&self.screen);
// for (0..viewport_max) |y| {
// const bottom_y = viewport_max - y - 1;
// const row = self.screen.getRow(.{ .viewport = bottom_y });
// if (row.isEmpty()) continue;
// switch (row.getSemanticPrompt()) {
// // If we're at a prompt or input area, then we are at a prompt.
// .prompt,
// .prompt_continuation,
// .input,
// => break,
//
// // If we have command output, then we're most certainly not
// // at a prompt.
// .command => break :at_prompt,
//
// // If we don't know, we keep searching.
// .unknown => {},
// }
// } else break :at_prompt;
//
// self.screen.scroll(.{ .clear = {} }) catch {
// // If we fail, we just fall back to doing a normal clear
// // so we don't worry about the error.
// };
// }
// All active area
self.screen.clearRows(
.{ .active = .{} },
null,
protected,
);
// Unsets pending wrap state
self.screen.cursor.pending_wrap = false;
// Clear all Kitty graphics state for this screen
self.screen.kitty_images.delete(
self.screen.alloc,
self,
.{ .all = true },
);
},
.below => {
// All lines to the right (including the cursor)
self.eraseLine(.right, protected_req);
// All lines below
if (self.screen.cursor.y + 1 < self.rows) {
self.screen.clearRows(
.{ .active = .{ .y = self.screen.cursor.y + 1 } },
null,
protected,
);
}
// Unsets pending wrap state. Should be done by eraseLine.
assert(!self.screen.cursor.pending_wrap);
},
.above => {
// Erase to the left (including the cursor)
self.eraseLine(.left, protected_req);
// All lines above
if (self.screen.cursor.y > 0) {
self.screen.clearRows(
.{ .active = .{ .y = 0 } },
.{ .active = .{ .y = self.screen.cursor.y - 1 } },
protected,
);
}
// Unsets pending wrap state
assert(!self.screen.cursor.pending_wrap);
},
.scrollback => self.screen.eraseRows(.{ .history = .{} }, null),
}
}
/// Resets all margins and fills the whole screen with the character 'E'
///
/// Sets the cursor to the top left corner.
pub fn decaln(self: *Terminal) !void {
// Clear our stylistic attributes. This is the only thing that can
// fail so we do it first so we can undo it.
const old_style = self.screen.cursor.style;
self.screen.cursor.style = .{
.bg_color = self.screen.cursor.style.bg_color,
.fg_color = self.screen.cursor.style.fg_color,
// TODO: protected attribute
// .protected = self.screen.cursor.pen.attrs.protected,
};
errdefer self.screen.cursor.style = old_style;
try self.screen.manualStyleUpdate();
// Reset margins, also sets cursor to top-left
self.scrolling_region = .{
.top = 0,
.bottom = self.rows - 1,
.left = 0,
.right = self.cols - 1,
};
// Origin mode is disabled
self.modes.set(.origin, false);
// Move our cursor to the top-left
self.setCursorPos(1, 1);
// Erase the display which will deallocate graphames, styles, etc.
self.eraseDisplay(.complete, false);
// Fill with Es, does not move cursor.
var it = self.screen.pages.pageIterator(.right_down, .{ .active = .{} }, null);
while (it.next()) |chunk| {
for (chunk.rows()) |*row| {
const cells_multi: [*]Cell = row.cells.ptr(chunk.page.data.memory);
const cells = cells_multi[0..self.cols];
@memset(cells, .{
.content_tag = .codepoint,
.content = .{ .codepoint = 'E' },
.style_id = self.screen.cursor.style_id,
.protected = self.screen.cursor.protected,
});
// If we have a ref-counted style, increase
if (self.screen.cursor.style_ref) |ref| {
ref.* += @intCast(cells.len);
row.styled = true;
}
}
}
}
/// Execute a kitty graphics command. The buf is used to populate with
/// the response that should be sent as an APC sequence. The response will
/// be a full, valid APC sequence.
///
/// If an error occurs, the caller should response to the pty that a
/// an error occurred otherwise the behavior of the graphics protocol is
/// undefined.
pub fn kittyGraphics(
self: *Terminal,
alloc: Allocator,
cmd: *kitty.graphics.Command,
) ?kitty.graphics.Response {
return kitty.graphics.execute(alloc, self, cmd);
}
/// Set a style attribute.
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
try self.screen.setAttribute(attr);
}
/// Print the active attributes as a string. This is used to respond to DECRQSS
/// requests.
///
/// 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();
// The SGR response always starts with a 0. See https://vt100.net/docs/vt510-rm/DECRPSS
try writer.writeByte('0');
const pen = self.screen.cursor.style;
var attrs = [_]u8{0} ** 8;
var i: usize = 0;
if (pen.flags.bold) {
attrs[i] = '1';
i += 1;
}
if (pen.flags.faint) {
attrs[i] = '2';
i += 1;
}
if (pen.flags.italic) {
attrs[i] = '3';
i += 1;
}
if (pen.flags.underline != .none) {
attrs[i] = '4';
i += 1;
}
if (pen.flags.blink) {
attrs[i] = '5';
i += 1;
}
if (pen.flags.inverse) {
attrs[i] = '7';
i += 1;
}
if (pen.flags.invisible) {
attrs[i] = '8';
i += 1;
}
if (pen.flags.strikethrough) {
attrs[i] = '9';
i += 1;
}
for (attrs[0..i]) |c| {
try writer.print(";{c}", .{c});
}
switch (pen.fg_color) {
.none => {},
.palette => |idx| if (idx >= 16)
try writer.print(";38:5:{}", .{idx})
else if (idx >= 8)
try writer.print(";9{}", .{idx - 8})
else
try writer.print(";3{}", .{idx}),
.rgb => |rgb| try writer.print(";38:2::{[r]}:{[g]}:{[b]}", rgb),
}
switch (pen.bg_color) {
.none => {},
.palette => |idx| if (idx >= 16)
try writer.print(";48:5:{}", .{idx})
else if (idx >= 8)
try writer.print(";10{}", .{idx - 8})
else
try writer.print(";4{}", .{idx}),
.rgb => |rgb| try writer.print(";48:2::{[r]}:{[g]}:{[b]}", rgb),
}
return stream.getWritten();
}
/// The modes for DECCOLM.
pub const DeccolmMode = enum(u1) {
@"80_cols" = 0,
@"132_cols" = 1,
};
/// DECCOLM changes the terminal width between 80 and 132 columns. This
/// function call will do NOTHING unless `setDeccolmSupported` has been
/// called with "true".
///
/// This breaks the expectation around modern terminals that they resize
/// with the window. This will fix the grid at either 80 or 132 columns.
/// The rows will continue to be variable.
pub fn deccolm(self: *Terminal, alloc: Allocator, mode: DeccolmMode) !void {
// If DEC mode 40 isn't enabled, then this is ignored. We also make
// sure that we don't have deccolm set because we want to fully ignore
// set mode.
if (!self.modes.get(.enable_mode_3)) {
self.modes.set(.@"132_column", false);
return;
}
// Enable it
self.modes.set(.@"132_column", mode == .@"132_cols");
// Resize to the requested size
try self.resize(
alloc,
switch (mode) {
.@"132_cols" => 132,
.@"80_cols" => 80,
},
self.rows,
);
// Erase our display and move our cursor.
self.eraseDisplay(.complete, false);
self.setCursorPos(1, 1);
}
/// Resize the underlying terminal.
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) {
self.tabstops.deinit(alloc);
self.tabstops = try Tabstops.init(alloc, cols, 8);
}
// If we're making the screen smaller, dealloc the unused items.
if (self.active_screen == .primary) {
if (self.flags.shell_redraws_prompt) {
self.screen.clearPrompt();
}
if (self.modes.get(.wraparound)) {
try self.screen.resize(cols, rows);
} else {
try self.screen.resizeWithoutReflow(cols, rows);
}
try self.secondary_screen.resizeWithoutReflow(cols, rows);
} else {
try self.screen.resizeWithoutReflow(cols, rows);
if (self.modes.get(.wraparound)) {
try self.secondary_screen.resize(cols, rows);
} else {
try self.secondary_screen.resizeWithoutReflow(cols, rows);
}
}
// Set our size
self.cols = cols;
self.rows = rows;
// Reset the scrolling region
self.scrolling_region = .{
.top = 0,
.bottom = rows - 1,
.left = 0,
.right = cols - 1,
};
}
/// Set the pwd for the terminal.
pub fn setPwd(self: *Terminal, pwd: []const u8) !void {
self.pwd.clearRetainingCapacity();
try self.pwd.appendSlice(pwd);
}
/// Returns the pwd for the terminal, if any. The memory is owned by the
/// Terminal and is not copied. It is safe until a reset or setPwd.
pub fn getPwd(self: *const Terminal) ?[]const u8 {
if (self.pwd.items.len == 0) return null;
return self.pwd.items;
}
/// Get the screen pointer for the given type.
pub fn getScreen(self: *Terminal, t: ScreenType) *Screen {
return if (self.active_screen == t)
&self.screen
else
&self.secondary_screen;
}
/// Options for switching to the alternate screen.
pub const AlternateScreenOptions = struct {
cursor_save: bool = false,
clear_on_enter: bool = false,
clear_on_exit: bool = false,
};
/// Switch to the alternate screen buffer.
///
/// The alternate screen buffer:
/// * has its own grid
/// * has its own cursor state (included saved cursor)
/// * does not support scrollback
///
pub fn alternateScreen(
self: *Terminal,
options: AlternateScreenOptions,
) void {
//log.info("alt screen active={} options={} cursor={}", .{ self.active_screen, options, self.screen.cursor });
// TODO: test
// TODO(mitchellh): what happens if we enter alternate screen multiple times?
// for now, we ignore...
if (self.active_screen == .alternate) return;
// If we requested cursor save, we save the cursor in the primary screen
if (options.cursor_save) self.saveCursor();
// Switch the screens
const old = self.screen;
self.screen = self.secondary_screen;
self.secondary_screen = old;
self.active_screen = .alternate;
// Bring our charset state with us
self.screen.charset = old.charset;
// Clear our selection
self.screen.clearSelection();
// Mark kitty images as dirty so they redraw
self.screen.kitty_images.dirty = true;
// Bring our pen with us
self.screen.cursorCopy(old.cursor) catch |err| {
log.warn("cursor copy failed entering alt screen err={}", .{err});
};
if (options.clear_on_enter) {
self.eraseDisplay(.complete, false);
}
}
/// Switch back to the primary screen (reset alternate screen mode).
pub fn primaryScreen(
self: *Terminal,
options: AlternateScreenOptions,
) void {
//log.info("primary screen active={} options={}", .{ self.active_screen, options });
// TODO: test
// TODO(mitchellh): what happens if we enter alternate screen multiple times?
if (self.active_screen == .primary) return;
if (options.clear_on_exit) self.eraseDisplay(.complete, false);
// Switch the screens
const old = self.screen;
self.screen = self.secondary_screen;
self.secondary_screen = old;
self.active_screen = .primary;
// Clear our selection
self.screen.clearSelection();
// Mark kitty images as dirty so they redraw
self.screen.kitty_images.dirty = true;
// Restore the cursor from the primary screen. This should not
// fail because we should not have to allocate memory since swapping
// screens does not create new cursors.
if (options.cursor_save) self.restoreCursor() catch |err| {
log.warn("restore cursor on primary screen failed err={}", .{err});
};
}
/// Return the current string value of the terminal. Newlines are
/// encoded as "\n". This omits any formatting such as fg/bg.
///
/// The caller must free the string.
pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 {
return try self.screen.dumpStringAlloc(alloc, .{ .viewport = .{} });
}
/// Full reset
pub fn fullReset(self: *Terminal) void {
self.primaryScreen(.{ .clear_on_exit = true, .cursor_save = true });
self.screen.charset = .{};
self.modes = .{};
self.flags = .{};
self.tabstops.reset(TABSTOP_INTERVAL);
self.screen.saved_cursor = null;
self.screen.clearSelection();
self.screen.kitty_keyboard = .{};
self.screen.protected_mode = .off;
self.scrolling_region = .{
.top = 0,
.bottom = self.rows - 1,
.left = 0,
.right = self.cols - 1,
};
self.previous_char = null;
self.eraseDisplay(.scrollback, false);
self.eraseDisplay(.complete, false);
self.screen.cursorAbsolute(0, 0);
self.pwd.clearRetainingCapacity();
self.status_display = .main;
}
test "Terminal: input with no control characters" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 40, .rows = 40 });
defer t.deinit(alloc);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 5), t.screen.cursor.x);
{
const str = try t.plainString(alloc);
defer alloc.free(str);
try testing.expectEqualStrings("hello", str);
}
}
test "Terminal: input with basic wraparound" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 40 });
defer t.deinit(alloc);
// Basic grid writing
for ("helloworldabc12") |c| try t.print(c);
try testing.expectEqual(@as(usize, 2), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
try testing.expect(t.screen.cursor.pending_wrap);
{
const str = try t.plainString(alloc);
defer alloc.free(str);
try testing.expectEqualStrings("hello\nworld\nabc12", str);
}
}
test "Terminal: input that forces scroll" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 1, .rows = 5 });
defer t.deinit(alloc);
// Basic grid writing
for ("abcdef") |c| try t.print(c);
try testing.expectEqual(@as(usize, 4), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
{
const str = try t.plainString(alloc);
defer alloc.free(str);
try testing.expectEqualStrings("b\nc\nd\ne\nf", str);
}
}
test "Terminal: input unique style per cell" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
for (0..t.rows) |y| {
for (0..t.cols) |x| {
t.setCursorPos(y, x);
try t.setAttribute(.{ .direct_color_bg = .{
.r = @intCast(x),
.g = @intCast(y),
.b = 0,
} });
try t.print('x');
}
}
}
test "Terminal: zero-width character at start" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// This used to crash the terminal. This is not allowed so we should
// just ignore it.
try t.print(0x200D);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
}
// https://github.com/mitchellh/ghostty/issues/1400
test "Terminal: print single very long line" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// This would crash for issue 1400. So the assertion here is
// that we simply do not crash.
for (0..1000) |_| try t.print('x');
}
test "Terminal: print wide char" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x1F600), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.wide, cell.wide);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
}
}
test "Terminal: print wide char with 1-column width" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 1, .rows = 2 });
defer t.deinit(alloc);
try t.print('😀'); // 0x1F600
}
test "Terminal: print wide char in single-width terminal" {
var t = try init(testing.allocator, .{ .cols = 1, .rows = 80 });
defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expect(t.screen.cursor.pending_wrap);
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: print over wide char at 0,0" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.print(0x1F600); // Smiley face
t.setCursorPos(0, 0);
try t.print('A'); // Smiley face
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.x);
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: print over wide spacer tail" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
try t.print('橋');
t.setCursorPos(1, 2);
try t.print('X');
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'X'), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// https://github.com/mitchellh/ghostty/issues/289
// This is: 👨‍👩‍👧 (which may or may not render correctly)
try t.print(0x1F468);
try t.print(0x200D);
try t.print(0x1F469);
try t.print(0x200D);
try t.print(0x1F467);
// We should have 6 cells taken up
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 6), t.screen.cursor.x);
// Assert various properties about our screen to verify
// we have all expected cells.
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x1F469), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x1F467), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
}
}
test "Terminal: VS16 doesn't make character with 2027 disabled" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Disable grapheme clustering
t.modes.set(.grapheme_cluster, false);
try t.print(0x2764); // Heart
try t.print(0xFE0F); // VS16 to make wide
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("❤️", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
}
test "Terminal: print invalid VS16 non-grapheme" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// https://github.com/mitchellh/ghostty/issues/1482
try t.print('x');
try t.print(0xFE0F);
// We should have 2 cells taken up. It is one character but "wide".
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.x);
// Assert various properties about our screen to verify
// we have all expected cells.
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
}
}
test "Terminal: print multicodepoint grapheme, mode 2027" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/289
// This is: 👨‍👩‍👧 (which may or may not render correctly)
try t.print(0x1F468);
try t.print(0x200D);
try t.print(0x1F469);
try t.print(0x200D);
try t.print(0x1F467);
// We should have 2 cells taken up. It is one character but "wide".
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
// Assert various properties about our screen to verify
// we have all expected cells.
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x1F468), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 4), cps.len);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
}
}
test "Terminal: VS15 to make narrow character" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.print(0x26C8); // Thunder cloud and rain
try t.print(0xFE0E); // VS15 to make narrow
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("⛈︎", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x26C8), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
}
test "Terminal: VS16 to make wide character with mode 2027" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.print(0x2764); // Heart
try t.print(0xFE0F); // VS16 to make wide
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("❤️", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
}
test "Terminal: VS16 repeated with mode 2027" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.print(0x2764); // Heart
try t.print(0xFE0F); // VS16 to make wide
try t.print(0x2764); // Heart
try t.print(0xFE0F); // VS16 to make wide
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("❤️❤️", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0x2764), cell.content.codepoint);
try testing.expect(cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.wide, cell.wide);
const cps = list_cell.page.data.lookupGrapheme(cell).?;
try testing.expectEqual(@as(usize, 1), cps.len);
}
}
test "Terminal: print invalid VS16 grapheme" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/1482
try t.print('x');
try t.print(0xFE0F);
// We should have 2 cells taken up. It is one character but "wide".
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.x);
// Assert various properties about our screen to verify
// we have all expected cells.
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
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 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
// https://github.com/mitchellh/ghostty/issues/1482
try t.print('x');
try t.print(0xFE0F);
try t.print('y');
// We should have 2 cells taken up. It is one character but "wide".
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
// Assert various properties about our screen to verify
// we have all expected cells.
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'x'), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'y'), cell.content.codepoint);
try testing.expect(!cell.hasGrapheme());
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 });
defer t.deinit(testing.allocator);
// Enable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.print(0x26C8); // Thunder cloud and rain
try t.print(0xFE0E); // VS15 to make narrow
t.setCursorPos(1, 1);
try t.print('A');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
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 });
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
t.setCursorPos(0, 0);
// Make newlines so we create scrollback
// 3 pushes hello off the screen
try t.index();
try t.index();
try t.index();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
// Scroll to the top
t.screen.scroll(.{ .top = {} });
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello", str);
}
// Type
try t.print('A');
t.screen.scroll(.{ .active = {} });
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nA", str);
}
}
test "Terminal: print charset" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// G1 should have no effect
t.configureCharset(.G1, .dec_special);
t.configureCharset(.G2, .dec_special);
t.configureCharset(.G3, .dec_special);
// Basic grid writing
try t.print('`');
t.configureCharset(.G0, .utf8);
try t.print('`');
t.configureCharset(.G0, .ascii);
try t.print('`');
t.configureCharset(.G0, .dec_special);
try t.print('`');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("```◆", str);
}
}
test "Terminal: print charset outside of ASCII" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// G1 should have no effect
t.configureCharset(.G1, .dec_special);
t.configureCharset(.G2, .dec_special);
t.configureCharset(.G3, .dec_special);
// Basic grid writing
t.configureCharset(.G0, .dec_special);
try t.print('`');
try t.print(0x1F600);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: print invoke charset" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
t.configureCharset(.G1, .dec_special);
// Basic grid writing
try t.print('`');
t.invokeCharset(.GL, .G1, false);
try t.print('`');
try t.print('`');
t.invokeCharset(.GL, .G0, false);
try t.print('`');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("`◆◆`", str);
}
}
test "Terminal: print invoke charset single" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
t.configureCharset(.G1, .dec_special);
// Basic grid writing
try t.print('`');
t.invokeCharset(.GL, .G1, true);
try t.print('`');
try t.print('`');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("`◆`", str);
}
}
test "Terminal: soft wrap" {
var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hel\nlo", str);
}
}
test "Terminal: soft wrap with semantic prompt" {
var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
defer t.deinit(testing.allocator);
t.markSemanticPrompt(.prompt);
for ("hello") |c| try t.print(c);
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(Row.SemanticPrompt.prompt, list_cell.row.semantic_prompt);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
try testing.expectEqual(Row.SemanticPrompt.prompt, list_cell.row.semantic_prompt);
}
}
test "Terminal: disabled wraparound with wide char and one space" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.wraparound, false);
// This puts our cursor at the end and there is NO SPACE for a
// wide character.
try t.printString("AAAA");
try t.print(0x1F6A8); // Police car light
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AAAA", str);
}
// Make sure we printed nothing
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: disabled wraparound with wide char and no space" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.wraparound, false);
// This puts our cursor at the end and there is NO SPACE for a
// wide character.
try t.printString("AAAAA");
try t.print(0x1F6A8); // Police car light
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AAAAA", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: disabled wraparound with wide grapheme and half space" {
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
defer t.deinit(testing.allocator);
t.modes.set(.grapheme_cluster, true);
t.modes.set(.wraparound, false);
// This puts our cursor at the end and there is NO SPACE for a
// wide character.
try t.printString("AAAA");
try t.print(0x2764); // Heart
try t.print(0xFE0F); // VS16 to make wide
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AAAA❤", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, '❤'), cell.content.codepoint);
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
}
}
test "Terminal: print right margin wrap" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(3, 5);
t.setCursorPos(1, 5);
try t.printString("XY");
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("1234X6789\n Y", str);
}
}
test "Terminal: print right margin outside" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(3, 5);
t.setCursorPos(1, 6);
try t.printString("XY");
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("12345XY89", str);
}
}
test "Terminal: print right margin outside wrap" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.printString("123456789");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(3, 5);
t.setCursorPos(1, 10);
try t.printString("XY");
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("123456789X\n Y", str);
}
}
test "Terminal: linefeed and carriage return" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("world") |c| try t.print(c);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 5), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello\nworld", str);
}
}
test "Terminal: linefeed unsets pending wrap" {
var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap == true);
try t.linefeed();
try testing.expect(t.screen.cursor.pending_wrap == false);
}
test "Terminal: linefeed mode automatic carriage return" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
// Basic grid writing
t.modes.set(.linefeed, true);
try t.printString("123456");
try t.linefeed();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("123456\nX", str);
}
}
test "Terminal: carriage return unsets pending wrap" {
var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap == true);
t.carriageReturn();
try testing.expect(t.screen.cursor.pending_wrap == false);
}
test "Terminal: carriage return origin mode moves to left margin" {
var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
t.modes.set(.origin, true);
t.screen.cursor.x = 0;
t.scrolling_region.left = 2;
t.carriageReturn();
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
}
test "Terminal: carriage return left of left margin moves to zero" {
var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
t.screen.cursor.x = 1;
t.scrolling_region.left = 2;
t.carriageReturn();
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
}
test "Terminal: carriage return right of left margin moves to left margin" {
var t = try init(testing.allocator, .{ .cols = 5, .rows = 80 });
defer t.deinit(testing.allocator);
t.screen.cursor.x = 3;
t.scrolling_region.left = 2;
t.carriageReturn();
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
}
test "Terminal: backspace" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
// BS
for ("hello") |c| try t.print(c);
t.backspace();
try t.print('y');
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 5), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("helly", str);
}
}
test "Terminal: horizontal tabs" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
// HT
try t.print('1');
try t.horizontalTab();
try testing.expectEqual(@as(usize, 8), t.screen.cursor.x);
// HT
try t.horizontalTab();
try testing.expectEqual(@as(usize, 16), t.screen.cursor.x);
// HT at the end
try t.horizontalTab();
try testing.expectEqual(@as(usize, 19), t.screen.cursor.x);
try t.horizontalTab();
try testing.expectEqual(@as(usize, 19), t.screen.cursor.x);
}
test "Terminal: horizontal tabs starting on tabstop" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(t.screen.cursor.y, 9);
try t.print('X');
t.setCursorPos(t.screen.cursor.y, 9);
try t.horizontalTab();
try t.print('A');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X A", str);
}
}
test "Terminal: horizontal tabs with right margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.scrolling_region.left = 2;
t.scrolling_region.right = 5;
t.setCursorPos(t.screen.cursor.y, 1);
try t.print('X');
try t.horizontalTab();
try t.print('A');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X A", str);
}
}
test "Terminal: horizontal tabs back" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
// Edge of screen
t.setCursorPos(t.screen.cursor.y, 20);
// HT
try t.horizontalTabBack();
try testing.expectEqual(@as(usize, 16), t.screen.cursor.x);
// HT
try t.horizontalTabBack();
try testing.expectEqual(@as(usize, 8), t.screen.cursor.x);
// HT
try t.horizontalTabBack();
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try t.horizontalTabBack();
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
}
test "Terminal: horizontal tabs back starting on tabstop" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(t.screen.cursor.y, 9);
try t.print('X');
t.setCursorPos(t.screen.cursor.y, 9);
try t.horizontalTabBack();
try t.print('A');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A X", str);
}
}
test "Terminal: horizontal tabs with left margin in origin mode" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
t.scrolling_region.left = 2;
t.scrolling_region.right = 5;
t.setCursorPos(1, 2);
try t.print('X');
try t.horizontalTabBack();
try t.print('A');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" AX", str);
}
}
test "Terminal: horizontal tab back with cursor before left margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 20, .rows = 5 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
t.saveCursor();
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(5, 0);
try t.restoreCursor();
try t.horizontalTabBack();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X", str);
}
}
test "Terminal: cursorPos resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.setCursorPos(1, 1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("XBCDE", str);
}
}
test "Terminal: cursorPos off the screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(500, 500);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\n\n X", str);
}
}
test "Terminal: cursorPos relative to origin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.modes.set(.origin, true);
t.setCursorPos(1, 1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\nX", str);
}
}
test "Terminal: cursorPos relative to origin with left/right" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.modes.set(.origin, true);
t.setCursorPos(1, 1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n X", str);
}
}
test "Terminal: cursorPos limits with full scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.top = 2;
t.scrolling_region.bottom = 3;
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.modes.set(.origin, true);
t.setCursorPos(500, 500);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\n X", str);
}
}
// 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 });
defer t.deinit(testing.allocator);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
// Setting it to 0 should keep it zero (1 based)
t.setCursorPos(0, 0);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
// Should clamp to size
t.setCursorPos(81, 81);
try testing.expectEqual(@as(usize, 79), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 79), t.screen.cursor.y);
// Should reset pending wrap
t.setCursorPos(0, 80);
try t.print('c');
try testing.expect(t.screen.cursor.pending_wrap);
t.setCursorPos(0, 80);
try testing.expect(!t.screen.cursor.pending_wrap);
// Origin mode
t.modes.set(.origin, true);
// No change without a scroll region
t.setCursorPos(81, 81);
try testing.expectEqual(@as(usize, 79), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 79), t.screen.cursor.y);
// Set the scroll region
t.setTopAndBottomMargin(10, t.rows);
t.setCursorPos(0, 0);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 9), t.screen.cursor.y);
t.setCursorPos(1, 1);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 9), t.screen.cursor.y);
t.setCursorPos(100, 0);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 79), t.screen.cursor.y);
t.setTopAndBottomMargin(10, 11);
t.setCursorPos(2, 0);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 10), t.screen.cursor.y);
}
test "Terminal: setTopAndBottomMargin simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(0, 0);
t.scrollDown(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nABC\nDEF\nGHI", str);
}
}
test "Terminal: setTopAndBottomMargin top only" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(2, 0);
t.scrollDown(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str);
}
}
test "Terminal: setTopAndBottomMargin top and bottom" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(1, 2);
t.scrollDown(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nABC\nGHI", str);
}
}
test "Terminal: setTopAndBottomMargin top equal to bottom" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(2, 2);
t.scrollDown(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nABC\nDEF\nGHI", str);
}
}
test "Terminal: setLeftAndRightMargin simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(0, 0);
t.eraseChars(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" BC\nDEF\nGHI", str);
}
}
test "Terminal: setLeftAndRightMargin left only" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(2, 0);
try testing.expectEqual(@as(usize, 1), t.scrolling_region.left);
try testing.expectEqual(@as(usize, t.cols - 1), t.scrolling_region.right);
t.setCursorPos(1, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str);
}
}
test "Terminal: setLeftAndRightMargin left and right" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(1, 2);
t.setCursorPos(1, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C\nABF\nDEI\nGH", str);
}
}
test "Terminal: setLeftAndRightMargin left equal right" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(2, 2);
t.setCursorPos(1, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nABC\nDEF\nGHI", str);
}
}
test "Terminal: setLeftAndRightMargin mode 69 unset" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, false);
t.setLeftAndRightMargin(1, 2);
t.setCursorPos(1, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nABC\nDEF\nGHI", str);
}
}
test "Terminal: insertLines simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str);
}
}
test "Terminal: insertLines colors with bg color" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str);
}
for (0..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
test "Terminal: insertLines handles style refs" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
// For the line being deleted, create a refcounted style
try t.setAttribute(.{ .bold = {} });
try t.printString("GHI");
try t.setAttribute(.{ .unset = {} });
// verify we have styles in our style map
const page = t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory));
t.setCursorPos(2, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF", str);
}
// verify we have no styles in our style map
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory));
}
test "Terminal: insertLines outside of scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(3, 4);
t.setCursorPos(2, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: insertLines top/bottom scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.carriageReturn();
try t.linefeed();
try t.printString("123");
t.setTopAndBottomMargin(1, 3);
t.setCursorPos(2, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\nDEF\n123", str);
}
}
test "Terminal: insertLines (legacy test)" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.carriageReturn();
try t.linefeed();
try t.print('E');
// Move to row 2
t.setCursorPos(2, 1);
// Insert two lines
t.insertLines(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n\n\nB\nC", str);
}
}
test "Terminal: insertLines zero" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// This should do nothing
t.setCursorPos(1, 1);
t.insertLines(0);
}
test "Terminal: insertLines with scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 6 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.carriageReturn();
try t.linefeed();
try t.print('E');
t.setTopAndBottomMargin(1, 2);
t.setCursorPos(1, 1);
t.insertLines(1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X\nA\nC\nD\nE", str);
}
}
test "Terminal: insertLines more than remaining" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.carriageReturn();
try t.linefeed();
try t.print('E');
// Move to row 2
t.setCursorPos(2, 1);
// Insert a bunch of lines
t.insertLines(20);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
test "Terminal: insertLines resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.insertLines(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('B');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("B\nABCDE", str);
}
}
test "Terminal: insertLines multi-codepoint graphemes" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Disable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
// This is: 👨‍👩‍👧 (which may or may not render correctly)
try t.print(0x1F468);
try t.print(0x200D);
try t.print(0x1F469);
try t.print(0x200D);
try t.print(0x1F467);
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\n\n👨‍👩‍👧\nGHI", str);
}
}
test "Terminal: insertLines left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
t.insertLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123\nD 56\nGEF489\n HI7", str);
}
}
test "Terminal: scrollUp simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
const cursor = t.screen.cursor;
t.scrollUp(1);
try testing.expectEqual(cursor.x, t.screen.cursor.x);
try testing.expectEqual(cursor.y, t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("DEF\nGHI", str);
}
}
test "Terminal: scrollUp top/bottom scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(2, 3);
t.setCursorPos(1, 1);
t.scrollUp(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nGHI", str);
}
}
test "Terminal: scrollUp left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
const cursor = t.screen.cursor;
t.scrollUp(1);
try testing.expectEqual(cursor.x, t.screen.cursor.x);
try testing.expectEqual(cursor.y, t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str);
}
}
test "Terminal: scrollUp preserves pending wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
try t.print('A');
t.setCursorPos(2, 5);
try t.print('B');
t.setCursorPos(3, 5);
try t.print('C');
t.scrollUp(1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" B\n C\n\nX", str);
}
}
test "Terminal: scrollUp full top/bottom region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("top");
t.setCursorPos(5, 1);
try t.printString("ABCDE");
t.setTopAndBottomMargin(2, 5);
t.scrollUp(4);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("top", str);
}
}
test "Terminal: scrollUp full top/bottomleft/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("top");
t.setCursorPos(5, 1);
try t.printString("ABCDE");
t.modes.set(.enable_left_and_right_margin, true);
t.setTopAndBottomMargin(2, 5);
t.setLeftAndRightMargin(2, 4);
t.scrollUp(4);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("top\n\n\n\nA E", str);
}
}
test "Terminal: scrollDown simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
const cursor = t.screen.cursor;
t.scrollDown(1);
try testing.expectEqual(cursor.x, t.screen.cursor.x);
try testing.expectEqual(cursor.y, t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nABC\nDEF\nGHI", str);
}
}
test "Terminal: scrollDown outside of scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setTopAndBottomMargin(3, 4);
t.setCursorPos(2, 2);
const cursor = t.screen.cursor;
t.scrollDown(1);
try testing.expectEqual(cursor.x, t.screen.cursor.x);
try testing.expectEqual(cursor.y, t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\n\nGHI", str);
}
}
test "Terminal: scrollDown left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
const cursor = t.screen.cursor;
t.scrollDown(1);
try testing.expectEqual(cursor.x, t.screen.cursor.x);
try testing.expectEqual(cursor.y, t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str);
}
}
test "Terminal: scrollDown outside of left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(1, 1);
const cursor = t.screen.cursor;
t.scrollDown(1);
try testing.expectEqual(cursor.x, t.screen.cursor.x);
try testing.expectEqual(cursor.y, t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A 23\nDBC156\nGEF489\n HI7", str);
}
}
test "Terminal: scrollDown preserves pending wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
try t.print('A');
t.setCursorPos(2, 5);
try t.print('B');
t.setCursorPos(3, 5);
try t.print('C');
t.scrollDown(1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n A\n B\nX C", str);
}
}
test "Terminal: eraseChars simple operation" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(2);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X C", str);
}
}
test "Terminal: eraseChars minimum one" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(0);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("XBC", str);
}
}
test "Terminal: eraseChars beyond screen edge" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for (" ABC") |c| try t.print(c);
t.setCursorPos(1, 4);
t.eraseChars(10);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" A", str);
}
}
test "Terminal: eraseChars wide character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('橋');
for ("BC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X BC", str);
}
}
test "Terminal: eraseChars resets pending wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.eraseChars(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}
test "Terminal: eraseChars resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c);
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
const row = list_cell.row;
try testing.expect(row.wrap);
}
t.setCursorPos(1, 1);
t.eraseChars(1);
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
const row = list_cell.row;
try testing.expect(!row.wrap);
}
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("XBCDE\n123", str);
}
}
test "Terminal: eraseChars preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.eraseChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 1, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: eraseChars handles refcounted styles" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
try t.print('A');
try t.print('B');
try t.setAttribute(.{ .unset = {} });
try t.print('C');
// verify we have styles in our style map
const page = t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory));
t.setCursorPos(1, 1);
t.eraseChars(2);
// verify we have no styles in our style map
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory));
}
test "Terminal: eraseChars protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseChars protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(1, 1);
t.eraseChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: eraseChars protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: reverseIndex" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.reverseIndex();
try t.print('D');
t.carriageReturn();
try t.linefeed();
t.carriageReturn();
try t.linefeed();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nBD\nC", str);
}
}
test "Terminal: reverseIndex from the top" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
t.carriageReturn();
try t.linefeed();
t.setCursorPos(1, 1);
t.reverseIndex();
try t.print('D');
t.carriageReturn();
try t.linefeed();
t.setCursorPos(1, 1);
t.reverseIndex();
try t.print('E');
t.carriageReturn();
try t.linefeed();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("E\nD\nA\nB", str);
}
}
test "Terminal: reverseIndex top of scrolling region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 10 });
defer t.deinit(alloc);
// Initial value
t.setCursorPos(2, 1);
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.carriageReturn();
try t.linefeed();
// Set our scroll region
t.setTopAndBottomMargin(2, 5);
t.setCursorPos(2, 1);
t.reverseIndex();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nX\nA\nB\nC", str);
}
}
test "Terminal: reverseIndex top of screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
t.setCursorPos(2, 1);
try t.print('B');
t.setCursorPos(3, 1);
try t.print('C');
t.setCursorPos(1, 1);
t.reverseIndex();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X\nA\nB\nC", str);
}
}
test "Terminal: reverseIndex not top of screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
t.setCursorPos(2, 1);
try t.print('B');
t.setCursorPos(3, 1);
try t.print('C');
t.setCursorPos(2, 1);
t.reverseIndex();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X\nB\nC", str);
}
}
test "Terminal: reverseIndex top/bottom margins" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
t.setCursorPos(2, 1);
try t.print('B');
t.setCursorPos(3, 1);
try t.print('C');
t.setTopAndBottomMargin(2, 3);
t.setCursorPos(2, 1);
t.reverseIndex();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n\nB", str);
}
}
test "Terminal: reverseIndex outside top/bottom margins" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
t.setCursorPos(2, 1);
try t.print('B');
t.setCursorPos(3, 1);
try t.print('C');
t.setTopAndBottomMargin(2, 3);
t.setCursorPos(1, 1);
t.reverseIndex();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nB\nC", str);
}
}
test "Terminal: reverseIndex left/right margins" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.setCursorPos(2, 1);
try t.printString("DEF");
t.setCursorPos(3, 1);
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(2, 3);
t.setCursorPos(1, 2);
t.reverseIndex();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nDBC\nGEF\n HI", str);
}
}
test "Terminal: reverseIndex outside left/right margins" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.setCursorPos(2, 1);
try t.printString("DEF");
t.setCursorPos(3, 1);
try t.printString("GHI");
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(2, 3);
t.setCursorPos(1, 1);
t.reverseIndex();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: index" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
try t.index();
try t.print('A');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nA", str);
}
}
test "Terminal: index from the bottom" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
try t.print('A');
t.cursorLeft(1); // undo moving right from 'A'
try t.index();
try t.print('B');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\nA\nB", str);
}
}
test "Terminal: index outside of scrolling region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
t.setTopAndBottomMargin(2, 5);
try t.index();
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
}
test "Terminal: index from the bottom outside of scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 2);
t.setCursorPos(5, 1);
try t.print('A');
try t.index();
try t.print('B');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\n\nAB", str);
}
}
test "Terminal: index no scroll region, top of screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
try t.index();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n X", str);
}
}
test "Terminal: index bottom of primary screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
try t.print('A');
try t.index();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\nA\n X", str);
}
}
test "Terminal: index bottom of primary screen background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(5, 1);
try t.print('A');
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
try t.index();
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\nA", str);
for (0..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: index inside scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
try t.print('A');
try t.index();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n X", str);
}
}
test "Terminal: index bottom of primary screen with scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
t.setCursorPos(3, 1);
try t.print('A');
t.setCursorPos(5, 1);
try t.index();
try t.index();
try t.index();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\nA\n\nX", str);
}
}
test "Terminal: index outside left/right margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
t.scrolling_region.left = 3;
t.scrolling_region.right = 5;
t.setCursorPos(3, 3);
try t.print('A');
t.setCursorPos(3, 1);
try t.index();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\nX A", str);
}
}
test "Terminal: index inside left/right margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.printString("AAAAAA");
t.carriageReturn();
try t.linefeed();
try t.printString("AAAAAA");
t.carriageReturn();
try t.linefeed();
try t.printString("AAAAAA");
t.modes.set(.enable_left_and_right_margin, true);
t.setTopAndBottomMargin(1, 3);
t.setLeftAndRightMargin(1, 3);
t.setCursorPos(3, 1);
try t.index();
try testing.expectEqual(@as(usize, 2), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AAAAAA\nAAAAAA\n AAA", str);
}
}
test "Terminal: index bottom of scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
t.setCursorPos(4, 1);
try t.print('B');
t.setCursorPos(3, 1);
try t.print('A');
try t.index();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nA\n X\nB", str);
}
}
test "Terminal: cursorUp basic" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(3, 1);
try t.print('A');
t.cursorUp(10);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X\n\nA", str);
}
}
test "Terminal: cursorUp below top scroll margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(2, 4);
t.setCursorPos(3, 1);
try t.print('A');
t.cursorUp(5);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n X\nA", str);
}
}
test "Terminal: cursorUp above top scroll margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(3, 5);
t.setCursorPos(3, 1);
try t.print('A');
t.setCursorPos(2, 1);
t.cursorUp(10);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X\n\nA", str);
}
}
test "Terminal: cursorUp resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorUp(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}
test "Terminal: cursorLeft no wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.cursorLeft(10);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nB", str);
}
}
test "Terminal: cursorLeft unsets pending wrap state" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorLeft(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCXE", str);
}
}
test "Terminal: cursorLeft unsets pending wrap state with longer jump" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorLeft(3);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AXCDE", str);
}
}
test "Terminal: cursorLeft reverse wrap with pending wrap state" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap, true);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorLeft(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}
test "Terminal: cursorLeft reverse wrap extended with pending wrap state" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorLeft(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}
test "Terminal: cursorLeft reverse wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap, true);
for ("ABCDE1") |c| try t.print(c);
t.cursorLeft(2);
try t.print('X');
try testing.expect(t.screen.cursor.pending_wrap);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX\n1", str);
}
}
test "Terminal: cursorLeft reverse wrap with no soft wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap, true);
for ("ABCDE") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
try t.print('1');
t.cursorLeft(2);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDE\nX", str);
}
}
test "Terminal: cursorLeft reverse wrap before left margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap, true);
t.setTopAndBottomMargin(3, 0);
t.cursorLeft(1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\nX", str);
}
}
test "Terminal: cursorLeft extended reverse wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
try t.print('1');
t.cursorLeft(2);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX\n1", str);
}
}
test "Terminal: cursorLeft extended reverse wrap bottom wraparound" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
try t.print('1');
t.cursorLeft(1 + t.cols + 1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDE\n1\n X", str);
}
}
test "Terminal: cursorLeft extended reverse wrap is priority if both set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 3 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap, true);
t.modes.set(.reverse_wrap_extended, true);
for ("ABCDE") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
try t.print('1');
t.cursorLeft(1 + t.cols + 1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDE\n1\n X", str);
}
}
test "Terminal: cursorLeft extended reverse wrap above top scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap_extended, true);
t.setTopAndBottomMargin(3, 0);
t.setCursorPos(2, 1);
t.cursorLeft(1000);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
}
test "Terminal: cursorLeft reverse wrap on first row" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
t.modes.set(.reverse_wrap, true);
t.setTopAndBottomMargin(3, 0);
t.setCursorPos(1, 2);
t.cursorLeft(1000);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
}
test "Terminal: cursorDown basic" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
t.cursorDown(10);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n\n\n\n X", str);
}
}
test "Terminal: cursorDown above bottom scroll margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
try t.print('A');
t.cursorDown(10);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n\n X", str);
}
}
test "Terminal: cursorDown below bottom scroll margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setTopAndBottomMargin(1, 3);
try t.print('A');
t.setCursorPos(4, 1);
t.cursorDown(10);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n\n\n\nX", str);
}
}
test "Terminal: cursorDown resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorDown(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDE\n X", str);
}
}
test "Terminal: cursorRight resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.cursorRight(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}
test "Terminal: cursorRight to the edge of screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.cursorRight(100);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: cursorRight left of right margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.right = 2;
t.cursorRight(100);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: cursorRight right of right margin" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.scrolling_region.right = 2;
t.setCursorPos(1, 4);
t.cursorRight(100);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: deleteLines simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
t.deleteLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nGHI", str);
}
}
test "Terminal: deleteLines colors with bg color" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("ABC");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI");
t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.deleteLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nGHI", str);
}
for (0..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
test "Terminal: deleteLines (legacy)" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.cursorUp(2);
t.deleteLines(1);
try t.print('E');
t.carriageReturn();
try t.linefeed();
// We should be
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expectEqual(@as(usize, 2), t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nE\nD", str);
}
}
test "Terminal: deleteLines with scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.setTopAndBottomMargin(1, 3);
t.setCursorPos(1, 1);
t.deleteLines(1);
try t.print('E');
t.carriageReturn();
try t.linefeed();
// We should be
// try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
// try testing.expectEqual(@as(usize, 2), t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("E\nC\n\nD", str);
}
}
// X
test "Terminal: deleteLines with scroll region, large count" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.setTopAndBottomMargin(1, 3);
t.setCursorPos(1, 1);
t.deleteLines(5);
try t.print('E');
t.carriageReturn();
try t.linefeed();
// We should be
// try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
// try testing.expectEqual(@as(usize, 2), t.screen.cursor.y);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("E\n\n\nD", str);
}
}
// X
test "Terminal: deleteLines with scroll region, cursor outside of region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 80, .rows = 80 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
t.carriageReturn();
try t.linefeed();
try t.print('C');
t.carriageReturn();
try t.linefeed();
try t.print('D');
t.setTopAndBottomMargin(1, 3);
t.setCursorPos(4, 1);
t.deleteLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\nB\nC\nD", str);
}
}
test "Terminal: deleteLines resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.deleteLines(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('B');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("B", str);
}
}
test "Terminal: deleteLines left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
t.deleteLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123\nDHI756\nG 89", str);
}
}
test "Terminal: deleteLines left/right scroll region from top" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(1, 2);
t.deleteLines(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AEF423\nDHI756\nG 89", str);
}
}
test "Terminal: deleteLines left/right scroll region high count" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.carriageReturn();
try t.linefeed();
try t.printString("DEF456");
t.carriageReturn();
try t.linefeed();
try t.printString("GHI789");
t.scrolling_region.left = 1;
t.scrolling_region.right = 3;
t.setCursorPos(2, 2);
t.deleteLines(100);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123\nD 56\nG 89", str);
}
}
test "Terminal: default style is empty" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.print('A');
{
const list_cell = t.screen.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(style.Id, 0), cell.style_id);
}
}
test "Terminal: bold style" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
try t.print('A');
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint);
try testing.expect(cell.style_id != 0);
try testing.expect(t.screen.cursor.style_ref.?.* > 0);
}
}
test "Terminal: garbage collect overwritten" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
try t.print('A');
t.setCursorPos(1, 1);
try t.setAttribute(.{ .unset = {} });
try t.print('B');
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint);
try testing.expect(cell.style_id == 0);
}
// verify we have no styles in our style map
const page = t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory));
}
test "Terminal: do not garbage collect old styles in use" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
try t.print('A');
try t.setAttribute(.{ .unset = {} });
try t.print('B');
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell;
try testing.expectEqual(@as(u21, 'B'), cell.content.codepoint);
try testing.expect(cell.style_id == 0);
}
// verify we have no styles in our style map
const page = t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory));
}
test "Terminal: print with style marks the row as styled" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
try t.print('A');
try t.setAttribute(.{ .unset = {} });
try t.print('B');
{
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expect(list_cell.row.styled);
}
}
test "Terminal: DECALN" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 2, .rows = 2 });
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
try t.linefeed();
try t.print('B');
try t.decaln();
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("EE\nEE", str);
}
}
test "Terminal: decaln reset margins" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
// Initial value
t.modes.set(.origin, true);
t.setTopAndBottomMargin(2, 3);
try t.decaln();
t.scrollDown(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nEEE\nEEE", str);
}
}
test "Terminal: decaln preserves color" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
// Initial value
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0 } });
t.modes.set(.origin, true);
t.setTopAndBottomMargin(2, 3);
try t.decaln();
t.scrollDown(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nEEE\nEEE", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
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 });
defer t.deinit(alloc);
try t.print('A');
try t.print('B');
try t.print('C');
t.setCursorPos(1, 1);
t.insertBlanks(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" ABC", str);
}
}
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 });
defer t.deinit(alloc);
try t.print('A');
try t.print('B');
try t.print('C');
t.setCursorPos(1, 1);
t.insertBlanks(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" A", str);
}
}
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 });
defer t.deinit(alloc);
try t.print('A');
try t.print('B');
try t.print('C');
t.setCursorPos(1, 1);
t.insertBlanks(5);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: insertBlanks no scroll region, fits" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.insertBlanks(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" ABC", str);
}
}
test "Terminal: insertBlanks preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.insertBlanks(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" ABC", str);
}
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
test "Terminal: insertBlanks shift off screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
for (" ABC") |c| try t.print(c);
t.setCursorPos(1, 3);
t.insertBlanks(2);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X A", str);
}
}
test "Terminal: insertBlanks split multi-cell character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 10 });
defer t.deinit(alloc);
for ("123") |c| try t.print(c);
try t.print('橋');
t.setCursorPos(1, 1);
t.insertBlanks(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" 123", str);
}
}
test "Terminal: insertBlanks inside left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.setCursorPos(1, 3);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 3);
t.insertBlanks(2);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X A", str);
}
}
test "Terminal: insertBlanks outside left/right scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
t.setCursorPos(1, 4);
for ("ABC") |c| try t.print(c);
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
try testing.expect(t.screen.cursor.pending_wrap);
t.insertBlanks(2);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" ABX", str);
}
}
test "Terminal: insertBlanks left/right scroll region large count" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(3, 5);
t.setCursorPos(1, 1);
t.insertBlanks(140);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: insertBlanks deleting graphemes" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Disable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.printString("ABC");
// This is: 👨‍👩‍👧 (which may or may not render correctly)
try t.print(0x1F468);
try t.print(0x200D);
try t.print(0x1F469);
try t.print(0x200D);
try t.print(0x1F467);
// We should have one cell with graphemes
const page = t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.graphemeCount());
t.setCursorPos(1, 1);
t.insertBlanks(4);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" A", str);
}
// We should have no graphemes
try testing.expectEqual(@as(usize, 0), page.graphemeCount());
}
test "Terminal: insertBlanks shift graphemes" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Disable grapheme clustering
t.modes.set(.grapheme_cluster, true);
try t.printString("A");
// This is: 👨‍👩‍👧 (which may or may not render correctly)
try t.print(0x1F468);
try t.print(0x200D);
try t.print(0x1F469);
try t.print(0x200D);
try t.print(0x1F467);
// We should have one cell with graphemes
const page = t.screen.cursor.page_pin.page.data;
try testing.expectEqual(@as(usize, 1), page.graphemeCount());
t.setCursorPos(1, 1);
t.insertBlanks(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" A👨👩👧", str);
}
// We should have no graphemes
try testing.expectEqual(@as(usize, 1), page.graphemeCount());
}
test "Terminal: insert mode with space" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
t.setCursorPos(1, 2);
t.modes.set(.insert, true);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hXello", str);
}
}
test "Terminal: insert mode doesn't wrap pushed characters" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
t.setCursorPos(1, 2);
t.modes.set(.insert, true);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hXell", str);
}
}
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 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
t.modes.set(.insert, true);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello\nX", str);
}
}
test "Terminal: insert mode with wide characters" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("hello") |c| try t.print(c);
t.setCursorPos(1, 2);
t.modes.set(.insert, true);
try t.print('😀'); // 0x1F600
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("h😀el", str);
}
}
test "Terminal: insert mode with wide characters at end" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("well") |c| try t.print(c);
t.modes.set(.insert, true);
try t.print('😀'); // 0x1F600
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("well\n😀", str);
}
}
test "Terminal: insert mode pushing off wide character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
for ("123") |c| try t.print(c);
try t.print('😀'); // 0x1F600
t.modes.set(.insert, true);
t.setCursorPos(1, 1);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X123", str);
}
}
test "Terminal: deleteChars" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.deleteChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ADE", str);
}
}
test "Terminal: deleteChars zero count" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.deleteChars(0);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDE", str);
}
}
test "Terminal: deleteChars more than half" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.deleteChars(3);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AE", str);
}
}
test "Terminal: deleteChars more than line width" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.deleteChars(10);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
test "Terminal: deleteChars should shift left" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
t.deleteChars(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ACDE", str);
}
}
test "Terminal: deleteChars resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.deleteChars(1);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDX", str);
}
}
test "Terminal: deleteChars simple operation" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.setCursorPos(1, 3);
t.deleteChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB23", str);
}
}
test "Terminal: deleteChars preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 10 });
defer t.deinit(alloc);
for ("ABC123") |c| try t.print(c);
t.setCursorPos(1, 3);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.deleteChars(2);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB23", str);
}
for (t.cols - 2..t.cols) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
test "Terminal: deleteChars outside scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
try testing.expect(t.screen.cursor.pending_wrap);
t.deleteChars(2);
try testing.expect(t.screen.cursor.pending_wrap);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC123", str);
}
}
test "Terminal: deleteChars inside scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("ABC123");
t.scrolling_region.left = 2;
t.scrolling_region.right = 4;
t.setCursorPos(1, 4);
t.deleteChars(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC2 3", str);
}
}
test "Terminal: deleteChars split wide character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 6, .rows = 10 });
defer t.deinit(alloc);
try t.printString("A橋123");
t.setCursorPos(1, 3);
t.deleteChars(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A 123", str);
}
}
test "Terminal: deleteChars split wide character tail" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, t.cols - 1);
try t.print(0x6A4B); // 橋
t.carriageReturn();
t.deleteChars(t.cols - 1);
try t.print('0');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("0", str);
}
}
test "Terminal: saveCursor" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
t.screen.charset.gr = .G3;
t.modes.set(.origin, true);
t.saveCursor();
t.screen.charset.gr = .G0;
try t.setAttribute(.{ .unset = {} });
t.modes.set(.origin, false);
try t.restoreCursor();
try testing.expect(t.screen.cursor.style.flags.bold);
try testing.expect(t.screen.charset.gr == .G3);
try testing.expect(t.modes.get(.origin));
}
test "Terminal: saveCursor with screen change" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try t.setAttribute(.{ .bold = {} });
t.setCursorPos(t.screen.cursor.y + 1, 3);
try testing.expect(t.screen.cursor.x == 2);
t.screen.charset.gr = .G3;
t.modes.set(.origin, true);
t.alternateScreen(.{
.cursor_save = true,
.clear_on_enter = true,
});
// make sure our cursor and charset have come with us
try testing.expect(t.screen.cursor.style.flags.bold);
try testing.expect(t.screen.cursor.x == 2);
try testing.expect(t.screen.charset.gr == .G3);
try testing.expect(t.modes.get(.origin));
t.screen.charset.gr = .G0;
try t.setAttribute(.{ .reset_bold = {} });
t.modes.set(.origin, false);
t.primaryScreen(.{
.cursor_save = true,
.clear_on_enter = true,
});
try testing.expect(t.screen.cursor.style.flags.bold);
try testing.expect(t.screen.charset.gr == .G3);
try testing.expect(t.modes.get(.origin));
}
test "Terminal: saveCursor position" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
try t.print('A');
t.saveCursor();
t.setCursorPos(1, 1);
try t.print('B');
try t.restoreCursor();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("B AX", str);
}
}
test "Terminal: saveCursor pending wrap state" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 5);
try t.print('A');
t.saveCursor();
t.setCursorPos(1, 1);
try t.print('B');
try t.restoreCursor();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("B A\nX", str);
}
}
test "Terminal: saveCursor origin mode" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.modes.set(.origin, true);
t.saveCursor();
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(3, 5);
t.setTopAndBottomMargin(2, 4);
try t.restoreCursor();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X", str);
}
}
test "Terminal: saveCursor resize" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setCursorPos(1, 10);
t.saveCursor();
try t.resize(alloc, 5, 5);
try t.restoreCursor();
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: saveCursor protected pen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
try testing.expect(t.screen.cursor.protected);
t.setCursorPos(1, 10);
t.saveCursor();
t.setProtectedMode(.off);
try testing.expect(!t.screen.cursor.protected);
try t.restoreCursor();
try testing.expect(t.screen.cursor.protected);
}
test "Terminal: setProtectedMode" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 3 });
defer t.deinit(alloc);
try testing.expect(!t.screen.cursor.protected);
t.setProtectedMode(.off);
try testing.expect(!t.screen.cursor.protected);
t.setProtectedMode(.iso);
try testing.expect(t.screen.cursor.protected);
t.setProtectedMode(.dec);
try testing.expect(t.screen.cursor.protected);
t.setProtectedMode(.off);
try testing.expect(!t.screen.cursor.protected);
}
test "Terminal: eraseLine simple erase right" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 3);
t.eraseLine(.right, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB", str);
}
}
test "Terminal: eraseLine resets pending wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.eraseLine(.right, false);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('B');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABCDB", str);
}
}
test "Terminal: eraseLine resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE123") |c| try t.print(c);
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
try testing.expect(list_cell.row.wrap);
}
t.setCursorPos(1, 1);
t.eraseLine(.right, false);
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
try testing.expect(!list_cell.row.wrap);
}
try t.print('X');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("X\n123", str);
}
}
test "Terminal: eraseLine right preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.eraseLine(.right, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
for (1..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: eraseLine right wide character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("AB") |c| try t.print(c);
try t.print('橋');
for ("DE") |c| try t.print(c);
t.setCursorPos(1, 4);
t.eraseLine(.right, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB", str);
}
}
test "Terminal: eraseLine right protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseLine(.right, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseLine right protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(1, 2);
t.eraseLine(.right, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
test "Terminal: eraseLine right protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 2);
t.eraseLine(.right, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
test "Terminal: eraseLine right protected requested" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("12345678") |c| try t.print(c);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorPos(t.screen.cursor.y + 1, 4);
t.eraseLine(.right, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("123 X", str);
}
}
test "Terminal: eraseLine simple erase left" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 3);
t.eraseLine(.left, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" DE", str);
}
}
test "Terminal: eraseLine left resets wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.eraseLine(.left, false);
try testing.expect(!t.screen.cursor.pending_wrap);
try t.print('B');
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" B", str);
}
}
test "Terminal: eraseLine left preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.eraseLine(.left, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" CDE", str);
for (0..2) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: eraseLine left wide character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("AB") |c| try t.print(c);
try t.print('橋');
for ("DE") |c| try t.print(c);
t.setCursorPos(1, 3);
t.eraseLine(.left, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" DE", str);
}
}
test "Terminal: eraseLine left protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseLine(.left, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseLine left protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(1, 2);
t.eraseLine(.left, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: eraseLine left protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 2);
t.eraseLine(.left, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" C", str);
}
}
test "Terminal: eraseLine left protected requested" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("123456789") |c| try t.print(c);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorPos(t.screen.cursor.y + 1, 8);
t.eraseLine(.left, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X 9", str);
}
}
test "Terminal: eraseLine complete preserves background sgr" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
t.setCursorPos(1, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.eraseLine(.complete, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
for (0..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: eraseLine complete protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 1);
t.eraseLine(.complete, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: eraseLine complete protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(1, 2);
t.eraseLine(.complete, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: eraseLine complete protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.setCursorPos(1, 2);
t.eraseLine(.complete, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: eraseLine complete protected requested" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
for ("123456789") |c| try t.print(c);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorPos(t.screen.cursor.y + 1, 8);
t.eraseLine(.complete, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" X", str);
}
}
test "Terminal: tabClear single" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 30, .rows = 5 });
defer t.deinit(alloc);
try t.horizontalTab();
t.tabClear(.current);
t.setCursorPos(1, 1);
try t.horizontalTab();
try testing.expectEqual(@as(usize, 16), t.screen.cursor.x);
}
test "Terminal: tabClear all" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 30, .rows = 5 });
defer t.deinit(alloc);
t.tabClear(.all);
t.setCursorPos(1, 1);
try t.horizontalTab();
try testing.expectEqual(@as(usize, 29), t.screen.cursor.x);
}
test "Terminal: printRepeat simple" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("A");
try t.printRepeat(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AA", str);
}
}
test "Terminal: printRepeat wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString(" A");
try t.printRepeat(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings(" A\nA", str);
}
}
test "Terminal: printRepeat no previous character" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printRepeat(1);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: printAttributes" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
var storage: [64]u8 = undefined;
{
try t.setAttribute(.{ .direct_color_fg = .{ .r = 1, .g = 2, .b = 3 } });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;38:2::1:2:3", buf);
}
{
try t.setAttribute(.bold);
try t.setAttribute(.{ .direct_color_bg = .{ .r = 1, .g = 2, .b = 3 } });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;1;48:2::1:2:3", buf);
}
{
try t.setAttribute(.bold);
try t.setAttribute(.faint);
try t.setAttribute(.italic);
try t.setAttribute(.{ .underline = .single });
try t.setAttribute(.blink);
try t.setAttribute(.inverse);
try t.setAttribute(.invisible);
try t.setAttribute(.strikethrough);
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 t.setAttribute(.{ .underline = .single });
defer t.setAttribute(.unset) catch unreachable;
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0;4", buf);
}
{
const buf = try t.printAttributes(&storage);
try testing.expectEqualStrings("0", buf);
}
}
test "Terminal: eraseDisplay simple erase below" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.below, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nD", str);
}
}
test "Terminal: eraseDisplay erase below preserves SGR bg" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.eraseDisplay(.below, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nD", str);
for (1..5) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: eraseDisplay below split multi-cell" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("AB橋C");
t.carriageReturn();
try t.linefeed();
try t.printString("DE橋F");
t.carriageReturn();
try t.linefeed();
try t.printString("GH橋I");
t.setCursorPos(2, 4);
t.eraseDisplay(.below, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("AB橋C\nDE", str);
}
}
test "Terminal: eraseDisplay below protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.below, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: eraseDisplay below protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(2, 2);
t.eraseDisplay(.below, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nD", str);
}
}
test "Terminal: eraseDisplay below protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.below, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nD", str);
}
}
test "Terminal: eraseDisplay below protected attributes respected with force" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.below, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: eraseDisplay simple erase above" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.above, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n F\nGHI", str);
}
}
test "Terminal: eraseDisplay erase above preserves SGR bg" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.eraseDisplay(.above, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n F\nGHI", str);
for (0..2) |x| {
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
}
test "Terminal: eraseDisplay above split multi-cell" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.printString("AB橋C");
t.carriageReturn();
try t.linefeed();
try t.printString("DE橋F");
t.carriageReturn();
try t.linefeed();
try t.printString("GH橋I");
t.setCursorPos(2, 3);
t.eraseDisplay(.above, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n F\nGH橋I", str);
}
}
test "Terminal: eraseDisplay above protected attributes respected with iso" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.above, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: eraseDisplay above protected attributes ignored with dec most recent" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.iso);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setProtectedMode(.dec);
t.setProtectedMode(.off);
t.setCursorPos(2, 2);
t.eraseDisplay(.above, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n F\nGHI", str);
}
}
test "Terminal: eraseDisplay above protected attributes ignored with dec set" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.above, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n F\nGHI", str);
}
}
test "Terminal: eraseDisplay above protected attributes respected with force" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.setProtectedMode(.dec);
for ("ABC") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("DEF") |c| try t.print(c);
t.carriageReturn();
try t.linefeed();
for ("GHI") |c| try t.print(c);
t.setCursorPos(2, 2);
t.eraseDisplay(.above, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC\nDEF\nGHI", str);
}
}
test "Terminal: eraseDisplay protected complete" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
t.carriageReturn();
try t.linefeed();
for ("123456789") |c| try t.print(c);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorPos(t.screen.cursor.y + 1, 4);
t.eraseDisplay(.complete, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n X", str);
}
}
test "Terminal: eraseDisplay protected below" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
t.carriageReturn();
try t.linefeed();
for ("123456789") |c| try t.print(c);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorPos(t.screen.cursor.y + 1, 4);
t.eraseDisplay(.below, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n123 X", str);
}
}
test "Terminal: eraseDisplay scroll complete" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 5 });
defer t.deinit(alloc);
try t.print('A');
t.carriageReturn();
try t.linefeed();
t.eraseDisplay(.scroll_complete, false);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
}
test "Terminal: eraseDisplay protected above" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 10, .rows = 3 });
defer t.deinit(alloc);
try t.print('A');
t.carriageReturn();
try t.linefeed();
for ("123456789") |c| try t.print(c);
t.setCursorPos(t.screen.cursor.y + 1, 6);
t.setProtectedMode(.dec);
try t.print('X');
t.setCursorPos(t.screen.cursor.y + 1, 8);
t.eraseDisplay(.above, true);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n X 9", str);
}
}
test "Terminal: cursorIsAtPrompt" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 2 });
defer t.deinit(alloc);
try testing.expect(!t.cursorIsAtPrompt());
t.markSemanticPrompt(.prompt);
try testing.expect(t.cursorIsAtPrompt());
// Input is also a prompt
t.markSemanticPrompt(.input);
try testing.expect(t.cursorIsAtPrompt());
// Newline -- we expect we're still at a prompt if we received
// prompt stuff before.
try t.linefeed();
try testing.expect(t.cursorIsAtPrompt());
// But once we say we're starting output, we're not a prompt
t.markSemanticPrompt(.command);
try testing.expect(!t.cursorIsAtPrompt());
try t.linefeed();
try testing.expect(!t.cursorIsAtPrompt());
// Until we know we're at a prompt again
try t.linefeed();
t.markSemanticPrompt(.prompt);
try testing.expect(t.cursorIsAtPrompt());
}
test "Terminal: cursorIsAtPrompt alternate screen" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 3, .rows = 2 });
defer t.deinit(alloc);
try testing.expect(!t.cursorIsAtPrompt());
t.markSemanticPrompt(.prompt);
try testing.expect(t.cursorIsAtPrompt());
// Secondary screen is never a prompt
t.alternateScreen(.{});
try testing.expect(!t.cursorIsAtPrompt());
t.markSemanticPrompt(.prompt);
try testing.expect(!t.cursorIsAtPrompt());
}
test "Terminal: fullReset with a non-empty pen" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);
try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
try t.setAttribute(.{ .direct_color_bg = .{ .r = 0xFF, .g = 0, .b = 0x7F } });
t.fullReset();
{
const list_cell = t.screen.pages.getCell(.{ .active = .{
.x = t.screen.cursor.x,
.y = t.screen.cursor.y,
} }).?;
const cell = list_cell.cell;
try testing.expect(cell.style_id == 0);
}
}
test "Terminal: fullReset origin mode" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
t.setCursorPos(3, 5);
t.modes.set(.origin, true);
t.fullReset();
// Origin mode should be reset and the cursor should be moved
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
try testing.expect(!t.modes.get(.origin));
}
test "Terminal: fullReset status display" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
defer t.deinit(testing.allocator);
t.status_display = .status_line;
t.fullReset();
try testing.expect(t.status_display == .main);
}
// https://github.com/mitchellh/ghostty/issues/272
// This is also tested in depth in screen resize tests but I want to keep
// 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 });
defer t.deinit(alloc);
try t.print('x');
try t.print('😀'); // 0x1F600
try t.resize(alloc, 2, 3);
t.setCursorPos(1, 2);
try t.print('😀'); // 0x1F600
}
// https://github.com/mitchellh/ghostty/issues/723
// This was found via fuzzing so its highly specific.
test "Terminal: resize with left and right margin set" {
const alloc = testing.allocator;
const cols = 70;
const rows = 23;
var t = try init(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);
t.setLeftAndRightMargin(2, 0);
try t.printRepeat(1850);
_ = t.modes.restore(.enable_mode_3);
try t.resize(alloc, cols, rows);
}
// https://github.com/mitchellh/ghostty/issues/1343
test "Terminal: resize with wraparound off" {
const alloc = testing.allocator;
const cols = 4;
const rows = 2;
var t = try init(alloc, .{ .cols = cols, .rows = rows });
defer t.deinit(alloc);
t.modes.set(.wraparound, false);
try t.print('0');
try t.print('1');
try t.print('2');
try t.print('3');
const new_cols = 2;
try t.resize(alloc, new_cols, rows);
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("01", str);
}
test "Terminal: resize with wraparound on" {
const alloc = testing.allocator;
const cols = 4;
const rows = 2;
var t = try init(alloc, .{ .cols = cols, .rows = rows });
defer t.deinit(alloc);
t.modes.set(.wraparound, true);
try t.print('0');
try t.print('1');
try t.print('2');
try t.print('3');
const new_cols = 2;
try t.resize(alloc, new_cols, rows);
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("01\n23", str);
}
test "Terminal: resize with high unique style per cell" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 30, .rows = 30 });
defer t.deinit(alloc);
for (0..t.rows) |y| {
for (0..t.cols) |x| {
t.setCursorPos(y, x);
try t.setAttribute(.{ .direct_color_bg = .{
.r = @intCast(x),
.g = @intCast(y),
.b = 0,
} });
try t.print('x');
}
}
try t.resize(alloc, 60, 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 });
defer t.deinit(alloc);
const cell_count: u16 = @intCast(t.rows * t.cols);
for (0..cell_count) |i| {
const r: u8 = @intCast(i >> 8);
const g: u8 = @intCast(i & 0xFF);
try t.setAttribute(.{ .direct_color_bg = .{
.r = r,
.g = g,
.b = 0,
} });
try t.print('x');
}
try t.resize(alloc, 60, 30);
}
test "Terminal: DECCOLM without DEC mode 40" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.@"132_column", true);
try t.deccolm(alloc, .@"132_cols");
try testing.expectEqual(@as(usize, 5), t.cols);
try testing.expectEqual(@as(usize, 5), t.rows);
try testing.expect(!t.modes.get(.@"132_column"));
}
test "Terminal: DECCOLM unset" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.enable_mode_3, true);
try t.deccolm(alloc, .@"80_cols");
try testing.expectEqual(@as(usize, 80), t.cols);
try testing.expectEqual(@as(usize, 5), t.rows);
}
test "Terminal: DECCOLM resets pending wrap" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
for ("ABCDE") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap);
t.modes.set(.enable_mode_3, true);
try t.deccolm(alloc, .@"80_cols");
try testing.expectEqual(@as(usize, 80), t.cols);
try testing.expectEqual(@as(usize, 5), t.rows);
try testing.expect(!t.screen.cursor.pending_wrap);
}
test "Terminal: DECCOLM preserves SGR bg" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
try t.setAttribute(.{ .direct_color_bg = .{
.r = 0xFF,
.g = 0,
.b = 0,
} });
t.modes.set(.enable_mode_3, true);
try t.deccolm(alloc, .@"80_cols");
{
const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?;
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
try testing.expectEqual(Cell.RGB{
.r = 0xFF,
.g = 0,
.b = 0,
}, list_cell.cell.content.color_rgb);
}
}
test "Terminal: DECCOLM resets scroll region" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
t.modes.set(.enable_left_and_right_margin, true);
t.setTopAndBottomMargin(2, 3);
t.setLeftAndRightMargin(3, 5);
t.modes.set(.enable_mode_3, true);
try t.deccolm(alloc, .@"80_cols");
try testing.expect(t.modes.get(.enable_left_and_right_margin));
try testing.expectEqual(@as(usize, 0), t.scrolling_region.top);
try testing.expectEqual(@as(usize, 4), t.scrolling_region.bottom);
try testing.expectEqual(@as(usize, 0), t.scrolling_region.left);
try testing.expectEqual(@as(usize, 79), t.scrolling_region.right);
}