mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
4293 lines
132 KiB
Zig
4293 lines
132 KiB
Zig
//! 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();
|
|
|
|
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 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,
|
|
};
|
|
|
|
/// 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,
|
|
};
|
|
|
|
/// 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,
|
|
};
|
|
|
|
/// Initialize a new terminal.
|
|
pub fn init(alloc: Allocator, cols: size.CellCountInt, rows: size.CellCountInt) !Terminal {
|
|
return Terminal{
|
|
.cols = cols,
|
|
.rows = rows,
|
|
.active_screen = .primary,
|
|
// TODO: configurable scrollback
|
|
.screen = try Screen.init(alloc, cols, rows, 10000),
|
|
// No scrollback for the alternate screen
|
|
.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),
|
|
}
|
|
}
|
|
}
|
|
|
|
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_offset.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_offset.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_offset.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)
|
|
{
|
|
@panic("TODO: insert mode");
|
|
//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
|
|
|
|
// TODO: charsets
|
|
const c: u21 = 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_offset.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,
|
|
};
|
|
|
|
// Handle the style ref count handling
|
|
style_ref: {
|
|
if (prev_style_id != style.default_id) {
|
|
// 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_offset.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;
|
|
}
|
|
}
|
|
|
|
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.
|
|
// TODO(mitchellh): before merge
|
|
//const old_prompt = row.getSemanticPrompt();
|
|
|
|
// Move to the next line
|
|
try self.index();
|
|
self.screen.cursorHorizontalAbsolute(self.scrolling_region.left);
|
|
|
|
// TODO(mitchellh): before merge
|
|
// New line must inherit semantic prompt of the old line
|
|
// const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
|
// new_row.setSemanticPrompt(old_prompt);
|
|
self.screen.cursor.page_row.wrap_continuation = true;
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// 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_offset.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
|
|
const blank_cell = self.blankCell();
|
|
for (0..adjusted_count) |i| {
|
|
const row: *Row = @ptrCast(top + i);
|
|
|
|
// Clear the src row.
|
|
var page = self.screen.cursor.page_offset.page.data;
|
|
const cells = page.getCells(row);
|
|
|
|
// If this row has graphemes, then we need go through a slow path
|
|
// and delete the cell graphemes.
|
|
if (row.grapheme) {
|
|
for (cells) |*cell| {
|
|
if (cell.hasGrapheme()) page.clearGrapheme(row, cell);
|
|
}
|
|
assert(!row.grapheme);
|
|
}
|
|
|
|
@memset(
|
|
cells[self.scrolling_region.left .. self.scrolling_region.right + 1],
|
|
blank_cell,
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
|
|
if (self.scrolling_region.left > 0 or
|
|
self.scrolling_region.right < self.cols - 1)
|
|
{
|
|
@panic("TODO: left/right margins");
|
|
}
|
|
|
|
// 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) {
|
|
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);
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
const bottom: [*]Row = top + (rem - 1);
|
|
const blank_cell = self.blankCell();
|
|
while (@intFromPtr(y) <= @intFromPtr(bottom)) : (y += 1) {
|
|
const row: *Row = @ptrCast(y);
|
|
|
|
// Clear the src row.
|
|
var page = self.screen.cursor.page_offset.page.data;
|
|
const cells = page.getCells(row);
|
|
|
|
// If this row has graphemes, then we need go through a slow path
|
|
// and delete the cell graphemes.
|
|
if (row.grapheme) {
|
|
for (cells) |*cell| {
|
|
if (cell.hasGrapheme()) page.clearGrapheme(row, cell);
|
|
}
|
|
assert(!row.grapheme);
|
|
}
|
|
|
|
@memset(cells, blank_cell);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
pub fn eraseChars(self: *Terminal, count_req: usize) void {
|
|
const count = @max(count_req, 1);
|
|
|
|
// 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);
|
|
@memset(cells[0..end], self.blankCell());
|
|
|
|
// 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;
|
|
|
|
// TODO: protected mode, see below for old logic
|
|
//
|
|
// const pen: Screen.Cell = .{
|
|
// .bg = self.screen.cursor.pen.bg,
|
|
// };
|
|
//
|
|
// // 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) {
|
|
// row.fillSlice(pen, self.screen.cursor.x, end);
|
|
// }
|
|
//
|
|
// // We had a protection mode at some point. We must go through each
|
|
// // cell and check its protection attribute.
|
|
// for (self.screen.cursor.x..end) |x| {
|
|
// const cell = row.getCellPtr(x);
|
|
// if (cell.attrs.protected) continue;
|
|
// cell.* = pen;
|
|
// }
|
|
}
|
|
|
|
/// Set a style attribute.
|
|
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
|
|
try self.screen.setAttribute(attr);
|
|
}
|
|
|
|
/// 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 = .{} });
|
|
}
|
|
|
|
/// Returns the blank cell to use when doing terminal operations that
|
|
/// require preserving the bg color.
|
|
fn blankCell(self: *const Terminal) Cell {
|
|
if (self.screen.cursor.style_id == style.default_id) return .{};
|
|
return self.screen.cursor.style.bgCell() orelse .{};
|
|
}
|
|
|
|
test "Terminal: input with no control characters" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 40, 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, 5, 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, 1, 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: zero-width character at start" {
|
|
var t = try init(testing.allocator, 80, 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, 5, 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, 80, 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 in single-width terminal" {
|
|
var t = try init(testing.allocator, 1, 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, 80, 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, 5, 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, 80, 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, 5, 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, 80, 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, 80, 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, 5, 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, 5, 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, 5, 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, 80, 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, 80, 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, 5, 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, 5, 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: soft wrap" {
|
|
var t = try init(testing.allocator, 3, 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: disabled wraparound with wide char and one space" {
|
|
var t = try init(testing.allocator, 5, 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, 5, 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, 5, 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, 10, 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, 10, 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, 10, 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, 80, 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, 5, 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, 10, 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, 5, 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, 5, 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, 5, 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, 5, 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, 80, 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, 20, 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, 20, 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, 20, 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, 20, 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, 20, 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, 20, 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: cursorPos resets wrap" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 80, 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
|
|
// TODO
|
|
// 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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 outside of scroll region" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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, 2, 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, 2, 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, 2, 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, 2, 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, 5, 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, 5, 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: scrollUp simple" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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 preserves pending wrap" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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: scrollDown simple" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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 preserves pending wrap" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 10, 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: reverseIndex" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 2, 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, 2, 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, 2, 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, 5, 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, 5, 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, 5, 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, 5, 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: index" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 2, 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, 2, 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, 2, 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, 2, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 10, 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 bottom of scroll region" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 10, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 5, 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, 80, 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, 80, 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, 80, 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, 80, 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, 5, 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: default style is empty" {
|
|
const alloc = testing.allocator;
|
|
var t = try init(alloc, 5, 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, 5, 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, 5, 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_offset.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, 5, 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_offset.page.data;
|
|
try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory));
|
|
}
|