ghostty/src/terminal/Terminal.zig
Mitchell Hashimoto fbd79ee585 comment to be clear
2022-07-17 10:06:17 -07:00

1311 lines
38 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 testing = std.testing;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ansi = @import("ansi.zig");
const csi = @import("csi.zig");
const sgr = @import("sgr.zig");
const Tabstops = @import("Tabstops.zig");
const trace = @import("../tracy/tracy.zig").trace;
const color = @import("color.zig");
const Screen = @import("Screen.zig");
const log = std.log.scoped(.terminal);
/// Default tabstop interval
const TABSTOP_INTERVAL = 8;
/// Screen is the current screen state.
screen: Screen,
/// Cursor position.
cursor: Cursor,
/// Saved cursor saved with DECSC (ESC 7).
saved_cursor: Cursor,
/// Where the tabstops are.
tabstops: Tabstops,
/// The size of the terminal.
rows: usize,
cols: usize,
/// The current scrolling region.
scrolling_region: ScrollingRegion,
/// Modes - This isn't exhaustive, since some modes (i.e. cursor origin)
/// are applied to the cursor and others aren't boolean yes/no.
modes: packed struct {
reverse_colors: u1 = 0, // 5,
origin: u1 = 0, // 6
autowrap: u1 = 1, // 7
@"132_column": u1 = 0, // 3,
enable_mode_3: u1 = 0, // 40
} = .{},
/// Scrolling region is the area of the screen designated where scrolling
/// occurs. Wen scrolling the screen, only this viewport is scrolled.
const ScrollingRegion = struct {
// Top and bottom of the scroll region (0-indexed)
// Precondition: top < bottom
top: usize,
bottom: usize,
};
/// Cursor represents the cursor state.
const Cursor = struct {
// x, y where the cursor currently exists (0-indexed).
x: usize = 0,
y: usize = 0,
// pen is the current cell styling to apply to new cells.
pen: Screen.Cell = .{ .char = 0 },
// The last column flag (LCF) used to do soft wrapping.
pending_wrap: bool = false,
};
/// Initialize a new terminal.
pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal {
return Terminal{
.cols = cols,
.rows = rows,
// TODO: configurable scrollback
.screen = try Screen.init(alloc, rows, cols, 1000),
.cursor = .{},
.saved_cursor = .{},
.tabstops = try Tabstops.init(alloc, cols, TABSTOP_INTERVAL),
.scrolling_region = .{
.top = 0,
.bottom = rows - 1,
},
};
}
pub fn deinit(self: *Terminal, alloc: Allocator) void {
self.tabstops.deinit(alloc);
self.screen.deinit(alloc);
self.* = undefined;
}
/// Resize the underlying terminal.
pub fn resize(self: *Terminal, alloc: Allocator, cols: usize, rows: usize) !void {
// TODO: test, wrapping, etc.
// Resize our tabstops
// TODO: use resize, but it doesn't set new tabstops
if (self.cols != cols) {
self.tabstops.deinit(alloc);
self.tabstops = try Tabstops.init(alloc, cols, 8);
}
// If we're making the screen smaller, dealloc the unused items.
// TODO: reflow
try self.screen.resize(alloc, rows, cols);
// Set our size
self.cols = cols;
self.rows = rows;
// Reset the scrolling region
self.scrolling_region = .{
.top = 0,
.bottom = rows - 1,
};
// Move our cursor
self.cursor.x = @minimum(self.cursor.x, self.cols - 1);
self.cursor.y = @minimum(self.cursor.y, self.rows - 1);
}
/// 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.testString(alloc);
}
/// Save cursor position and further state.
///
/// The primary and alternate screen have distinct save state. One saved state
/// is kept per screen (main / alternative). If for the current screen state
/// was already saved it is overwritten.
pub fn saveCursor(self: *Terminal) void {
self.saved_cursor = self.cursor;
}
/// Restore cursor position and other state.
///
/// The primary and alternate screen have distinct save state.
/// If no save was done before values are reset to their initial values.
pub fn restoreCursor(self: *Terminal) void {
self.cursor = self.saved_cursor;
}
/// TODO: test
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
switch (attr) {
.unset => {
self.cursor.pen.fg = null;
self.cursor.pen.bg = null;
self.cursor.pen.attrs = .{};
},
.bold => {
self.cursor.pen.attrs.bold = 1;
},
.underline => {
self.cursor.pen.attrs.underline = 1;
},
.inverse => {
self.cursor.pen.attrs.inverse = 1;
},
.direct_color_fg => |rgb| {
self.cursor.pen.fg = .{
.r = rgb.r,
.g = rgb.g,
.b = rgb.b,
};
},
.direct_color_bg => |rgb| {
self.cursor.pen.bg = .{
.r = rgb.r,
.g = rgb.g,
.b = rgb.b,
};
},
.@"8_fg" => |n| self.cursor.pen.fg = color.default[@enumToInt(n)],
.@"8_bg" => |n| self.cursor.pen.bg = color.default[@enumToInt(n)],
.@"8_bright_fg" => |n| self.cursor.pen.fg = color.default[@enumToInt(n)],
.@"8_bright_bg" => |n| self.cursor.pen.bg = color.default[@enumToInt(n)],
.@"256_fg" => |idx| self.cursor.pen.fg = color.default[idx],
.@"256_bg" => |idx| self.cursor.pen.bg = color.default[idx],
else => return error.InvalidAttribute,
}
}
pub fn print(self: *Terminal, c: u21) !void {
const tracy = trace(@src());
defer tracy.end();
// If we're not at the bottom, then we need to move there
if (!self.screen.displayIsBottom()) self.screen.scroll(.{ .bottom = {} });
// If we're soft-wrapping, then handle that first.
if (self.cursor.pending_wrap and self.modes.autowrap == 1) {
// Mark that the cell is wrapped, which guarantees that there is
// at least one cell after it in the next row.
const cell = self.screen.getCell(self.cursor.y, self.cursor.x);
cell.attrs.wrap = 1;
// Move to the next line
self.index();
self.cursor.x = 0;
}
// Build our cell
const cell = self.screen.getCell(self.cursor.y, self.cursor.x);
cell.* = self.cursor.pen;
cell.char = @intCast(u32, c);
// Move the cursor
self.cursor.x += 1;
// If we're at the column limit, then we need to wrap the next time.
// This is unlikely so we do the increment above and decrement here
// if we need to rather than check once.
if (self.cursor.x == self.cols) {
self.cursor.x -= 1;
self.cursor.pending_wrap = true;
}
}
/// Resets all margins and fills the whole screen with the character 'E'
///
/// Sets the cursor to the top left corner.
pub fn decaln(self: *Terminal) void {
// Reset margins, also sets cursor to top-left
self.setScrollingRegion(0, 0);
// Fill with Es, does not move cursor. We reset fg/bg so we can just
// optimize here by doing row copies.
const filled = self.screen.getRow(0);
var col: usize = 0;
while (col < self.cols) : (col += 1) {
filled[col] = .{ .char = 'E' };
}
var row: usize = 1;
while (row < self.rows) : (row += 1) {
std.mem.copy(Screen.Cell, self.screen.getRow(row), filled);
}
}
/// 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.cursor.pending_wrap = false;
// Outside of the scroll region we move the cursor one line down.
if (self.cursor.y < self.scrolling_region.top or
self.cursor.y > self.scrolling_region.bottom)
{
self.cursor.y = @minimum(self.cursor.y + 1, self.rows - 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.cursor.y == self.scrolling_region.bottom) {
// 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)
{
self.screen.scroll(.{ .delta = 1 });
} else {
// TODO: test
self.scrollUp(1);
}
}
// Increase cursor by 1, maximum to bottom of scroll region
self.cursor.y = @minimum(self.cursor.y + 1, self.scrolling_region.bottom);
}
/// 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 {
// TODO: scrolling region
if (self.cursor.y == 0) {
self.scrollDown(1);
} else {
self.cursor.y -|= 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: usize, col: 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: usize = 0,
y_offset: usize = 0,
x_max: usize,
y_max: usize,
} = if (self.modes.origin == 1) .{
.x_offset = 0, // TODO: left/right margins
.x_max = self.cols, // TODO: left/right margins
.y_offset = self.scrolling_region.top + 1,
.y_max = self.scrolling_region.bottom + 1, // We need this 1-indexed
} else .{
.x_max = self.cols,
.y_max = self.rows,
};
self.cursor.x = @minimum(params.x_max, col) -| 1;
self.cursor.y = @minimum(params.y_max, row + params.y_offset) -| 1;
log.info("set cursor position: col={} row={}", .{ self.cursor.x, self.cursor.y });
// Unset pending wrap state
self.cursor.pending_wrap = false;
}
/// Erase the display.
/// TODO: test
pub fn eraseDisplay(
self: *Terminal,
mode: csi.EraseDisplay,
) void {
switch (mode) {
.complete => {
const all = self.screen.getVisible();
std.mem.set(Screen.Cell, all, self.cursor.pen);
},
.below => {
// All lines to the right (including the cursor)
var x: usize = self.cursor.x;
while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, self.cursor.y);
cell.* = self.cursor.pen;
cell.char = 0;
}
// All lines below
var y: usize = self.cursor.y + 1;
while (y < self.rows) : (y += 1) {
x = 0;
while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, y);
cell.* = self.cursor.pen;
cell.char = 0;
}
}
},
.above => {
// Erase to the left (including the cursor)
var x: usize = 0;
while (x <= self.cursor.x) : (x += 1) {
const cell = self.getOrPutCell(x, self.cursor.y);
cell.* = self.cursor.pen;
cell.char = 0;
}
// All lines above
var y: usize = 0;
while (y < self.cursor.y) : (y += 1) {
x = 0;
while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, y);
cell.* = self.cursor.pen;
cell.char = 0;
}
}
},
else => {
log.err("unimplemented display mode: {}", .{mode});
@panic("unimplemented");
},
}
}
/// Erase the line.
/// TODO: test
pub fn eraseLine(
self: *Terminal,
mode: csi.EraseLine,
) !void {
switch (mode) {
.right => {
const row = self.screen.getRow(self.cursor.y);
std.mem.set(Screen.Cell, row[self.cursor.x..], self.cursor.pen);
},
.left => {
const row = self.screen.getRow(self.cursor.y);
std.mem.set(Screen.Cell, row[0..self.cursor.x], self.cursor.pen);
},
.complete => {
const row = self.screen.getRow(self.cursor.y);
std.mem.set(Screen.Cell, row, self.cursor.pen);
},
else => {
log.err("unimplemented erase line mode: {}", .{mode});
@panic("unimplemented");
},
}
}
/// Removes amount characters from the current cursor position to the right.
/// The remaining characters are shifted to the left and space from the right
/// margin is filled with spaces.
///
/// If amount is greater than the remaining number of characters in the
/// scrolling region, it is adjusted down.
///
/// Does not change the cursor position.
///
/// TODO: test
pub fn deleteChars(self: *Terminal, count: usize) !void {
const line = self.screen.getRow(self.cursor.y);
// Our last index is at most the end of the number of chars we have
// in the current line.
const end = self.cols - count;
// Shift
var i: usize = self.cursor.x;
while (i < end) : (i += 1) {
const j = i + count;
line[i] = line[j];
line[j].char = 0;
}
}
// TODO: test, docs
pub fn eraseChars(self: *Terminal, count: usize) void {
// Our last index is at most the end of the number of chars we have
// in the current line.
const end = @minimum(self.cols, self.cursor.x + count);
// Shift
var x: usize = self.cursor.x;
while (x < end) : (x += 1) {
const cell = self.getOrPutCell(x, self.cursor.y);
cell.* = self.cursor.pen;
cell.char = 0;
}
}
/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1.
/// TODO: test
pub fn cursorLeft(self: *Terminal, count: usize) void {
const tracy = trace(@src());
defer tracy.end();
// TODO: scroll region, wrap
self.cursor.x -|= if (count == 0) 1 else 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.
/// TODO: test
pub fn cursorRight(self: *Terminal, count: usize) void {
const tracy = trace(@src());
defer tracy.end();
self.cursor.x += count;
if (self.cursor.x >= self.cols) {
self.cursor.x = self.cols - 1;
}
}
/// 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.
// TODO: test
pub fn cursorDown(self: *Terminal, count: usize) void {
const tracy = trace(@src());
defer tracy.end();
self.cursor.y += count;
if (self.cursor.y >= self.rows) {
self.cursor.y = self.rows - 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.
// TODO: test
pub fn cursorUp(self: *Terminal, count: usize) void {
const tracy = trace(@src());
defer tracy.end();
self.cursor.y -|= count;
}
/// Backspace moves the cursor back a column (but not less than 0).
pub fn backspace(self: *Terminal) void {
const tracy = trace(@src());
defer tracy.end();
self.cursor.x -|= 1;
}
/// Horizontal tab moves the cursor to the next tabstop, clearing
/// the screen to the left the tabstop.
pub fn horizontalTab(self: *Terminal) !void {
const tracy = trace(@src());
defer tracy.end();
while (self.cursor.x < self.cols) {
// Move the cursor right
self.cursor.x += 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.cursor.x)) return;
}
}
/// Clear tab stops.
/// TODO: test
pub fn tabClear(self: *Terminal, cmd: csi.TabClear) void {
switch (cmd) {
.current => self.tabstops.unset(self.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.cursor.x);
}
/// Carriage return moves the cursor to the first column.
pub fn carriageReturn(self: *Terminal) void {
const tracy = trace(@src());
defer tracy.end();
// TODO: left/right margin mode
// TODO: origin mode
self.cursor.x = 0;
self.cursor.pending_wrap = false;
}
/// Linefeed moves the cursor to the next line.
pub fn linefeed(self: *Terminal) void {
self.index();
}
/// 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 {
// Move the cursor to the left margin
self.cursor.x = 0;
// Remaining rows from our cursor
const rem = self.scrolling_region.bottom - self.cursor.y + 1;
// If count is greater than the amount of rows, adjust down.
const adjusted_count = @minimum(count, rem);
// The the top `scroll_amount` lines need to move to the bottom
// scroll area. We may have nothing to scroll if we're clearing.
const scroll_amount = rem - adjusted_count;
var y: usize = self.scrolling_region.bottom;
const top = y - scroll_amount;
// Ensure we have the lines populated to the end
while (y > top) : (y -= 1) {
self.screen.copyRow(y, y - adjusted_count);
}
// Insert count blank lines
y = self.cursor.y;
while (y < self.cursor.y + adjusted_count) : (y += 1) {
var x: usize = 0;
while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, y);
cell.* = self.cursor.pen;
cell.char = 0;
}
}
}
/// 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: usize) void {
// TODO: scroll region bounds
// Move the cursor to the left margin
self.cursor.x = 0;
// Remaining number of lines in the scrolling region
const rem = self.scrolling_region.bottom - self.cursor.y + 1;
// If the count is more than our remaining lines, we adjust down.
const adjusted_count = @minimum(count, rem);
// Scroll up the count amount.
var y: usize = self.cursor.y;
while (y <= self.scrolling_region.bottom - adjusted_count) : (y += 1) {
self.screen.copyRow(y, y + adjusted_count);
}
while (y <= self.scrolling_region.bottom) : (y += 1) {
const row = self.screen.getRow(y);
std.mem.set(Screen.Cell, row, self.cursor.pen);
}
}
/// Scroll the text down by one row.
/// TODO: test
pub fn scrollDown(self: *Terminal, count: usize) void {
const tracy = trace(@src());
defer tracy.end();
// Preserve the cursor
const cursor = self.cursor;
defer self.cursor = cursor;
// Move to the top of the scroll region
self.cursor.y = 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.
// TODO: test
pub fn scrollUp(self: *Terminal, count: usize) void {
// Preserve the cursor
const cursor = self.cursor;
defer self.cursor = cursor;
// Move to the top of the scroll region
self.cursor.y = self.scrolling_region.top;
self.deleteLines(count);
}
/// Options for scrolling the viewport of the terminal grid.
pub const ScrollViewport = union(enum) {
/// Scroll to the top of the scrollback
top: void,
delta: isize,
};
/// Scroll the viewport of the terminal grid.
pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void {
self.screen.scroll(switch (behavior) {
.top => .{ .top = {} },
.delta => |delta| .{ .delta_no_grow = delta },
});
}
/// 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 setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void {
var t = if (top == 0) 1 else top;
var b = @minimum(bottom, self.rows);
if (t >= b) {
t = 1;
b = self.rows;
}
self.scrolling_region = .{
.top = t - 1,
.bottom = b - 1,
};
self.setCursorPos(1, 1);
}
fn getOrPutCell(self: *Terminal, x: usize, y: usize) *Screen.Cell {
const tracy = trace(@src());
defer tracy.end();
return self.screen.getCell(y, x);
}
test "Terminal: input with no control characters" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
try testing.expectEqual(@as(usize, 0), t.cursor.y);
try testing.expectEqual(@as(usize, 5), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello", 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.cursor.y);
try testing.expectEqual(@as(usize, 2), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hel\nlo", str);
}
}
test "Terminal: print scrolls back to bottom" {
var t = try init(testing.allocator, 5, 2);
defer t.deinit(testing.allocator);
// Basic grid writing
for ("hello") |c| try t.print(c);
// Make newlines so we create scrollback
// 3 pushes hello off the screen
t.index();
t.index();
t.index();
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
// Scroll to the top
t.scrollViewport(.{ .top = {} });
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello", str);
}
// Type
try t.print('A');
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\nA", 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();
t.linefeed();
for ("world") |c| try t.print(c);
try testing.expectEqual(@as(usize, 1), t.cursor.y);
try testing.expectEqual(@as(usize, 5), t.cursor.x);
{
var 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.cursor.pending_wrap == true);
t.linefeed();
try testing.expect(t.cursor.pending_wrap == false);
}
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.cursor.pending_wrap == true);
t.carriageReturn();
try testing.expect(t.cursor.pending_wrap == false);
}
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.cursor.y);
try testing.expectEqual(@as(usize, 5), t.cursor.x);
{
var 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, 80, 5);
defer t.deinit(alloc);
// HT
try t.print('1');
try t.horizontalTab();
try testing.expectEqual(@as(usize, 7), t.cursor.x);
// HT
try t.horizontalTab();
try testing.expectEqual(@as(usize, 15), t.cursor.x);
}
test "Terminal: setCursorPosition" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 0), t.cursor.y);
// Setting it to 0 should keep it zero (1 based)
t.setCursorPos(0, 0);
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 0), t.cursor.y);
// Should clamp to size
t.setCursorPos(81, 81);
try testing.expectEqual(@as(usize, 79), t.cursor.x);
try testing.expectEqual(@as(usize, 79), t.cursor.y);
// Should reset pending wrap
t.setCursorPos(0, 80);
try t.print('c');
try testing.expect(t.cursor.pending_wrap);
t.setCursorPos(0, 80);
try testing.expect(!t.cursor.pending_wrap);
// Origin mode
t.modes.origin = 1;
// No change without a scroll region
t.setCursorPos(81, 81);
try testing.expectEqual(@as(usize, 79), t.cursor.x);
try testing.expectEqual(@as(usize, 79), t.cursor.y);
// Set the scroll region
t.setScrollingRegion(10, t.rows);
t.setCursorPos(0, 0);
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 9), t.cursor.y);
t.setCursorPos(100, 0);
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 79), t.cursor.y);
t.setScrollingRegion(10, 11);
t.setCursorPos(2, 0);
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 10), t.cursor.y);
}
test "Terminal: setScrollingRegion" {
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
// Initial value
try testing.expectEqual(@as(usize, 0), t.scrolling_region.top);
try testing.expectEqual(@as(usize, t.rows - 1), t.scrolling_region.bottom);
// Move our cusor so we can verify we move it back
t.setCursorPos(5, 5);
t.setScrollingRegion(3, 7);
// Cursor should move back to top-left
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 0), t.cursor.y);
// Scroll region is set
try testing.expectEqual(@as(usize, 2), t.scrolling_region.top);
try testing.expectEqual(@as(usize, 6), t.scrolling_region.bottom);
// Scroll region invalid
t.setScrollingRegion(7, 3);
try testing.expectEqual(@as(usize, 0), t.scrolling_region.top);
try testing.expectEqual(@as(usize, t.rows - 1), t.scrolling_region.bottom);
// Scroll region with zero top and bottom
t.setScrollingRegion(0, 0);
try testing.expectEqual(@as(usize, 0), t.scrolling_region.top);
try testing.expectEqual(@as(usize, t.rows - 1), t.scrolling_region.bottom);
}
test "Terminal: deleteLines" {
const alloc = testing.allocator;
var t = try init(alloc, 80, 80);
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
try t.print('C');
t.carriageReturn();
t.linefeed();
try t.print('D');
t.cursorUp(2);
t.deleteLines(1);
try t.print('E');
t.carriageReturn();
t.linefeed();
// We should be
try testing.expectEqual(@as(usize, 0), t.cursor.x);
try testing.expectEqual(@as(usize, 2), t.cursor.y);
{
var 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();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
try t.print('C');
t.carriageReturn();
t.linefeed();
try t.print('D');
t.setScrollingRegion(1, 3);
t.setCursorPos(1, 1);
t.deleteLines(1);
try t.print('E');
t.carriageReturn();
t.linefeed();
// We should be
// try testing.expectEqual(@as(usize, 0), t.cursor.x);
// try testing.expectEqual(@as(usize, 2), t.cursor.y);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("E\nC\n\nD", str);
}
}
test "Terminal: insertLines" {
const alloc = testing.allocator;
var t = try init(alloc, 2, 5);
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
try t.print('C');
t.carriageReturn();
t.linefeed();
try t.print('D');
t.carriageReturn();
t.linefeed();
try t.print('E');
// Move to row 2
t.setCursorPos(2, 1);
// Insert two lines
t.insertLines(2);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A\n\n\nB\nC", str);
}
}
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();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
try t.print('C');
t.carriageReturn();
t.linefeed();
try t.print('D');
t.carriageReturn();
t.linefeed();
try t.print('E');
t.setScrollingRegion(1, 2);
t.setCursorPos(1, 1);
t.insertLines(1);
try t.print('X');
{
var 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();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
try t.print('C');
t.carriageReturn();
t.linefeed();
try t.print('D');
t.carriageReturn();
t.linefeed();
try t.print('E');
// Move to row 2
t.setCursorPos(2, 1);
// Insert a bunch of lines
t.insertLines(20);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("A", str);
}
}
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();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
try t.print('C');
try t.reverseIndex();
try t.print('D');
t.carriageReturn();
t.linefeed();
t.carriageReturn();
t.linefeed();
{
var 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();
t.linefeed();
try t.print('B');
t.carriageReturn();
t.linefeed();
t.carriageReturn();
t.linefeed();
t.setCursorPos(1, 1);
try t.reverseIndex();
try t.print('D');
t.carriageReturn();
t.linefeed();
t.setCursorPos(1, 1);
try t.reverseIndex();
try t.print('E');
t.carriageReturn();
t.linefeed();
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("E\nD\nA\nB", str);
}
}
test "Terminal: index" {
const alloc = testing.allocator;
var t = try init(alloc, 2, 5);
defer t.deinit(alloc);
t.index();
try t.print('A');
{
var 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.index();
try t.print('B');
{
var 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.cursor.y);
t.setScrollingRegion(2, 5);
t.index();
try testing.expectEqual(@as(usize, 1), t.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.setScrollingRegion(1, 2);
t.setCursorPos(5, 1);
try t.print('A');
t.index();
try t.print('B');
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("\n\n\n\nAB", str);
}
}
test "Terminal: DECALN" {
const alloc = testing.allocator;
var t = try init(alloc, 2, 2);
defer t.deinit(alloc);
// Initial value
try t.print('A');
t.carriageReturn();
t.linefeed();
try t.print('B');
t.decaln();
try testing.expectEqual(@as(usize, 0), t.cursor.y);
try testing.expectEqual(@as(usize, 0), t.cursor.x);
{
var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("EE\nEE", str);
}
}