New Screen Storage

This rewrites the "screen" implementation. The immediate user-impacting highlights:

  * `time cat <large file>.txt` is ~25% faster
  * Fully loaded (full scrollback) memory usage is 25% smaller
  * Scrollback is now dynamically allocated. This lowers initial memory usage
  * Resize without reflow (common with full screen apps like vim) is now ~10% faster
  * Resize with text reflow is ~20% faster when the column count doesn't change
  * All resize operations now maintain the cursor position correctly in more cases

Besides those impacts, the goals of this rewrite was to lay the foundation for the screen state to performantly handle text shaping (ligatures) and grapheme clusters (unicode zero-width joiners, combination markers, etc.). This new structure will let us track dirty state on a row-by-row basis for better caching of GPU resources, eventually (not in this PR), which is particularly important for text shaping (ligatures).
This commit is contained in:
Mitchell Hashimoto
2022-09-03 21:50:24 -07:00
committed by GitHub
11 changed files with 1880 additions and 1144 deletions

View File

@ -15,6 +15,9 @@ pub usingnamespace if (enabled) Impl else Noop;
const Impl = struct { const Impl = struct {
const c = @cImport({ const c = @cImport({
//uncomment to enable callstacks, very slow
//@cDefine("TRACY_CALLSTACK", "");
@cDefine("TRACY_ENABLE", ""); @cDefine("TRACY_ENABLE", "");
@cInclude("TracyC.h"); @cInclude("TracyC.h");
}); });

View File

@ -322,7 +322,7 @@ pub fn deinit(self: *Grid) void {
/// ///
/// Note this doesn't have to typically be manually called. Internally, /// Note this doesn't have to typically be manually called. Internally,
/// the renderer will do this when it needs more memory space. /// the renderer will do this when it needs more memory space.
pub fn rebuildCells(self: *Grid, term: Terminal) !void { pub fn rebuildCells(self: *Grid, term: *Terminal) !void {
const t = trace(@src()); const t = trace(@src());
defer t.end(); defer t.end();
@ -344,9 +344,13 @@ pub fn rebuildCells(self: *Grid, term: Terminal) !void {
// Build each cell // Build each cell
var rowIter = term.screen.rowIterator(.viewport); var rowIter = term.screen.rowIterator(.viewport);
var y: usize = 0; var y: usize = 0;
while (rowIter.next()) |line| { while (rowIter.next()) |row| {
defer y += 1; defer y += 1;
for (line) |cell, x| {
var cellIter = row.cellIterator();
var x: usize = 0;
while (cellIter.next()) |cell| {
defer x += 1;
assert(try self.updateCell(term, cell, x, y)); assert(try self.updateCell(term, cell, x, y));
} }
} }
@ -358,7 +362,7 @@ pub fn rebuildCells(self: *Grid, term: Terminal) !void {
/// This should be called prior to render to finalize the cells and prepare /// This should be called prior to render to finalize the cells and prepare
/// for render. This performs tasks such as preparing the cursor, refreshing /// for render. This performs tasks such as preparing the cursor, refreshing
/// the cells if necessary, etc. /// the cells if necessary, etc.
pub fn finalizeCells(self: *Grid, term: Terminal) !void { pub fn finalizeCells(self: *Grid, term: *Terminal) !void {
// Add the cursor // Add the cursor
// TODO: only add cursor if it changed // TODO: only add cursor if it changed
if (self.cells.items.len < self.cells.capacity) if (self.cells.items.len < self.cells.capacity)
@ -375,10 +379,11 @@ pub fn finalizeCells(self: *Grid, term: Terminal) !void {
try self.flushAtlas(); try self.flushAtlas();
} }
fn addCursor(self: *Grid, term: Terminal) void { fn addCursor(self: *Grid, term: *Terminal) void {
// Add the cursor // Add the cursor
if (self.cursor_visible and term.screen.viewportIsBottom()) { if (self.cursor_visible and term.screen.viewportIsBottom()) {
const cell = term.screen.getCell( const cell = term.screen.getCell(
.active,
term.screen.cursor.y, term.screen.cursor.y,
term.screen.cursor.x, term.screen.cursor.x,
); );
@ -410,7 +415,7 @@ fn addCursor(self: *Grid, term: Terminal) void {
/// needed. /// needed.
pub fn updateCell( pub fn updateCell(
self: *Grid, self: *Grid,
term: Terminal, term: *Terminal,
cell: terminal.Screen.Cell, cell: terminal.Screen.Cell,
x: usize, x: usize,
y: usize, y: usize,
@ -454,14 +459,14 @@ pub fn updateCell(
const res: BgFg = if (!cell.attrs.inverse) .{ const res: BgFg = if (!cell.attrs.inverse) .{
// In normal mode, background and fg match the cell. We // In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color. // un-optionalize the fg by defaulting to our fg color.
.bg = cell.bg, .bg = if (cell.attrs.has_bg) cell.bg else null,
.fg = cell.fg orelse self.foreground, .fg = if (cell.attrs.has_fg) cell.fg else self.foreground,
} else .{ } else .{
// In inverted mode, the background MUST be set to something // In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The // (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background. // fg is either the bg or default background.
.bg = cell.fg orelse self.foreground, .bg = if (cell.attrs.has_fg) cell.fg else self.foreground,
.fg = cell.bg orelse self.background, .fg = if (cell.attrs.has_bg) cell.bg else self.background,
}; };
break :colors res; break :colors res;
}; };

View File

@ -33,6 +33,7 @@ const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
/// Allocator /// Allocator
alloc: Allocator, alloc: Allocator,
alloc_io_arena: std.heap.ArenaAllocator,
/// The glfw window handle. /// The glfw window handle.
window: glfw.Window, window: glfw.Window,
@ -323,8 +324,15 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
errdefer cursor.destroy(); errdefer cursor.destroy();
try window.setCursor(cursor); try window.setCursor(cursor);
// Create our IO allocator arena. Libuv appears to guarantee (in code,
// not in docs) that read_alloc is called directly before a read so
// we can use an arena to make allocation faster.
var io_arena = std.heap.ArenaAllocator.init(alloc);
errdefer io_arena.deinit();
self.* = .{ self.* = .{
.alloc = alloc, .alloc = alloc,
.alloc_io_arena = io_arena,
.window = window, .window = window,
.cursor = cursor, .cursor = cursor,
.focused = false, .focused = false,
@ -410,6 +418,8 @@ pub fn destroy(self: *Window) void {
// We can destroy the cursor right away. glfw will just revert any // We can destroy the cursor right away. glfw will just revert any
// windows using it to the default. // windows using it to the default.
self.cursor.destroy(); self.cursor.destroy();
self.alloc_io_arena.deinit();
} }
pub fn shouldClose(self: Window) bool { pub fn shouldClose(self: Window) bool {
@ -529,7 +539,8 @@ fn charCallback(window: glfw.Window, codepoint: u21) void {
// We want to scroll to the bottom // We want to scroll to the bottom
// TODO: detect if we're at the bottom to avoid the render call here. // TODO: detect if we're at the bottom to avoid the render call here.
win.terminal.scrollViewport(.{ .bottom = {} }); win.terminal.scrollViewport(.{ .bottom = {} }) catch |err|
log.err("error scrolling viewport err={}", .{err});
win.render_timer.schedule() catch |err| win.render_timer.schedule() catch |err|
log.err("error scheduling render in charCallback err={}", .{err}); log.err("error scheduling render in charCallback err={}", .{err});
@ -785,7 +796,8 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
const sign: isize = if (yoff > 0) -1 else 1; const sign: isize = if (yoff > 0) -1 else 1;
const delta: isize = sign * @maximum(@divFloor(win.grid.size.rows, 15), 1); const delta: isize = sign * @maximum(@divFloor(win.grid.size.rows, 15), 1);
log.info("scroll: delta={}", .{delta}); log.info("scroll: delta={}", .{delta});
win.terminal.scrollViewport(.{ .delta = delta }); win.terminal.scrollViewport(.{ .delta = delta }) catch |err|
log.err("error scrolling viewport err={}", .{err});
// Schedule render since scrolling usually does something. // Schedule render since scrolling usually does something.
// TODO(perf): we can only schedule render if we know scrolling // TODO(perf): we can only schedule render if we know scrolling
@ -1233,7 +1245,8 @@ fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
const alloc = t.loop().getData(Allocator).?.*; const win = t.getData(Window) orelse return null;
const alloc = win.alloc_io_arena.allocator();
return alloc.alloc(u8, size) catch null; return alloc.alloc(u8, size) catch null;
} }
@ -1243,7 +1256,10 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
defer tracy.end(); defer tracy.end();
const win = t.getData(Window).?; const win = t.getData(Window).?;
defer win.alloc.free(buf); defer {
const alloc = win.alloc_io_arena.allocator();
alloc.free(buf);
}
// log.info("DATA: {d}", .{n}); // log.info("DATA: {d}", .{n});
// log.info("DATA: {any}", .{buf[0..@intCast(usize, n)]}); // log.info("DATA: {any}", .{buf[0..@intCast(usize, n)]});
@ -1269,9 +1285,44 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
// Schedule a render // Schedule a render
win.render_timer.schedule() catch unreachable; win.render_timer.schedule() catch unreachable;
// Process the terminal data // Process the terminal data. This is an extremely hot part of the
win.terminal_stream.nextSlice(buf[0..@intCast(usize, n)]) catch |err| // terminal emulator, so we do some abstraction leakage to avoid
log.err("error processing terminal data: {}", .{err}); // function calls and unnecessary logic.
//
// The ground state is the only state that we can see and print/execute
// ASCII, so we only execute this hot path if we're already in the ground
// state.
//
// Empirically, this alone improved throughput of large text output by ~20%.
var i: usize = 0;
const end = @intCast(usize, n);
if (win.terminal_stream.parser.state == .ground) {
for (buf[i..end]) |c| {
switch (terminal.parse_table.table[c][@enumToInt(terminal.Parser.State.ground)].action) {
// Print, call directly.
.print => win.print(@intCast(u21, c)) catch |err|
log.err("error processing terminal data: {}", .{err}),
// C0 execute, let our stream handle this one but otherwise
// continue since we're guaranteed to be back in ground.
.execute => win.terminal_stream.execute(c) catch |err|
log.err("error processing terminal data: {}", .{err}),
// Otherwise, break out and go the slow path until we're
// back in ground. There is a slight optimization here where
// could try to find the next transition to ground but when
// I implemented that it didn't materially change performance.
else => break,
}
i += 1;
}
}
if (i < end) {
win.terminal_stream.nextSlice(buf[i..end]) catch |err|
log.err("error processing terminal data: {}", .{err});
}
} }
fn ttyWrite(req: *libuv.WriteReq, status: i32) void { fn ttyWrite(req: *libuv.WriteReq, status: i32) void {
@ -1338,11 +1389,11 @@ fn renderTimerCallback(t: *libuv.Timer) void {
gl.clear(gl.c.GL_COLOR_BUFFER_BIT); gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
// For now, rebuild all cells // For now, rebuild all cells
win.grid.rebuildCells(win.terminal) catch |err| win.grid.rebuildCells(&win.terminal) catch |err|
log.err("error calling rebuildCells in render timer err={}", .{err}); log.err("error calling rebuildCells in render timer err={}", .{err});
// Finalize the cells prior to render // Finalize the cells prior to render
win.grid.finalizeCells(win.terminal) catch |err| win.grid.finalizeCells(&win.terminal) catch |err|
log.err("error calling updateCells in render timer err={}", .{err}); log.err("error calling updateCells in render timer err={}", .{err});
// Render the grid // Render the grid
@ -1382,7 +1433,9 @@ pub fn horizontalTab(self: *Window) !void {
} }
pub fn linefeed(self: *Window) !void { pub fn linefeed(self: *Window) !void {
self.terminal.linefeed(); // Small optimization: call index instead of linefeed because they're
// identical and this avoids one layer of function call overhead.
try self.terminal.index();
} }
pub fn carriageReturn(self: *Window) !void { pub fn carriageReturn(self: *Window) !void {
@ -1426,7 +1479,7 @@ pub fn setCursorPos(self: *Window, row: u16, col: u16) !void {
pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void { pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void {
if (mode == .complete) { if (mode == .complete) {
// Whenever we erase the full display, scroll to bottom. // Whenever we erase the full display, scroll to bottom.
self.terminal.scrollViewport(.{ .bottom = {} }); try self.terminal.scrollViewport(.{ .bottom = {} });
try self.render_timer.schedule(); try self.render_timer.schedule();
} }
@ -1462,12 +1515,12 @@ pub fn reverseIndex(self: *Window) !void {
} }
pub fn index(self: *Window) !void { pub fn index(self: *Window) !void {
self.terminal.index(); try self.terminal.index();
} }
pub fn nextLine(self: *Window) !void { pub fn nextLine(self: *Window) !void {
self.terminal.carriageReturn(); self.terminal.carriageReturn();
self.terminal.index(); try self.terminal.index();
} }
pub fn setTopAndBottomMargin(self: *Window, top: u16, bot: u16) !void { pub fn setTopAndBottomMargin(self: *Window, top: u16, bot: u16) !void {

View File

@ -74,7 +74,7 @@ pub const RunIterator = struct {
i: usize = 0, i: usize = 0,
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
if (self.i >= self.row.len) return null; if (self.i >= self.row.lenCells()) return null;
// Track the font for our curent run // Track the font for our curent run
var current_font: Group.FontIndex = .{}; var current_font: Group.FontIndex = .{};
@ -85,8 +85,8 @@ pub const RunIterator = struct {
// Go through cell by cell and accumulate while we build our run. // Go through cell by cell and accumulate while we build our run.
var j: usize = self.i; var j: usize = self.i;
while (j < self.row.len) : (j += 1) { while (j < self.row.lenCells()) : (j += 1) {
const cell = self.row[j]; const cell = self.row.getCell(j);
// Ignore tailing wide spacers, this will get fixed up by the shaper // Ignore tailing wide spacers, this will get fixed up by the shaper
if (cell.empty() or cell.attrs.wide_spacer_tail) continue; if (cell.empty() or cell.attrs.wide_spacer_tail) continue;
@ -129,8 +129,8 @@ test "run iterator" {
{ {
// Make a screen with some data // Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 5, 0); var screen = try terminal.Screen.init(alloc, 3, 5, 0);
defer screen.deinit(alloc); defer screen.deinit();
screen.testWriteString("ABCD"); try screen.testWriteString("ABCD");
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
@ -143,8 +143,8 @@ test "run iterator" {
{ {
// Make a screen with some data // Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 5, 0); var screen = try terminal.Screen.init(alloc, 3, 5, 0);
defer screen.deinit(alloc); defer screen.deinit();
screen.testWriteString("A😃D"); try screen.testWriteString("A😃D");
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;
@ -175,8 +175,8 @@ test "shape" {
// Make a screen with some data // Make a screen with some data
var screen = try terminal.Screen.init(alloc, 3, 10, 0); var screen = try terminal.Screen.init(alloc, 3, 10, 0);
defer screen.deinit(alloc); defer screen.deinit();
screen.testWriteString(buf[0..buf_idx]); try screen.testWriteString(buf[0..buf_idx]);
// Get our run iterator // Get our run iterator
var shaper = testdata.shaper; var shaper = testdata.shaper;

View File

@ -6,6 +6,7 @@ const Parser = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const trace = @import("tracy").trace;
const testing = std.testing; const testing = std.testing;
const table = @import("parse_table.zig").table; const table = @import("parse_table.zig").table;
const osc = @import("osc.zig"); const osc = @import("osc.zig");
@ -212,6 +213,9 @@ pub fn init() Parser {
/// Up to 3 actions may need to be exected -- in order -- representing /// Up to 3 actions may need to be exected -- in order -- representing
/// the state exit, transition, and entry actions. /// the state exit, transition, and entry actions.
pub fn next(self: *Parser, c: u8) [3]?Action { pub fn next(self: *Parser, c: u8) [3]?Action {
const tracy = trace(@src());
defer tracy.end();
// If we're processing UTF-8, we handle this manually. // If we're processing UTF-8, we handle this manually.
if (self.state == .utf8) { if (self.state == .utf8) {
return .{ self.next_utf8(c), null, null }; return .{ self.next_utf8(c), null, null };

File diff suppressed because it is too large Load Diff

View File

@ -149,8 +149,8 @@ pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal {
pub fn deinit(self: *Terminal, alloc: Allocator) void { pub fn deinit(self: *Terminal, alloc: Allocator) void {
self.tabstops.deinit(alloc); self.tabstops.deinit(alloc);
self.screen.deinit(alloc); self.screen.deinit();
self.secondary_screen.deinit(alloc); self.secondary_screen.deinit();
self.* = undefined; self.* = undefined;
} }
@ -286,11 +286,11 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) !
// If we're making the screen smaller, dealloc the unused items. // If we're making the screen smaller, dealloc the unused items.
if (self.active_screen == .primary) { if (self.active_screen == .primary) {
try self.screen.resize(alloc, rows, cols); try self.screen.resize(rows, cols);
try self.secondary_screen.resizeWithoutReflow(alloc, rows, cols); try self.secondary_screen.resizeWithoutReflow(rows, cols);
} else { } else {
try self.screen.resizeWithoutReflow(alloc, rows, cols); try self.screen.resizeWithoutReflow(rows, cols);
try self.secondary_screen.resize(alloc, rows, cols); try self.secondary_screen.resize(rows, cols);
} }
// Set our size // Set our size
@ -308,7 +308,7 @@ pub fn resize(self: *Terminal, alloc: Allocator, cols_req: usize, rows: usize) !
/// encoded as "\n". This omits any formatting such as fg/bg. /// encoded as "\n". This omits any formatting such as fg/bg.
/// ///
/// The caller must free the string. /// The caller must free the string.
pub fn plainString(self: Terminal, alloc: Allocator) ![]const u8 { pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 {
return try self.screen.testString(alloc, .viewport); return try self.screen.testString(alloc, .viewport);
} }
@ -336,8 +336,8 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
switch (attr) { switch (attr) {
.unset => { .unset => {
self.screen.cursor.pen.fg = null; self.screen.cursor.pen.attrs.has_fg = false;
self.screen.cursor.pen.bg = null; self.screen.cursor.pen.attrs.has_bg = false;
self.screen.cursor.pen.attrs = .{}; self.screen.cursor.pen.attrs = .{};
}, },
@ -362,6 +362,7 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
}, },
.direct_color_fg => |rgb| { .direct_color_fg => |rgb| {
self.screen.cursor.pen.attrs.has_fg = true;
self.screen.cursor.pen.fg = .{ self.screen.cursor.pen.fg = .{
.r = rgb.r, .r = rgb.r,
.g = rgb.g, .g = rgb.g,
@ -370,6 +371,7 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
}, },
.direct_color_bg => |rgb| { .direct_color_bg => |rgb| {
self.screen.cursor.pen.attrs.has_bg = true;
self.screen.cursor.pen.bg = .{ self.screen.cursor.pen.bg = .{
.r = rgb.r, .r = rgb.r,
.g = rgb.g, .g = rgb.g,
@ -377,21 +379,39 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
}; };
}, },
.@"8_fg" => |n| self.screen.cursor.pen.fg = color.default[@enumToInt(n)], .@"8_fg" => |n| {
self.screen.cursor.pen.attrs.has_fg = true;
self.screen.cursor.pen.fg = color.default[@enumToInt(n)];
},
.@"8_bg" => |n| self.screen.cursor.pen.bg = color.default[@enumToInt(n)], .@"8_bg" => |n| {
self.screen.cursor.pen.attrs.has_bg = true;
self.screen.cursor.pen.bg = color.default[@enumToInt(n)];
},
.reset_fg => self.screen.cursor.pen.fg = null, .reset_fg => self.screen.cursor.pen.attrs.has_fg = false,
.reset_bg => self.screen.cursor.pen.bg = null, .reset_bg => self.screen.cursor.pen.attrs.has_bg = false,
.@"8_bright_fg" => |n| self.screen.cursor.pen.fg = color.default[@enumToInt(n)], .@"8_bright_fg" => |n| {
self.screen.cursor.pen.attrs.has_fg = true;
self.screen.cursor.pen.fg = color.default[@enumToInt(n)];
},
.@"8_bright_bg" => |n| self.screen.cursor.pen.bg = color.default[@enumToInt(n)], .@"8_bright_bg" => |n| {
self.screen.cursor.pen.attrs.has_bg = true;
self.screen.cursor.pen.bg = color.default[@enumToInt(n)];
},
.@"256_fg" => |idx| self.screen.cursor.pen.fg = color.default[idx], .@"256_fg" => |idx| {
self.screen.cursor.pen.attrs.has_fg = true;
self.screen.cursor.pen.fg = color.default[idx];
},
.@"256_bg" => |idx| self.screen.cursor.pen.bg = color.default[idx], .@"256_bg" => |idx| {
self.screen.cursor.pen.attrs.has_bg = true;
self.screen.cursor.pen.bg = color.default[idx];
},
else => return error.InvalidAttribute, else => return error.InvalidAttribute,
} }
@ -440,11 +460,11 @@ pub fn print(self: *Terminal, c: u21) !void {
// If we're soft-wrapping, then handle that first. // If we're soft-wrapping, then handle that first.
if (self.screen.cursor.pending_wrap and self.modes.autowrap) if (self.screen.cursor.pending_wrap and self.modes.autowrap)
_ = self.printWrap(); try self.printWrap();
switch (width) { switch (width) {
// Single cell is very easy: just write in the cell // Single cell is very easy: just write in the cell
1 => _ = self.printCell(c), 1 => _ = @call(.{ .modifier = .always_inline }, self.printCell, .{c}),
// Wide character requires a spacer. We print this by // Wide character requires a spacer. We print this by
// using two cells: the first is flagged "wide" and has the // using two cells: the first is flagged "wide" and has the
@ -457,7 +477,7 @@ pub fn print(self: *Terminal, c: u21) !void {
if (self.screen.cursor.x == self.cols - 1) { if (self.screen.cursor.x == self.cols - 1) {
const spacer_head = self.printCell(' '); const spacer_head = self.printCell(' ');
spacer_head.attrs.wide_spacer_head = true; spacer_head.attrs.wide_spacer_head = true;
_ = self.printWrap(); try self.printWrap();
} }
const wide_cell = self.printCell(c); const wide_cell = self.printCell(c);
@ -485,6 +505,9 @@ pub fn print(self: *Terminal, c: u21) !void {
} }
fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell { fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell {
// const tracy = trace(@src());
// defer tracy.end();
const c = c: { const c = c: {
// TODO: non-utf8 handling, gr // TODO: non-utf8 handling, gr
@ -503,10 +526,8 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell {
break :c @intCast(u21, table[@intCast(u8, unmapped_c)]); break :c @intCast(u21, table[@intCast(u8, unmapped_c)]);
}; };
const cell = self.screen.getCell( const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
self.screen.cursor.y, const cell = row.getCellPtr(self.screen.cursor.x);
self.screen.cursor.x,
);
// If this cell is wide char then we need to clear it. // If this cell is wide char then we need to clear it.
// We ignore wide spacer HEADS because we can just write // We ignore wide spacer HEADS because we can just write
@ -515,7 +536,7 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell {
const x = self.screen.cursor.x + 1; const x = self.screen.cursor.x + 1;
assert(x < self.cols); assert(x < self.cols);
const spacer_cell = self.screen.getCell(self.screen.cursor.y, x); const spacer_cell = row.getCellPtr(x);
spacer_cell.attrs.wide_spacer_tail = false; spacer_cell.attrs.wide_spacer_tail = false;
if (self.screen.cursor.x <= 1) { if (self.screen.cursor.x <= 1) {
@ -525,7 +546,7 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell {
assert(self.screen.cursor.x > 0); assert(self.screen.cursor.x > 0);
const x = self.screen.cursor.x - 1; const x = self.screen.cursor.x - 1;
const wide_cell = self.screen.getCell(self.screen.cursor.y, x); const wide_cell = row.getCellPtr(x);
wide_cell.attrs.wide = false; wide_cell.attrs.wide = false;
if (self.screen.cursor.x <= 1) { if (self.screen.cursor.x <= 1) {
@ -539,26 +560,23 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell {
return cell; return cell;
} }
fn printWrap(self: *Terminal) *Screen.Cell { fn printWrap(self: *Terminal) !void {
// Mark that the cell is wrapped, which guarantees that there is const tracy = trace(@src());
// at least one cell after it in the next row. defer tracy.end();
const cell = self.screen.getCell(
self.screen.cursor.y, const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
self.screen.cursor.x, row.setWrapped(true);
);
cell.attrs.wrap = true;
// Move to the next line // Move to the next line
self.index(); try self.index();
self.screen.cursor.x = 0; self.screen.cursor.x = 0;
return cell;
} }
fn clearWideSpacerHead(self: *Terminal) void { fn clearWideSpacerHead(self: *Terminal) void {
// TODO: handle deleting wide char on row 0 of active // TODO: handle deleting wide char on row 0 of active
assert(self.screen.cursor.y >= 1); assert(self.screen.cursor.y >= 1);
const cell = self.screen.getCell( const cell = self.screen.getCellPtr(
.active,
self.screen.cursor.y - 1, self.screen.cursor.y - 1,
self.cols - 1, self.cols - 1,
); );
@ -578,14 +596,11 @@ pub fn decaln(self: *Terminal) void {
// Fill with Es, does not move cursor. We reset fg/bg so we can just // Fill with Es, does not move cursor. We reset fg/bg so we can just
// optimize here by doing row copies. // optimize here by doing row copies.
const filled = self.screen.getRow(.{ .active = 0 }); const filled = self.screen.getRow(.{ .active = 0 });
var col: usize = 0; filled.fill(.{ .char = 'E' });
while (col < self.cols) : (col += 1) {
filled[col] = .{ .char = 'E' };
}
var row: usize = 1; var row: usize = 1;
while (row < self.rows) : (row += 1) { while (row < self.rows) : (row += 1) {
std.mem.copy(Screen.Cell, self.screen.getRow(.{ .active = row }), filled); self.screen.getRow(.{ .active = row }).copyRow(filled);
} }
} }
@ -601,7 +616,7 @@ pub fn decaln(self: *Terminal) void {
/// move the cursor one line down /// move the cursor one line down
/// ///
/// This unsets the pending wrap state without wrapping. /// This unsets the pending wrap state without wrapping.
pub fn index(self: *Terminal) void { pub fn index(self: *Terminal) !void {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
@ -625,7 +640,7 @@ pub fn index(self: *Terminal) void {
if (self.scrolling_region.top == 0 and if (self.scrolling_region.top == 0 and
self.scrolling_region.bottom == self.rows - 1) self.scrolling_region.bottom == self.rows - 1)
{ {
self.screen.scroll(.{ .delta = 1 }); try self.screen.scroll(.{ .delta = 1 });
} else { } else {
// TODO: test // TODO: test
self.scrollUp(1); self.scrollUp(1);
@ -736,9 +751,8 @@ pub fn eraseDisplay(
switch (mode) { switch (mode) {
.complete => { .complete => {
const region = self.screen.region(.active); var it = self.screen.rowIterator(.active);
std.mem.set(Screen.Cell, region[0], self.screen.cursor.pen); while (it.next()) |row| row.clear(self.screen.cursor.pen);
std.mem.set(Screen.Cell, region[1], self.screen.cursor.pen);
// Unsets pending wrap state // Unsets pending wrap state
self.screen.cursor.pending_wrap = false; self.screen.cursor.pending_wrap = false;
@ -748,7 +762,7 @@ pub fn eraseDisplay(
// All lines to the right (including the cursor) // All lines to the right (including the cursor)
var x: usize = self.screen.cursor.x; var x: usize = self.screen.cursor.x;
while (x < self.cols) : (x += 1) { while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, self.screen.cursor.y); const cell = self.screen.getCellPtr(.active, self.screen.cursor.y, x);
cell.* = self.screen.cursor.pen; cell.* = self.screen.cursor.pen;
cell.char = 0; cell.char = 0;
} }
@ -758,7 +772,7 @@ pub fn eraseDisplay(
while (y < self.rows) : (y += 1) { while (y < self.rows) : (y += 1) {
x = 0; x = 0;
while (x < self.cols) : (x += 1) { while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, y); const cell = self.screen.getCellPtr(.active, y, x);
cell.* = self.screen.cursor.pen; cell.* = self.screen.cursor.pen;
cell.char = 0; cell.char = 0;
} }
@ -772,7 +786,7 @@ pub fn eraseDisplay(
// Erase to the left (including the cursor) // Erase to the left (including the cursor)
var x: usize = 0; var x: usize = 0;
while (x <= self.screen.cursor.x) : (x += 1) { while (x <= self.screen.cursor.x) : (x += 1) {
const cell = self.getOrPutCell(x, self.screen.cursor.y); const cell = self.screen.getCellPtr(.active, self.screen.cursor.y, x);
cell.* = self.screen.cursor.pen; cell.* = self.screen.cursor.pen;
cell.char = 0; cell.char = 0;
} }
@ -782,7 +796,7 @@ pub fn eraseDisplay(
while (y < self.screen.cursor.y) : (y += 1) { while (y < self.screen.cursor.y) : (y += 1) {
x = 0; x = 0;
while (x < self.cols) : (x += 1) { while (x < self.cols) : (x += 1) {
const cell = self.getOrPutCell(x, y); const cell = self.screen.getCellPtr(.active, y, x);
cell.* = self.screen.cursor.pen; cell.* = self.screen.cursor.pen;
cell.char = 0; cell.char = 0;
} }
@ -792,16 +806,7 @@ pub fn eraseDisplay(
self.screen.cursor.pending_wrap = false; self.screen.cursor.pending_wrap = false;
}, },
.scrollback => { .scrollback => self.screen.clearHistory(),
const region = self.screen.region(.history);
std.mem.set(Screen.Cell, region[0], self.screen.cursor.pen);
std.mem.set(Screen.Cell, region[1], self.screen.cursor.pen);
// TODO: move this logic to the Screen implementation
self.screen.top = self.screen.visible_offset;
self.screen.bottom = self.screen.bottom - self.screen.visible_offset;
self.screen.visible_offset = 0;
},
} }
} }
@ -817,12 +822,12 @@ pub fn eraseLine(
switch (mode) { switch (mode) {
.right => { .right => {
const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
std.mem.set(Screen.Cell, row[self.screen.cursor.x..], self.screen.cursor.pen); row.fillSlice(self.screen.cursor.pen, self.screen.cursor.x, self.cols);
}, },
.left => { .left => {
const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
std.mem.set(Screen.Cell, row[0 .. self.screen.cursor.x + 1], self.screen.cursor.pen); row.fillSlice(self.screen.cursor.pen, 0, self.screen.cursor.x + 1);
// Unsets pending wrap state // Unsets pending wrap state
self.screen.cursor.pending_wrap = false; self.screen.cursor.pending_wrap = false;
@ -830,7 +835,7 @@ pub fn eraseLine(
.complete => { .complete => {
const row = self.screen.getRow(.{ .active = self.screen.cursor.y }); const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
std.mem.set(Screen.Cell, row, self.screen.cursor.pen); row.fill(self.screen.cursor.pen);
}, },
else => { else => {
@ -864,8 +869,9 @@ pub fn deleteChars(self: *Terminal, count: usize) !void {
var i: usize = self.screen.cursor.x; var i: usize = self.screen.cursor.x;
while (i < end) : (i += 1) { while (i < end) : (i += 1) {
const j = i + count; const j = i + count;
line[i] = line[j]; const j_cell = line.getCellPtr(j);
line[j].char = 0; line.getCellPtr(i).* = j_cell.*;
j_cell.char = 0;
} }
} }
@ -879,12 +885,10 @@ pub fn eraseChars(self: *Terminal, count: usize) void {
const end = @minimum(self.cols, self.screen.cursor.x + count); const end = @minimum(self.cols, self.screen.cursor.x + count);
// Shift // Shift
var x: usize = self.screen.cursor.x; var pen = self.screen.cursor.pen;
while (x < end) : (x += 1) { pen.char = 0;
const cell = self.getOrPutCell(x, self.screen.cursor.y); const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
cell.* = self.screen.cursor.pen; row.fillSlice(pen, self.screen.cursor.x, end);
cell.char = 0;
}
} }
/// Move the cursor to the left amount cells. If amount is 0, adjust it to 1. /// Move the cursor to the left amount cells. If amount is 0, adjust it to 1.
@ -994,11 +998,11 @@ pub fn carriageReturn(self: *Terminal) void {
} }
/// Linefeed moves the cursor to the next line. /// Linefeed moves the cursor to the next line.
pub fn linefeed(self: *Terminal) void { pub fn linefeed(self: *Terminal) !void {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
self.index(); try self.index();
} }
/// Inserts spaces at current cursor position moving existing cell contents /// Inserts spaces at current cursor position moving existing cell contents
@ -1031,7 +1035,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// This is the number of spaces we have left to shift existing data. // This is the number of spaces we have left to shift existing data.
// If count is bigger than the available space left after the cursor, // If count is bigger than the available space left after the cursor,
// we may have no space at all for copying. // we may have no space at all for copying.
const copyable = row.len - pivot; const copyable = self.screen.cols - pivot;
if (copyable > 0) { if (copyable > 0) {
// This is the index of the final copyable value that we need to copy. // This is the index of the final copyable value that we need to copy.
const copyable_end = start + copyable - 1; const copyable_end = start + copyable - 1;
@ -1040,16 +1044,16 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// allocated new space, otherwise we'll copy duplicates. // allocated new space, otherwise we'll copy duplicates.
var i: usize = 0; var i: usize = 0;
while (i < copyable) : (i += 1) { while (i < copyable) : (i += 1) {
const to = row.len - 1 - i; const to = self.screen.cols - 1 - i;
const from = copyable_end - i; const from = copyable_end - i;
row[to] = row[from]; row.getCellPtr(to).* = row.getCell(from);
} }
} }
// Insert zero // Insert zero
var pen = self.screen.cursor.pen; var pen = self.screen.cursor.pen;
pen.char = ' '; // NOTE: this should be 0 but we need space for tests pen.char = ' '; // NOTE: this should be 0 but we need space for tests
std.mem.set(Screen.Cell, row[start..pivot], pen); row.fillSlice(pen, start, pivot);
} }
/// Insert amount lines at the current cursor row. The contents of the line /// Insert amount lines at the current cursor row. The contents of the line
@ -1091,18 +1095,14 @@ pub fn insertLines(self: *Terminal, count: usize) void {
// Ensure we have the lines populated to the end // Ensure we have the lines populated to the end
while (y > top) : (y -= 1) { while (y > top) : (y -= 1) {
self.screen.copyRow(y, y - adjusted_count); self.screen.copyRow(.{ .active = y }, .{ .active = y - adjusted_count });
} }
// Insert count blank lines // Insert count blank lines
y = self.screen.cursor.y; y = self.screen.cursor.y;
while (y < self.screen.cursor.y + adjusted_count) : (y += 1) { while (y < self.screen.cursor.y + adjusted_count) : (y += 1) {
var x: usize = 0; const row = self.screen.getRow(.{ .active = y });
while (x < self.cols) : (x += 1) { row.clear(self.screen.cursor.pen);
const cell = self.getOrPutCell(x, y);
cell.* = self.screen.cursor.pen;
cell.char = 0;
}
} }
} }
@ -1140,12 +1140,12 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
// Scroll up the count amount. // Scroll up the count amount.
var y: usize = self.screen.cursor.y; var y: usize = self.screen.cursor.y;
while (y <= self.scrolling_region.bottom - adjusted_count) : (y += 1) { while (y <= self.scrolling_region.bottom - adjusted_count) : (y += 1) {
self.screen.copyRow(y, y + adjusted_count); self.screen.copyRow(.{ .active = y }, .{ .active = y + adjusted_count });
} }
while (y <= self.scrolling_region.bottom) : (y += 1) { while (y <= self.scrolling_region.bottom) : (y += 1) {
const row = self.screen.getRow(.{ .active = y }); const row = self.screen.getRow(.{ .active = y });
std.mem.set(Screen.Cell, row, self.screen.cursor.pen); row.fill(self.screen.cursor.pen);
} }
} }
@ -1194,11 +1194,11 @@ pub const ScrollViewport = union(enum) {
}; };
/// Scroll the viewport of the terminal grid. /// Scroll the viewport of the terminal grid.
pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void { pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void {
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
self.screen.scroll(switch (behavior) { try self.screen.scroll(switch (behavior) {
.top => .{ .top = {} }, .top => .{ .top = {} },
.bottom => .{ .bottom = {} }, .bottom => .{ .bottom = {} },
.delta => |delta| .{ .delta_no_grow = delta }, .delta => |delta| .{ .delta_no_grow = delta },
@ -1236,13 +1236,6 @@ pub fn setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void {
self.setCursorPos(1, 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" { test "Terminal: input with no control characters" {
var t = try init(testing.allocator, 80, 80); var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator); defer t.deinit(testing.allocator);
@ -1282,9 +1275,9 @@ test "Terminal: print writes to bottom if scrolled" {
// Make newlines so we create scrollback // Make newlines so we create scrollback
// 3 pushes hello off the screen // 3 pushes hello off the screen
t.index(); try t.index();
t.index(); try t.index();
t.index(); try t.index();
{ {
var str = try t.plainString(testing.allocator); var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str); defer testing.allocator.free(str);
@ -1292,7 +1285,7 @@ test "Terminal: print writes to bottom if scrolled" {
} }
// Scroll to the top // Scroll to the top
t.scrollViewport(.{ .top = {} }); try t.scrollViewport(.{ .top = {} });
{ {
var str = try t.plainString(testing.allocator); var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str); defer testing.allocator.free(str);
@ -1301,7 +1294,7 @@ test "Terminal: print writes to bottom if scrolled" {
// Type // Type
try t.print('A'); try t.print('A');
t.scrollViewport(.{ .bottom = {} }); try t.scrollViewport(.{ .bottom = {} });
{ {
var str = try t.plainString(testing.allocator); var str = try t.plainString(testing.allocator);
defer testing.allocator.free(str); defer testing.allocator.free(str);
@ -1378,7 +1371,7 @@ test "Terminal: linefeed and carriage return" {
// Basic grid writing // Basic grid writing
for ("hello") |c| try t.print(c); for ("hello") |c| try t.print(c);
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
for ("world") |c| try t.print(c); for ("world") |c| try t.print(c);
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
try testing.expectEqual(@as(usize, 5), t.screen.cursor.x); try testing.expectEqual(@as(usize, 5), t.screen.cursor.x);
@ -1396,7 +1389,7 @@ test "Terminal: linefeed unsets pending wrap" {
// Basic grid writing // Basic grid writing
for ("hello") |c| try t.print(c); for ("hello") |c| try t.print(c);
try testing.expect(t.screen.cursor.pending_wrap == true); try testing.expect(t.screen.cursor.pending_wrap == true);
t.linefeed(); try t.linefeed();
try testing.expect(t.screen.cursor.pending_wrap == false); try testing.expect(t.screen.cursor.pending_wrap == false);
} }
@ -1540,13 +1533,13 @@ test "Terminal: deleteLines" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('C'); try t.print('C');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('D'); try t.print('D');
t.cursorUp(2); t.cursorUp(2);
@ -1554,7 +1547,7 @@ test "Terminal: deleteLines" {
try t.print('E'); try t.print('E');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
// We should be // We should be
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
@ -1575,13 +1568,13 @@ test "Terminal: deleteLines with scroll region" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('C'); try t.print('C');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('D'); try t.print('D');
t.setScrollingRegion(1, 3); t.setScrollingRegion(1, 3);
@ -1590,7 +1583,7 @@ test "Terminal: deleteLines with scroll region" {
try t.print('E'); try t.print('E');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
// We should be // We should be
// try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); // try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
@ -1611,16 +1604,16 @@ test "Terminal: insertLines" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('C'); try t.print('C');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('D'); try t.print('D');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('E'); try t.print('E');
// Move to row 2 // Move to row 2
@ -1644,16 +1637,16 @@ test "Terminal: insertLines with scroll region" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('C'); try t.print('C');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('D'); try t.print('D');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('E'); try t.print('E');
t.setScrollingRegion(1, 2); t.setScrollingRegion(1, 2);
@ -1677,16 +1670,16 @@ test "Terminal: insertLines more than remaining" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('C'); try t.print('C');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('D'); try t.print('D');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('E'); try t.print('E');
// Move to row 2 // Move to row 2
@ -1710,17 +1703,17 @@ test "Terminal: reverseIndex" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('C'); try t.print('C');
try t.reverseIndex(); try t.reverseIndex();
try t.print('D'); try t.print('D');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
{ {
var str = try t.plainString(testing.allocator); var str = try t.plainString(testing.allocator);
@ -1736,24 +1729,24 @@ test "Terminal: reverseIndex from the top" {
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
t.setCursorPos(1, 1); t.setCursorPos(1, 1);
try t.reverseIndex(); try t.reverseIndex();
try t.print('D'); try t.print('D');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
t.setCursorPos(1, 1); t.setCursorPos(1, 1);
try t.reverseIndex(); try t.reverseIndex();
try t.print('E'); try t.print('E');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
{ {
var str = try t.plainString(testing.allocator); var str = try t.plainString(testing.allocator);
@ -1767,7 +1760,7 @@ test "Terminal: index" {
var t = try init(alloc, 2, 5); var t = try init(alloc, 2, 5);
defer t.deinit(alloc); defer t.deinit(alloc);
t.index(); try t.index();
try t.print('A'); try t.print('A');
{ {
@ -1784,7 +1777,7 @@ test "Terminal: index from the bottom" {
t.setCursorPos(5, 1); t.setCursorPos(5, 1);
try t.print('A'); try t.print('A');
t.index(); try t.index();
try t.print('B'); try t.print('B');
@ -1802,7 +1795,7 @@ test "Terminal: index outside of scrolling region" {
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
t.setScrollingRegion(2, 5); t.setScrollingRegion(2, 5);
t.index(); try t.index();
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
} }
@ -1814,7 +1807,7 @@ test "Terminal: index from the bottom outside of scroll region" {
t.setScrollingRegion(1, 2); t.setScrollingRegion(1, 2);
t.setCursorPos(5, 1); t.setCursorPos(5, 1);
try t.print('A'); try t.print('A');
t.index(); try t.index();
try t.print('B'); try t.print('B');
{ {
@ -1832,7 +1825,7 @@ test "Terminal: DECALN" {
// Initial value // Initial value
try t.print('A'); try t.print('A');
t.carriageReturn(); t.carriageReturn();
t.linefeed(); try t.linefeed();
try t.print('B'); try t.print('B');
t.decaln(); t.decaln();

456
src/terminal/circ_buf.zig Normal file
View File

@ -0,0 +1,456 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
/// Returns a circular buffer containing type T.
pub fn CircBuf(comptime T: type, comptime default: T) type {
return struct {
const Self = @This();
// Implementation note: there's a lot of unsafe addition of usize
// here in this implementation that can technically overflow. If someone
// wants to fix this and make it overflow safe (use subtractions for
// checks prior to additions) then I welcome it. In reality, we'd
// have to be a really, really large terminal screen to even worry
// about this so I'm punting it.
storage: []T,
head: usize,
tail: usize,
// We could remove this and just use math with head/tail to figure
// it out, but our usage of circular buffers stores so much data that
// this minor overhead is not worth optimizing out.
full: bool,
/// Initialize a new circular buffer that can store size elements.
pub fn init(alloc: Allocator, size: usize) !Self {
var buf = try alloc.alloc(T, size);
std.mem.set(T, buf, default);
return Self{
.storage = buf,
.head = 0,
.tail = 0,
.full = false,
};
}
pub fn deinit(self: *Self, alloc: Allocator) void {
alloc.free(self.storage);
self.* = undefined;
}
/// Resize the buffer to the given size (larger or smaller).
/// If larger, new values will be set to the default value.
pub fn resize(self: *Self, alloc: Allocator, size: usize) !void {
// Rotate to zero so it is aligned.
try self.rotateToZero(alloc);
// Reallocate, this adds to the end so we're ready to go.
const prev_len = self.len();
const prev_cap = self.storage.len;
self.storage = try alloc.realloc(self.storage, size);
// If we grew, we need to set our new defaults. We can add it
// at the end since we rotated to start.
if (size > prev_cap) {
std.mem.set(T, self.storage[prev_cap..], default);
// Fix up our head/tail
if (self.full) {
self.head = prev_len;
self.full = false;
}
}
}
/// Rotate the data so that it is zero-aligned.
fn rotateToZero(self: *Self, alloc: Allocator) !void {
// TODO: this does this in the worst possible way by allocating.
// rewrite to not allocate, its possible, I'm just lazy right now.
// If we're already at zero then do nothing.
if (self.tail == 0) return;
var buf = try alloc.alloc(T, self.storage.len);
defer {
self.head = if (self.full) 0 else self.len();
self.tail = 0;
alloc.free(self.storage);
self.storage = buf;
}
if (!self.full and self.head >= self.tail) {
std.mem.copy(T, buf, self.storage[self.tail..self.head]);
return;
}
const middle = self.storage.len - self.tail;
std.mem.copy(T, buf, self.storage[self.tail..]);
std.mem.copy(T, buf[middle..], self.storage[0..self.head]);
}
/// Returns if the buffer is currently empty. To check if its
/// full, just check the "full" attribute.
pub fn empty(self: Self) bool {
return !self.full and self.head == self.tail;
}
/// Returns the total capacity allocated for this buffer.
pub fn capacity(self: Self) usize {
return self.storage.len;
}
/// Returns the length in elements that are used.
pub fn len(self: Self) usize {
if (self.full) return self.storage.len;
if (self.head >= self.tail) return self.head - self.tail;
return self.storage.len - (self.tail - self.head);
}
/// Delete the oldest n values from the buffer. If there are less
/// than n values in the buffer, it'll delete everything.
pub fn deleteOldest(self: *Self, n: usize) void {
assert(n <= self.storage.len);
// Clear the values back to default
const slices = self.getPtrSlice(0, n);
for (slices) |slice| std.mem.set(T, slice, default);
// If we're not full, we can just advance the tail. We know
// it'll be less than the length because otherwise we'd be full.
self.tail += @minimum(self.len(), n);
if (self.tail >= self.storage.len) self.tail -= self.storage.len;
self.full = false;
}
/// Returns a pointer to the value at offset with the given length,
/// and considers this full amount of data "written" if it is beyond
/// the end of our buffer. This never "rotates" the buffer because
/// the offset can only be within the size of the buffer.
pub fn getPtrSlice(self: *Self, offset: usize, slice_len: usize) [2][]T {
// Note: this assertion is very important, it hints the compiler
// which generates ~10% faster code than without it.
assert(offset + slice_len <= self.capacity());
// End offset is the last offset (exclusive) for our slice.
// We use exclusive because it makes the math easier and it
// matches Zigs slicing parameterization.
const end_offset = offset + slice_len;
// If our slice can't fit it in our length, then we need to advance.
if (end_offset > self.len()) self.advance(end_offset - self.len());
// Our start and end indexes into the storage buffer
const start_idx = self.storageOffset(offset);
const end_idx = self.storageOffset(end_offset - 1);
// std.log.warn("A={} B={}", .{ start_idx, end_idx });
// Optimistically, our data fits in one slice
if (end_idx >= start_idx) {
return .{
self.storage[start_idx .. end_idx + 1],
self.storage[0..0], // So there is an empty slice
};
}
return .{
self.storage[start_idx..],
self.storage[0 .. end_idx + 1],
};
}
/// Advances the head/tail so that we can store amount.
fn advance(self: *Self, amount: usize) void {
assert(amount <= self.storage.len - self.len());
// Optimistically add our amount
self.head += amount;
// If we exceeded the length of the buffer, wrap around.
if (self.head >= self.storage.len) self.head = self.head - self.storage.len;
// If we're full, we have to keep tail lined up.
if (self.full) self.tail = self.head;
// We're full if the head reached the tail. The head can never
// pass the tail because advance asserts amount is only in
// available space left
self.full = self.head == self.tail;
}
/// For a given offset from zero, this returns the offset in the
/// storage buffer where this data can be found.
fn storageOffset(self: Self, offset: usize) usize {
assert(offset < self.storage.len);
// This should be subtraction ideally to avoid overflows but
// it would take a really, really, huge buffer to overflow.
const fits_offset = self.tail + offset;
if (fits_offset < self.storage.len) return fits_offset;
return fits_offset - self.storage.len;
}
};
}
test {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 12);
defer buf.deinit(alloc);
try testing.expect(buf.empty());
try testing.expectEqual(@as(usize, 0), buf.len());
}
test "getPtrSlice fits" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 12);
defer buf.deinit(alloc);
const slices = buf.getPtrSlice(0, 11);
try testing.expectEqual(@as(usize, 11), slices[0].len);
try testing.expectEqual(@as(usize, 0), slices[1].len);
try testing.expectEqual(@as(usize, 11), buf.len());
}
test "getPtrSlice wraps" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 4);
defer buf.deinit(alloc);
// Fill the buffer
_ = buf.getPtrSlice(0, buf.capacity());
try testing.expect(buf.full);
try testing.expectEqual(@as(usize, 4), buf.len());
// Delete
buf.deleteOldest(2);
try testing.expect(!buf.full);
try testing.expectEqual(@as(usize, 2), buf.len());
// Get a slice that doesn't grow
{
const slices = buf.getPtrSlice(0, 2);
try testing.expectEqual(@as(usize, 2), slices[0].len);
try testing.expectEqual(@as(usize, 0), slices[1].len);
try testing.expectEqual(@as(usize, 2), buf.len());
slices[0][0] = 1;
slices[0][1] = 2;
}
// Get a slice that does grow, and forces wrap
{
const slices = buf.getPtrSlice(2, 2);
try testing.expectEqual(@as(usize, 2), slices[0].len);
try testing.expectEqual(@as(usize, 0), slices[1].len);
try testing.expectEqual(@as(usize, 4), buf.len());
// should be empty
try testing.expectEqual(@as(u8, 0), slices[0][0]);
try testing.expectEqual(@as(u8, 0), slices[0][1]);
slices[0][0] = 3;
slices[0][1] = 4;
}
// Get a slice across boundaries
{
const slices = buf.getPtrSlice(0, 4);
try testing.expectEqual(@as(usize, 2), slices[0].len);
try testing.expectEqual(@as(usize, 2), slices[1].len);
try testing.expectEqual(@as(usize, 4), buf.len());
try testing.expectEqual(@as(u8, 1), slices[0][0]);
try testing.expectEqual(@as(u8, 2), slices[0][1]);
try testing.expectEqual(@as(u8, 3), slices[1][0]);
try testing.expectEqual(@as(u8, 4), slices[1][1]);
}
}
test "rotateToZero" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 12);
defer buf.deinit(alloc);
_ = buf.getPtrSlice(0, 11);
try buf.rotateToZero(alloc);
}
test "rotateToZero offset" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 4);
defer buf.deinit(alloc);
// Fill the buffer
_ = buf.getPtrSlice(0, 3);
try testing.expectEqual(@as(usize, 3), buf.len());
// Delete
buf.deleteOldest(2);
try testing.expect(!buf.full);
try testing.expectEqual(@as(usize, 1), buf.len());
try testing.expect(buf.tail > 0 and buf.head >= buf.tail);
// Rotate to zero
try buf.rotateToZero(alloc);
try testing.expectEqual(@as(usize, 0), buf.tail);
try testing.expectEqual(@as(usize, 1), buf.head);
}
test "rotateToZero wraps" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 4);
defer buf.deinit(alloc);
// Fill the buffer
_ = buf.getPtrSlice(0, 3);
try testing.expectEqual(@as(usize, 3), buf.len());
try testing.expect(buf.tail == 0 and buf.head == 3);
// Delete all
buf.deleteOldest(3);
try testing.expectEqual(@as(usize, 0), buf.len());
try testing.expect(buf.tail == 3 and buf.head == 3);
// Refill to force a wrap
{
const slices = buf.getPtrSlice(0, 3);
slices[0][0] = 1;
slices[1][0] = 2;
slices[1][1] = 3;
try testing.expectEqual(@as(usize, 3), buf.len());
try testing.expect(buf.tail == 3 and buf.head == 2);
}
// Rotate to zero
try buf.rotateToZero(alloc);
try testing.expectEqual(@as(usize, 0), buf.tail);
try testing.expectEqual(@as(usize, 3), buf.head);
{
const slices = buf.getPtrSlice(0, 3);
try testing.expectEqual(@as(u8, 1), slices[0][0]);
try testing.expectEqual(@as(u8, 2), slices[0][1]);
try testing.expectEqual(@as(u8, 3), slices[0][2]);
}
}
test "rotateToZero full no wrap" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 4);
defer buf.deinit(alloc);
// Fill the buffer
_ = buf.getPtrSlice(0, 3);
// Delete all
buf.deleteOldest(3);
// Refill to force a wrap
{
const slices = buf.getPtrSlice(0, 4);
try testing.expect(buf.full);
slices[0][0] = 1;
slices[1][0] = 2;
slices[1][1] = 3;
slices[1][2] = 4;
}
// Rotate to zero
try buf.rotateToZero(alloc);
try testing.expect(buf.full);
try testing.expectEqual(@as(usize, 0), buf.tail);
try testing.expectEqual(@as(usize, 0), buf.head);
{
const slices = buf.getPtrSlice(0, 4);
try testing.expectEqual(@as(u8, 1), slices[0][0]);
try testing.expectEqual(@as(u8, 2), slices[0][1]);
try testing.expectEqual(@as(u8, 3), slices[0][2]);
try testing.expectEqual(@as(u8, 4), slices[0][3]);
}
}
test "resize grow" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 4);
defer buf.deinit(alloc);
// Fill and write
{
const slices = buf.getPtrSlice(0, 4);
try testing.expect(buf.full);
slices[0][0] = 1;
slices[0][1] = 2;
slices[0][2] = 3;
slices[0][3] = 4;
}
// Resize
try buf.resize(alloc, 6);
try testing.expect(!buf.full);
try testing.expectEqual(@as(usize, 4), buf.len());
try testing.expectEqual(@as(usize, 6), buf.capacity());
{
const slices = buf.getPtrSlice(0, 4);
try testing.expectEqual(@as(u8, 1), slices[0][0]);
try testing.expectEqual(@as(u8, 2), slices[0][1]);
try testing.expectEqual(@as(u8, 3), slices[0][2]);
try testing.expectEqual(@as(u8, 4), slices[0][3]);
}
}
test "resize shrink" {
const testing = std.testing;
const alloc = testing.allocator;
const Buf = CircBuf(u8, 0);
var buf = try Buf.init(alloc, 4);
defer buf.deinit(alloc);
// Fill and write
{
const slices = buf.getPtrSlice(0, 4);
try testing.expect(buf.full);
slices[0][0] = 1;
slices[0][1] = 2;
slices[0][2] = 3;
slices[0][3] = 4;
}
// Resize
try buf.resize(alloc, 3);
try testing.expect(buf.full);
try testing.expectEqual(@as(usize, 3), buf.len());
try testing.expectEqual(@as(usize, 3), buf.capacity());
{
const slices = buf.getPtrSlice(0, 3);
try testing.expectEqual(@as(u8, 1), slices[0][0]);
try testing.expectEqual(@as(u8, 2), slices[0][1]);
try testing.expectEqual(@as(u8, 3), slices[0][2]);
}
}

View File

@ -7,6 +7,7 @@ const csi = @import("csi.zig");
const sgr = @import("sgr.zig"); const sgr = @import("sgr.zig");
pub const point = @import("point.zig"); pub const point = @import("point.zig");
pub const color = @import("color.zig"); pub const color = @import("color.zig");
pub const parse_table = @import("parse_table.zig");
pub const Charset = charsets.Charset; pub const Charset = charsets.Charset;
pub const CharsetSlot = charsets.Slots; pub const CharsetSlot = charsets.Slots;
@ -28,19 +29,5 @@ pub const TabClear = csi.TabClear;
pub const Attribute = sgr.Attribute; pub const Attribute = sgr.Attribute;
test { test {
_ = ansi; @import("std").testing.refAllDecls(@This());
_ = charsets;
_ = color;
_ = csi;
_ = point;
_ = sgr;
_ = stream;
_ = Parser;
_ = Selection;
_ = Terminal;
_ = Screen;
_ = @import("osc.zig");
_ = @import("parse_table.zig");
_ = @import("Tabstops.zig");
} }

View File

@ -16,7 +16,7 @@ pub const Viewport = struct {
// get the full offset from the top. // get the full offset from the top.
return .{ return .{
.x = self.x, .x = self.x,
.y = screen.visible_offset + self.y, .y = screen.viewport + self.y,
}; };
} }
@ -25,7 +25,7 @@ pub const Viewport = struct {
const alloc = testing.allocator; const alloc = testing.allocator;
var s = try Screen.init(alloc, 3, 5, 0); var s = try Screen.init(alloc, 3, 5, 0);
defer s.deinit(alloc); defer s.deinit();
try testing.expectEqual(ScreenPoint{ try testing.expectEqual(ScreenPoint{
.x = 1, .x = 1,
@ -38,24 +38,24 @@ pub const Viewport = struct {
const alloc = testing.allocator; const alloc = testing.allocator;
var s = try Screen.init(alloc, 3, 5, 3); var s = try Screen.init(alloc, 3, 5, 3);
defer s.deinit(alloc); defer s.deinit();
// At the bottom // At the bottom
s.scroll(.{ .delta = 6 }); try s.scroll(.{ .delta = 6 });
try testing.expectEqual(ScreenPoint{ try testing.expectEqual(ScreenPoint{
.x = 0, .x = 0,
.y = 3, .y = 3,
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
// Move the viewport a bit up // Move the viewport a bit up
s.scroll(.{ .delta = -1 }); try s.scroll(.{ .delta = -1 });
try testing.expectEqual(ScreenPoint{ try testing.expectEqual(ScreenPoint{
.x = 0, .x = 0,
.y = 2, .y = 2,
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s)); }, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
// Move the viewport to top // Move the viewport to top
s.scroll(.{ .top = {} }); try s.scroll(.{ .top = {} });
try testing.expectEqual(ScreenPoint{ try testing.expectEqual(ScreenPoint{
.x = 0, .x = 0,
.y = 0, .y = 0,
@ -83,15 +83,15 @@ pub const ScreenPoint = struct {
// TODO: test // TODO: test
// Before viewport // Before viewport
if (self.y < screen.visible_offset) return .{ .x = 0, .y = 0 }; if (self.y < screen.viewport) return .{ .x = 0, .y = 0 };
// After viewport // After viewport
if (self.y > screen.visible_offset + screen.rows) return .{ if (self.y > screen.viewport + screen.rows) return .{
.x = screen.cols - 1, .x = screen.cols - 1,
.y = screen.rows - 1, .y = screen.rows - 1,
}; };
return .{ .x = self.x, .y = self.y - screen.visible_offset }; return .{ .x = self.x, .y = self.y - screen.viewport };
} }
test "before" { test "before" {

View File

@ -68,7 +68,7 @@ pub fn Stream(comptime Handler: type) type {
} }
} }
fn execute(self: *Self, c: u8) !void { pub fn execute(self: *Self, c: u8) !void {
// log.warn("C0: {}", .{c}); // log.warn("C0: {}", .{c});
switch (@intToEnum(ansi.C0, c)) { switch (@intToEnum(ansi.C0, c)) {
.NUL => {}, .NUL => {},