mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
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:
@ -15,6 +15,9 @@ pub usingnamespace if (enabled) Impl else Noop;
|
||||
|
||||
const Impl = struct {
|
||||
const c = @cImport({
|
||||
//uncomment to enable callstacks, very slow
|
||||
//@cDefine("TRACY_CALLSTACK", "");
|
||||
|
||||
@cDefine("TRACY_ENABLE", "");
|
||||
@cInclude("TracyC.h");
|
||||
});
|
||||
|
25
src/Grid.zig
25
src/Grid.zig
@ -322,7 +322,7 @@ pub fn deinit(self: *Grid) void {
|
||||
///
|
||||
/// Note this doesn't have to typically be manually called. Internally,
|
||||
/// 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());
|
||||
defer t.end();
|
||||
|
||||
@ -344,9 +344,13 @@ pub fn rebuildCells(self: *Grid, term: Terminal) !void {
|
||||
// Build each cell
|
||||
var rowIter = term.screen.rowIterator(.viewport);
|
||||
var y: usize = 0;
|
||||
while (rowIter.next()) |line| {
|
||||
while (rowIter.next()) |row| {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
/// for render. This performs tasks such as preparing the cursor, refreshing
|
||||
/// the cells if necessary, etc.
|
||||
pub fn finalizeCells(self: *Grid, term: Terminal) !void {
|
||||
pub fn finalizeCells(self: *Grid, term: *Terminal) !void {
|
||||
// Add the cursor
|
||||
// TODO: only add cursor if it changed
|
||||
if (self.cells.items.len < self.cells.capacity)
|
||||
@ -375,10 +379,11 @@ pub fn finalizeCells(self: *Grid, term: Terminal) !void {
|
||||
try self.flushAtlas();
|
||||
}
|
||||
|
||||
fn addCursor(self: *Grid, term: Terminal) void {
|
||||
fn addCursor(self: *Grid, term: *Terminal) void {
|
||||
// Add the cursor
|
||||
if (self.cursor_visible and term.screen.viewportIsBottom()) {
|
||||
const cell = term.screen.getCell(
|
||||
.active,
|
||||
term.screen.cursor.y,
|
||||
term.screen.cursor.x,
|
||||
);
|
||||
@ -410,7 +415,7 @@ fn addCursor(self: *Grid, term: Terminal) void {
|
||||
/// needed.
|
||||
pub fn updateCell(
|
||||
self: *Grid,
|
||||
term: Terminal,
|
||||
term: *Terminal,
|
||||
cell: terminal.Screen.Cell,
|
||||
x: usize,
|
||||
y: usize,
|
||||
@ -454,14 +459,14 @@ pub fn updateCell(
|
||||
const res: BgFg = if (!cell.attrs.inverse) .{
|
||||
// In normal mode, background and fg match the cell. We
|
||||
// un-optionalize the fg by defaulting to our fg color.
|
||||
.bg = cell.bg,
|
||||
.fg = cell.fg orelse self.foreground,
|
||||
.bg = if (cell.attrs.has_bg) cell.bg else null,
|
||||
.fg = if (cell.attrs.has_fg) cell.fg else self.foreground,
|
||||
} else .{
|
||||
// In inverted mode, the background MUST be set to something
|
||||
// (is never null) so it is either the fg or default fg. The
|
||||
// fg is either the bg or default background.
|
||||
.bg = cell.fg orelse self.foreground,
|
||||
.fg = cell.bg orelse self.background,
|
||||
.bg = if (cell.attrs.has_fg) cell.fg else self.foreground,
|
||||
.fg = if (cell.attrs.has_bg) cell.bg else self.background,
|
||||
};
|
||||
break :colors res;
|
||||
};
|
||||
|
@ -33,6 +33,7 @@ const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
||||
|
||||
/// Allocator
|
||||
alloc: Allocator,
|
||||
alloc_io_arena: std.heap.ArenaAllocator,
|
||||
|
||||
/// The glfw window handle.
|
||||
window: glfw.Window,
|
||||
@ -323,8 +324,15 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
||||
errdefer cursor.destroy();
|
||||
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.* = .{
|
||||
.alloc = alloc,
|
||||
.alloc_io_arena = io_arena,
|
||||
.window = window,
|
||||
.cursor = cursor,
|
||||
.focused = false,
|
||||
@ -410,6 +418,8 @@ pub fn destroy(self: *Window) void {
|
||||
// We can destroy the cursor right away. glfw will just revert any
|
||||
// windows using it to the default.
|
||||
self.cursor.destroy();
|
||||
|
||||
self.alloc_io_arena.deinit();
|
||||
}
|
||||
|
||||
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
|
||||
// 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|
|
||||
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 delta: isize = sign * @maximum(@divFloor(win.grid.size.rows, 15), 1);
|
||||
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.
|
||||
// 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());
|
||||
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;
|
||||
}
|
||||
|
||||
@ -1243,7 +1256,10 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
|
||||
defer tracy.end();
|
||||
|
||||
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: {any}", .{buf[0..@intCast(usize, n)]});
|
||||
@ -1269,10 +1285,45 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
|
||||
// Schedule a render
|
||||
win.render_timer.schedule() catch unreachable;
|
||||
|
||||
// Process the terminal data
|
||||
win.terminal_stream.nextSlice(buf[0..@intCast(usize, n)]) catch |err|
|
||||
// Process the terminal data. This is an extremely hot part of the
|
||||
// terminal emulator, so we do some abstraction leakage to avoid
|
||||
// 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 {
|
||||
const tracy = trace(@src());
|
||||
@ -1338,11 +1389,11 @@ fn renderTimerCallback(t: *libuv.Timer) void {
|
||||
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
// 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});
|
||||
|
||||
// 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});
|
||||
|
||||
// Render the grid
|
||||
@ -1382,7 +1433,9 @@ pub fn horizontalTab(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 {
|
||||
@ -1426,7 +1479,7 @@ pub fn setCursorPos(self: *Window, row: u16, col: u16) !void {
|
||||
pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void {
|
||||
if (mode == .complete) {
|
||||
// Whenever we erase the full display, scroll to bottom.
|
||||
self.terminal.scrollViewport(.{ .bottom = {} });
|
||||
try self.terminal.scrollViewport(.{ .bottom = {} });
|
||||
try self.render_timer.schedule();
|
||||
}
|
||||
|
||||
@ -1462,12 +1515,12 @@ pub fn reverseIndex(self: *Window) !void {
|
||||
}
|
||||
|
||||
pub fn index(self: *Window) !void {
|
||||
self.terminal.index();
|
||||
try self.terminal.index();
|
||||
}
|
||||
|
||||
pub fn nextLine(self: *Window) !void {
|
||||
self.terminal.carriageReturn();
|
||||
self.terminal.index();
|
||||
try self.terminal.index();
|
||||
}
|
||||
|
||||
pub fn setTopAndBottomMargin(self: *Window, top: u16, bot: u16) !void {
|
||||
|
@ -74,7 +74,7 @@ pub const RunIterator = struct {
|
||||
i: usize = 0,
|
||||
|
||||
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
|
||||
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.
|
||||
var j: usize = self.i;
|
||||
while (j < self.row.len) : (j += 1) {
|
||||
const cell = self.row[j];
|
||||
while (j < self.row.lenCells()) : (j += 1) {
|
||||
const cell = self.row.getCell(j);
|
||||
|
||||
// Ignore tailing wide spacers, this will get fixed up by the shaper
|
||||
if (cell.empty() or cell.attrs.wide_spacer_tail) continue;
|
||||
@ -129,8 +129,8 @@ test "run iterator" {
|
||||
{
|
||||
// Make a screen with some data
|
||||
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||
defer screen.deinit(alloc);
|
||||
screen.testWriteString("ABCD");
|
||||
defer screen.deinit();
|
||||
try screen.testWriteString("ABCD");
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = testdata.shaper;
|
||||
@ -143,8 +143,8 @@ test "run iterator" {
|
||||
{
|
||||
// Make a screen with some data
|
||||
var screen = try terminal.Screen.init(alloc, 3, 5, 0);
|
||||
defer screen.deinit(alloc);
|
||||
screen.testWriteString("A😃D");
|
||||
defer screen.deinit();
|
||||
try screen.testWriteString("A😃D");
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = testdata.shaper;
|
||||
@ -175,8 +175,8 @@ test "shape" {
|
||||
|
||||
// Make a screen with some data
|
||||
var screen = try terminal.Screen.init(alloc, 3, 10, 0);
|
||||
defer screen.deinit(alloc);
|
||||
screen.testWriteString(buf[0..buf_idx]);
|
||||
defer screen.deinit();
|
||||
try screen.testWriteString(buf[0..buf_idx]);
|
||||
|
||||
// Get our run iterator
|
||||
var shaper = testdata.shaper;
|
||||
|
@ -6,6 +6,7 @@ const Parser = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const trace = @import("tracy").trace;
|
||||
const testing = std.testing;
|
||||
const table = @import("parse_table.zig").table;
|
||||
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
|
||||
/// the state exit, transition, and entry actions.
|
||||
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 (self.state == .utf8) {
|
||||
return .{ self.next_utf8(c), null, null };
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -149,8 +149,8 @@ pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal {
|
||||
|
||||
pub fn deinit(self: *Terminal, alloc: Allocator) void {
|
||||
self.tabstops.deinit(alloc);
|
||||
self.screen.deinit(alloc);
|
||||
self.secondary_screen.deinit(alloc);
|
||||
self.screen.deinit();
|
||||
self.secondary_screen.deinit();
|
||||
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 (self.active_screen == .primary) {
|
||||
try self.screen.resize(alloc, rows, cols);
|
||||
try self.secondary_screen.resizeWithoutReflow(alloc, rows, cols);
|
||||
try self.screen.resize(rows, cols);
|
||||
try self.secondary_screen.resizeWithoutReflow(rows, cols);
|
||||
} else {
|
||||
try self.screen.resizeWithoutReflow(alloc, rows, cols);
|
||||
try self.secondary_screen.resize(alloc, rows, cols);
|
||||
try self.screen.resizeWithoutReflow(rows, cols);
|
||||
try self.secondary_screen.resize(rows, cols);
|
||||
}
|
||||
|
||||
// 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.
|
||||
///
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@ -336,8 +336,8 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
|
||||
|
||||
switch (attr) {
|
||||
.unset => {
|
||||
self.screen.cursor.pen.fg = null;
|
||||
self.screen.cursor.pen.bg = null;
|
||||
self.screen.cursor.pen.attrs.has_fg = false;
|
||||
self.screen.cursor.pen.attrs.has_bg = false;
|
||||
self.screen.cursor.pen.attrs = .{};
|
||||
},
|
||||
|
||||
@ -362,6 +362,7 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
|
||||
},
|
||||
|
||||
.direct_color_fg => |rgb| {
|
||||
self.screen.cursor.pen.attrs.has_fg = true;
|
||||
self.screen.cursor.pen.fg = .{
|
||||
.r = rgb.r,
|
||||
.g = rgb.g,
|
||||
@ -370,6 +371,7 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
|
||||
},
|
||||
|
||||
.direct_color_bg => |rgb| {
|
||||
self.screen.cursor.pen.attrs.has_bg = true;
|
||||
self.screen.cursor.pen.bg = .{
|
||||
.r = rgb.r,
|
||||
.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,
|
||||
}
|
||||
@ -440,11 +460,11 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
|
||||
// If we're soft-wrapping, then handle that first.
|
||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap)
|
||||
_ = self.printWrap();
|
||||
try self.printWrap();
|
||||
|
||||
switch (width) {
|
||||
// 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
|
||||
// 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) {
|
||||
const spacer_head = self.printCell(' ');
|
||||
spacer_head.attrs.wide_spacer_head = true;
|
||||
_ = self.printWrap();
|
||||
try self.printWrap();
|
||||
}
|
||||
|
||||
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 {
|
||||
// const tracy = trace(@src());
|
||||
// defer tracy.end();
|
||||
|
||||
const c = c: {
|
||||
// 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)]);
|
||||
};
|
||||
|
||||
const cell = self.screen.getCell(
|
||||
self.screen.cursor.y,
|
||||
self.screen.cursor.x,
|
||||
);
|
||||
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
||||
const cell = row.getCellPtr(self.screen.cursor.x);
|
||||
|
||||
// If this cell is wide char then we need to clear it.
|
||||
// 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;
|
||||
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;
|
||||
|
||||
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);
|
||||
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;
|
||||
|
||||
if (self.screen.cursor.x <= 1) {
|
||||
@ -539,26 +560,23 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell {
|
||||
return cell;
|
||||
}
|
||||
|
||||
fn printWrap(self: *Terminal) *Screen.Cell {
|
||||
// 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.screen.cursor.y,
|
||||
self.screen.cursor.x,
|
||||
);
|
||||
cell.attrs.wrap = true;
|
||||
fn printWrap(self: *Terminal) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
||||
row.setWrapped(true);
|
||||
|
||||
// Move to the next line
|
||||
self.index();
|
||||
try self.index();
|
||||
self.screen.cursor.x = 0;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
fn clearWideSpacerHead(self: *Terminal) void {
|
||||
// TODO: handle deleting wide char on row 0 of active
|
||||
assert(self.screen.cursor.y >= 1);
|
||||
const cell = self.screen.getCell(
|
||||
const cell = self.screen.getCellPtr(
|
||||
.active,
|
||||
self.screen.cursor.y - 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
|
||||
// optimize here by doing row copies.
|
||||
const filled = self.screen.getRow(.{ .active = 0 });
|
||||
var col: usize = 0;
|
||||
while (col < self.cols) : (col += 1) {
|
||||
filled[col] = .{ .char = 'E' };
|
||||
}
|
||||
filled.fill(.{ .char = 'E' });
|
||||
|
||||
var row: usize = 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
|
||||
///
|
||||
/// This unsets the pending wrap state without wrapping.
|
||||
pub fn index(self: *Terminal) void {
|
||||
pub fn index(self: *Terminal) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
@ -625,7 +640,7 @@ pub fn index(self: *Terminal) void {
|
||||
if (self.scrolling_region.top == 0 and
|
||||
self.scrolling_region.bottom == self.rows - 1)
|
||||
{
|
||||
self.screen.scroll(.{ .delta = 1 });
|
||||
try self.screen.scroll(.{ .delta = 1 });
|
||||
} else {
|
||||
// TODO: test
|
||||
self.scrollUp(1);
|
||||
@ -736,9 +751,8 @@ pub fn eraseDisplay(
|
||||
|
||||
switch (mode) {
|
||||
.complete => {
|
||||
const region = self.screen.region(.active);
|
||||
std.mem.set(Screen.Cell, region[0], self.screen.cursor.pen);
|
||||
std.mem.set(Screen.Cell, region[1], self.screen.cursor.pen);
|
||||
var it = self.screen.rowIterator(.active);
|
||||
while (it.next()) |row| row.clear(self.screen.cursor.pen);
|
||||
|
||||
// Unsets pending wrap state
|
||||
self.screen.cursor.pending_wrap = false;
|
||||
@ -748,7 +762,7 @@ pub fn eraseDisplay(
|
||||
// All lines to the right (including the cursor)
|
||||
var x: usize = self.screen.cursor.x;
|
||||
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.char = 0;
|
||||
}
|
||||
@ -758,7 +772,7 @@ pub fn eraseDisplay(
|
||||
while (y < self.rows) : (y += 1) {
|
||||
x = 0;
|
||||
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.char = 0;
|
||||
}
|
||||
@ -772,7 +786,7 @@ pub fn eraseDisplay(
|
||||
// Erase to the left (including the cursor)
|
||||
var x: usize = 0;
|
||||
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.char = 0;
|
||||
}
|
||||
@ -782,7 +796,7 @@ pub fn eraseDisplay(
|
||||
while (y < self.screen.cursor.y) : (y += 1) {
|
||||
x = 0;
|
||||
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.char = 0;
|
||||
}
|
||||
@ -792,16 +806,7 @@ pub fn eraseDisplay(
|
||||
self.screen.cursor.pending_wrap = false;
|
||||
},
|
||||
|
||||
.scrollback => {
|
||||
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;
|
||||
},
|
||||
.scrollback => self.screen.clearHistory(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -817,12 +822,12 @@ pub fn eraseLine(
|
||||
switch (mode) {
|
||||
.right => {
|
||||
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 => {
|
||||
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
|
||||
self.screen.cursor.pending_wrap = false;
|
||||
@ -830,7 +835,7 @@ pub fn eraseLine(
|
||||
|
||||
.complete => {
|
||||
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 => {
|
||||
@ -864,8 +869,9 @@ pub fn deleteChars(self: *Terminal, count: usize) !void {
|
||||
var i: usize = self.screen.cursor.x;
|
||||
while (i < end) : (i += 1) {
|
||||
const j = i + count;
|
||||
line[i] = line[j];
|
||||
line[j].char = 0;
|
||||
const j_cell = line.getCellPtr(j);
|
||||
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);
|
||||
|
||||
// Shift
|
||||
var x: usize = self.screen.cursor.x;
|
||||
while (x < end) : (x += 1) {
|
||||
const cell = self.getOrPutCell(x, self.screen.cursor.y);
|
||||
cell.* = self.screen.cursor.pen;
|
||||
cell.char = 0;
|
||||
}
|
||||
var pen = self.screen.cursor.pen;
|
||||
pen.char = 0;
|
||||
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
||||
row.fillSlice(pen, self.screen.cursor.x, end);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn linefeed(self: *Terminal) void {
|
||||
pub fn linefeed(self: *Terminal) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
self.index();
|
||||
try self.index();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
// If count is bigger than the available space left after the cursor,
|
||||
// we may have no space at all for copying.
|
||||
const copyable = row.len - pivot;
|
||||
const copyable = self.screen.cols - pivot;
|
||||
if (copyable > 0) {
|
||||
// This is the index of the final copyable value that we need to copy.
|
||||
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.
|
||||
var i: usize = 0;
|
||||
while (i < copyable) : (i += 1) {
|
||||
const to = row.len - 1 - i;
|
||||
const to = self.screen.cols - 1 - i;
|
||||
const from = copyable_end - i;
|
||||
row[to] = row[from];
|
||||
row.getCellPtr(to).* = row.getCell(from);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert zero
|
||||
var pen = self.screen.cursor.pen;
|
||||
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
|
||||
@ -1091,18 +1095,14 @@ pub fn insertLines(self: *Terminal, count: usize) void {
|
||||
|
||||
// Ensure we have the lines populated to the end
|
||||
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
|
||||
y = self.screen.cursor.y;
|
||||
while (y < self.screen.cursor.y + adjusted_count) : (y += 1) {
|
||||
var x: usize = 0;
|
||||
while (x < self.cols) : (x += 1) {
|
||||
const cell = self.getOrPutCell(x, y);
|
||||
cell.* = self.screen.cursor.pen;
|
||||
cell.char = 0;
|
||||
}
|
||||
const row = self.screen.getRow(.{ .active = y });
|
||||
row.clear(self.screen.cursor.pen);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1140,12 +1140,12 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
|
||||
// Scroll up the count amount.
|
||||
var y: usize = self.screen.cursor.y;
|
||||
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) {
|
||||
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.
|
||||
pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void {
|
||||
pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
self.screen.scroll(switch (behavior) {
|
||||
try self.screen.scroll(switch (behavior) {
|
||||
.top => .{ .top = {} },
|
||||
.bottom => .{ .bottom = {} },
|
||||
.delta => |delta| .{ .delta_no_grow = delta },
|
||||
@ -1236,13 +1236,6 @@ pub fn setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void {
|
||||
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);
|
||||
@ -1282,9 +1275,9 @@ test "Terminal: print writes to bottom if scrolled" {
|
||||
|
||||
// Make newlines so we create scrollback
|
||||
// 3 pushes hello off the screen
|
||||
t.index();
|
||||
t.index();
|
||||
t.index();
|
||||
try t.index();
|
||||
try t.index();
|
||||
try t.index();
|
||||
{
|
||||
var str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
@ -1292,7 +1285,7 @@ test "Terminal: print writes to bottom if scrolled" {
|
||||
}
|
||||
|
||||
// Scroll to the top
|
||||
t.scrollViewport(.{ .top = {} });
|
||||
try t.scrollViewport(.{ .top = {} });
|
||||
{
|
||||
var str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
@ -1301,7 +1294,7 @@ test "Terminal: print writes to bottom if scrolled" {
|
||||
|
||||
// Type
|
||||
try t.print('A');
|
||||
t.scrollViewport(.{ .bottom = {} });
|
||||
try t.scrollViewport(.{ .bottom = {} });
|
||||
{
|
||||
var str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
@ -1378,7 +1371,7 @@ test "Terminal: linefeed and carriage return" {
|
||||
// Basic grid writing
|
||||
for ("hello") |c| try t.print(c);
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
for ("world") |c| try t.print(c);
|
||||
try testing.expectEqual(@as(usize, 1), t.screen.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 5), t.screen.cursor.x);
|
||||
@ -1396,7 +1389,7 @@ test "Terminal: linefeed unsets pending wrap" {
|
||||
// Basic grid writing
|
||||
for ("hello") |c| try t.print(c);
|
||||
try testing.expect(t.screen.cursor.pending_wrap == true);
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try testing.expect(t.screen.cursor.pending_wrap == false);
|
||||
}
|
||||
|
||||
@ -1540,13 +1533,13 @@ test "Terminal: deleteLines" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('C');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('D');
|
||||
|
||||
t.cursorUp(2);
|
||||
@ -1554,7 +1547,7 @@ test "Terminal: deleteLines" {
|
||||
|
||||
try t.print('E');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
|
||||
// We should be
|
||||
try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
|
||||
@ -1575,13 +1568,13 @@ test "Terminal: deleteLines with scroll region" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('C');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('D');
|
||||
|
||||
t.setScrollingRegion(1, 3);
|
||||
@ -1590,7 +1583,7 @@ test "Terminal: deleteLines with scroll region" {
|
||||
|
||||
try t.print('E');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
|
||||
// We should be
|
||||
// try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);
|
||||
@ -1611,16 +1604,16 @@ test "Terminal: insertLines" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('C');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('D');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('E');
|
||||
|
||||
// Move to row 2
|
||||
@ -1644,16 +1637,16 @@ test "Terminal: insertLines with scroll region" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('C');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('D');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('E');
|
||||
|
||||
t.setScrollingRegion(1, 2);
|
||||
@ -1677,16 +1670,16 @@ test "Terminal: insertLines more than remaining" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('C');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('D');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('E');
|
||||
|
||||
// Move to row 2
|
||||
@ -1710,17 +1703,17 @@ test "Terminal: reverseIndex" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('C');
|
||||
try t.reverseIndex();
|
||||
try t.print('D');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
|
||||
{
|
||||
var str = try t.plainString(testing.allocator);
|
||||
@ -1736,24 +1729,24 @@ test "Terminal: reverseIndex from the top" {
|
||||
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
|
||||
t.setCursorPos(1, 1);
|
||||
try t.reverseIndex();
|
||||
try t.print('D');
|
||||
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
t.setCursorPos(1, 1);
|
||||
try t.reverseIndex();
|
||||
try t.print('E');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
|
||||
{
|
||||
var str = try t.plainString(testing.allocator);
|
||||
@ -1767,7 +1760,7 @@ test "Terminal: index" {
|
||||
var t = try init(alloc, 2, 5);
|
||||
defer t.deinit(alloc);
|
||||
|
||||
t.index();
|
||||
try t.index();
|
||||
try t.print('A');
|
||||
|
||||
{
|
||||
@ -1784,7 +1777,7 @@ test "Terminal: index from the bottom" {
|
||||
|
||||
t.setCursorPos(5, 1);
|
||||
try t.print('A');
|
||||
t.index();
|
||||
try t.index();
|
||||
|
||||
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);
|
||||
t.setScrollingRegion(2, 5);
|
||||
t.index();
|
||||
try t.index();
|
||||
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.setCursorPos(5, 1);
|
||||
try t.print('A');
|
||||
t.index();
|
||||
try t.index();
|
||||
try t.print('B');
|
||||
|
||||
{
|
||||
@ -1832,7 +1825,7 @@ test "Terminal: DECALN" {
|
||||
// Initial value
|
||||
try t.print('A');
|
||||
t.carriageReturn();
|
||||
t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.print('B');
|
||||
t.decaln();
|
||||
|
||||
|
456
src/terminal/circ_buf.zig
Normal file
456
src/terminal/circ_buf.zig
Normal 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]);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ const csi = @import("csi.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
pub const point = @import("point.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const parse_table = @import("parse_table.zig");
|
||||
|
||||
pub const Charset = charsets.Charset;
|
||||
pub const CharsetSlot = charsets.Slots;
|
||||
@ -28,19 +29,5 @@ pub const TabClear = csi.TabClear;
|
||||
pub const Attribute = sgr.Attribute;
|
||||
|
||||
test {
|
||||
_ = ansi;
|
||||
_ = charsets;
|
||||
_ = color;
|
||||
_ = csi;
|
||||
_ = point;
|
||||
_ = sgr;
|
||||
_ = stream;
|
||||
_ = Parser;
|
||||
_ = Selection;
|
||||
_ = Terminal;
|
||||
_ = Screen;
|
||||
|
||||
_ = @import("osc.zig");
|
||||
_ = @import("parse_table.zig");
|
||||
_ = @import("Tabstops.zig");
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ pub const Viewport = struct {
|
||||
// get the full offset from the top.
|
||||
return .{
|
||||
.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;
|
||||
|
||||
var s = try Screen.init(alloc, 3, 5, 0);
|
||||
defer s.deinit(alloc);
|
||||
defer s.deinit();
|
||||
|
||||
try testing.expectEqual(ScreenPoint{
|
||||
.x = 1,
|
||||
@ -38,24 +38,24 @@ pub const Viewport = struct {
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try Screen.init(alloc, 3, 5, 3);
|
||||
defer s.deinit(alloc);
|
||||
defer s.deinit();
|
||||
|
||||
// At the bottom
|
||||
s.scroll(.{ .delta = 6 });
|
||||
try s.scroll(.{ .delta = 6 });
|
||||
try testing.expectEqual(ScreenPoint{
|
||||
.x = 0,
|
||||
.y = 3,
|
||||
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
|
||||
|
||||
// Move the viewport a bit up
|
||||
s.scroll(.{ .delta = -1 });
|
||||
try s.scroll(.{ .delta = -1 });
|
||||
try testing.expectEqual(ScreenPoint{
|
||||
.x = 0,
|
||||
.y = 2,
|
||||
}, (Viewport{ .x = 0, .y = 0 }).toScreen(&s));
|
||||
|
||||
// Move the viewport to top
|
||||
s.scroll(.{ .top = {} });
|
||||
try s.scroll(.{ .top = {} });
|
||||
try testing.expectEqual(ScreenPoint{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
@ -83,15 +83,15 @@ pub const ScreenPoint = struct {
|
||||
// TODO: test
|
||||
|
||||
// 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
|
||||
if (self.y > screen.visible_offset + screen.rows) return .{
|
||||
if (self.y > screen.viewport + screen.rows) return .{
|
||||
.x = screen.cols - 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" {
|
||||
|
@ -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});
|
||||
switch (@intToEnum(ansi.C0, c)) {
|
||||
.NUL => {},
|
||||
|
Reference in New Issue
Block a user