terminal/new: lots of code thrown at the wall

This commit is contained in:
Mitchell Hashimoto
2024-02-19 19:50:42 -08:00
parent 1473b3edf2
commit b5d7b0a87a
5 changed files with 294 additions and 96 deletions

View File

@ -55,6 +55,7 @@ test {
_ = @import("new/page.zig");
_ = @import("new/PageList.zig");
_ = @import("new/Screen.zig");
_ = @import("new/point.zig");
_ = @import("new/size.zig");
_ = @import("new/style.zig");
}

View File

@ -6,6 +6,7 @@ const PageList = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const point = @import("point.zig");
const pagepkg = @import("page.zig");
const Page = pagepkg.Page;
@ -41,10 +42,15 @@ pool: Pool,
/// The list of pages in the screen.
pages: List,
/// The page that contains the top of the current viewport and the row
/// within that page that is the top of the viewport (0-indexed).
viewport: *List.Node,
viewport_row: usize,
/// The top-left of certain parts of the screen that are frequently
/// accessed so we don't have to traverse the linked list to find them.
///
/// For other tags, don't need this:
/// - screen: pages.first
/// - history: active row minus one
///
viewport: RowOffset,
active: RowOffset,
/// The current desired screen dimensions. I say "desired" because individual
/// pages may still be a different size and not yet reflowed since we lazily
@ -87,8 +93,8 @@ pub fn init(
.rows = rows,
.pool = pool,
.pages = page_list,
.viewport = page,
.viewport_row = 0,
.viewport = .{ .page = page },
.active = .{ .page = page },
};
}
@ -99,6 +105,125 @@ pub fn deinit(self: *PageList) void {
self.pool.deinit();
}
/// Get the top-left of the screen for the given tag.
pub fn rowOffset(self: *const PageList, pt: point.Point) RowOffset {
// TODO: assert the point is valid
// This should never return null because we assert the point is valid.
return (switch (pt) {
.active => |v| self.active.forward(v.y),
.viewport => |v| self.viewport.forward(v.y),
.screen, .history => |v| offset: {
const tl: RowOffset = .{ .page = self.pages.first.? };
break :offset tl.forward(v.y);
},
}).?;
}
/// Get the cell at the given point, or null if the cell does not
/// exist or is out of bounds.
pub fn getCell(self: *const PageList, pt: point.Point) ?Cell {
const row = self.getTopLeft(pt).forward(pt.y) orelse return null;
const rac = row.page.data.getRowAndCell(row.row_offset, pt.x);
return .{
.page = row.page,
.row = rac.row,
.cell = rac.cell,
.row_idx = row.row_offset,
.col_idx = pt.x,
};
}
pub const RowIterator = struct {
row: ?RowOffset = null,
limit: ?usize = null,
pub fn next(self: *RowIterator) ?RowOffset {
const row = self.row orelse return null;
self.row = row.forward(1);
if (self.limit) |*limit| {
limit.* -= 1;
if (limit.* == 0) self.row = null;
}
return row;
}
};
/// Create an interator that can be used to iterate all the rows in
/// a region of the screen from the given top-left. The tag of the
/// top-left point will also determine the end of the iteration,
/// so convert from one reference point to another to change the
/// iteration bounds.
pub fn rowIterator(
self: *const PageList,
tl_pt: point.Point,
) RowIterator {
const tl = self.getTopLeft(tl_pt);
// TODO: limits
return .{ .row = tl.forward(tl_pt.coord().y) };
}
/// Get the top-left of the screen for the given tag.
fn getTopLeft(self: *const PageList, tag: point.Tag) RowOffset {
return switch (tag) {
.active => self.active,
.viewport => self.viewport,
.screen, .history => .{ .page = self.pages.first.? },
};
}
/// Represents some y coordinate within the screen. Since pages can
/// be split at any row boundary, getting some Y-coordinate within
/// any part of the screen may map to a different page and row offset
/// than the original y-coordinate. This struct represents that mapping.
pub const RowOffset = struct {
page: *List.Node,
row_offset: usize = 0,
pub fn rowAndCell(self: RowOffset, x: usize) struct {
row: *pagepkg.Row,
cell: *pagepkg.Cell,
} {
const rac = self.page.data.getRowAndCell(x, self.row_offset);
return .{ .row = rac.row, .cell = rac.cell };
}
/// Get the row at the given row index from this Topleft. This
/// may require traversing into the next page if the row index
/// is greater than the number of rows in this page.
///
/// This will return null if the row index is out of bounds.
fn forward(self: RowOffset, idx: usize) ?RowOffset {
// Index fits within this page
var rows = self.page.data.capacity.rows - self.row_offset;
if (idx < rows) return .{
.page = self.page,
.row_offset = idx + self.row_offset,
};
// Need to traverse page links to find the page
var page: *List.Node = self.page;
var idx_left: usize = idx;
while (idx_left >= rows) {
idx_left -= rows;
page = page.next orelse return null;
rows = page.data.capacity.rows;
}
return .{ .page = page, .row_offset = idx_left };
}
};
const Cell = struct {
page: *List.Node,
row: *pagepkg.Row,
cell: *pagepkg.Cell,
row_idx: usize,
col_idx: usize,
};
test "PageList" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -4,58 +4,21 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const unicode = @import("../../unicode/main.zig");
const PageList = @import("PageList.zig");
const pagepkg = @import("page.zig");
const point = @import("point.zig");
const Page = pagepkg.Page;
// Some magic constants we use that could be tweaked...
/// The number of PageList.Nodes we preheat the pool with. A node is
/// a very small struct so we can afford to preheat many, but the exact
/// number is uncertain. Any number too large is wasting memory, any number
/// too small will cause the pool to have to allocate more memory later.
/// This should be set to some reasonable minimum that we expect a terminal
/// window to scroll into quickly.
const page_preheat = 4;
/// The default number of unique styles per page we expect. It is currently
/// "32" because anecdotally amongst a handful of beta testers, no one
/// under normal terminal use ever used more than 32 unique styles in a
/// single page. We found outliers but it was rare enough that we could
/// allocate those when necessary.
const page_default_styles = 32;
/// The list of pages in the screen. These are expected to be in order
/// where the first page is the topmost page (scrollback) and the last is
/// the bottommost page (the current active page).
const PageList = std.DoublyLinkedList(Page);
/// The memory pool we get page nodes from.
const PagePool = std.heap.MemoryPool(PageList.Node);
/// The general purpose allocator to use for all memory allocations.
/// Unfortunately some screen operations do require allocation.
alloc: Allocator,
/// The memory pool we get page nodes for the linked list from.
page_pool: PagePool,
/// The list of pages in the screen.
pages: PageList,
/// The page that contains the top of the current viewport and the row
/// within that page that is the top of the viewport (0-indexed).
viewport: *PageList.Node,
viewport_row: usize,
/// The current cursor position
cursor: Cursor,
/// The current desired screen dimensions. I say "desired" because individual
/// pages may still be a different size and not yet reflowed since we lazily
/// reflow text.
cols: usize,
rows: usize,
/// The cursor position.
const Cursor = struct {
// The x/y position within the viewport.
@ -66,12 +29,11 @@ const Cursor = struct {
/// next character print will force a soft-wrap.
pending_wrap: bool = false,
// The page that the cursor is on and the offset into that page that
// the current y exists.
page: *PageList.Node,
page_row: usize,
page_row_ptr: *pagepkg.Row,
page_cell_ptr: *pagepkg.Cell,
/// The pointers into the page list where the cursor is currently
/// located. This makes it faster to move the cursor.
page_offset: PageList.RowOffset,
page_row: *pagepkg.Row,
page_cell: *pagepkg.Cell,
};
/// Initialize a new screen.
@ -81,65 +43,82 @@ pub fn init(
rows: usize,
max_scrollback: usize,
) !Screen {
_ = max_scrollback;
// Initialize our backing pages. This will initialize the viewport.
var pages = try PageList.init(alloc, cols, rows, max_scrollback);
errdefer pages.deinit();
// The screen starts with a single page that is the entire viewport,
// and we'll split it thereafter if it gets too large and add more as
// necessary.
var pool = try PagePool.initPreheated(alloc, page_preheat);
errdefer pool.deinit();
var page = try pool.create();
// no errdefer because the pool deinit will clean up the page
page.* = .{
.data = try Page.init(alloc, .{
.cols = cols,
.rows = rows,
.styles = page_default_styles,
}),
};
errdefer page.data.deinit(alloc);
var page_list: PageList = .{};
page_list.prepend(page);
const cursor_row_ptr, const cursor_cell_ptr = ptr: {
const rac = page.data.getRowAndCell(0, 0);
break :ptr .{ rac.row, rac.cell };
};
// The viewport is guaranteed to exist, so grab it so we can setup
// our initial cursor.
const page_offset = pages.rowOffset(.{ .active = .{ .x = 0, .y = 0 } });
const page_rac = page_offset.rowAndCell(0);
return .{
.alloc = alloc,
.cols = cols,
.rows = rows,
.page_pool = pool,
.pages = page_list,
.viewport = page,
.viewport_row = 0,
.pages = pages,
.cursor = .{
.x = 0,
.y = 0,
.page = page,
.page_row = 0,
.page_row_ptr = cursor_row_ptr,
.page_cell_ptr = cursor_cell_ptr,
.page_offset = page_offset,
.page_row = page_rac.row,
.page_cell = page_rac.cell,
},
};
}
pub fn deinit(self: *Screen) void {
// Deallocate all the pages. We don't need to deallocate the list or
// nodes because they all reside in the pool.
while (self.pages.popFirst()) |node| node.data.deinit(self.alloc);
self.page_pool.deinit();
self.pages.deinit();
}
/// Dump the screen to a string. The writer given should be buffered;
/// this function does not attempt to efficiently write and generally writes
/// one byte at a time.
pub fn dumpString(
self: *const Screen,
writer: anytype,
tl: point.Point,
) !void {
var blank_rows: usize = 0;
var iter = self.pages.rowIterator(tl);
while (iter.next()) |row_offset| {
const rac = row_offset.rowAndCell(0);
const cells = cells: {
const cells: [*]pagepkg.Cell = @ptrCast(rac.cell);
break :cells cells[0..self.pages.cols];
};
if (blank_rows > 0) {
for (0..blank_rows) |_| try writer.writeByte('\n');
blank_rows = 0;
}
// TODO: handle wrap
blank_rows += 1;
for (cells) |cell| {
// TODO: handle blanks between chars
if (cell.codepoint == 0) break;
try writer.print("{u}", .{cell.codepoint});
}
}
}
fn dumpStringAlloc(
self: *const Screen,
alloc: Allocator,
tl: point.Point,
) ![]const u8 {
var builder = std.ArrayList(u8).init(alloc);
defer builder.deinit();
try self.dumpString(builder.writer(), tl);
return try builder.toOwnedSlice();
}
fn testWriteString(self: *Screen, text: []const u8) !void {
const view = try std.unicode.Utf8View.init(text);
var iter = view.iterator();
while (iter.nextCodepoint()) |c| {
if (self.cursor.x == self.cols) {
if (self.cursor.x == self.pages.cols) {
@panic("wrap not implemented");
}
@ -151,11 +130,11 @@ fn testWriteString(self: *Screen, text: []const u8) !void {
assert(width == 1 or width == 2);
switch (width) {
1 => {
self.cursor.page_cell_ptr.codepoint = c;
self.cursor.page_cell.codepoint = c;
self.cursor.x += 1;
if (self.cursor.x < self.cols) {
const cell_ptr: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell_ptr);
self.cursor.page_cell_ptr = @ptrCast(cell_ptr + 1);
if (self.cursor.x < self.pages.cols) {
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
self.cursor.page_cell = @ptrCast(cell + 1);
} else {
@panic("wrap not implemented");
}
@ -175,4 +154,7 @@ test "Screen read and write" {
defer s.deinit();
try s.testWriteString("hello, world");
const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
defer alloc.free(str);
//try testing.expectEqualStrings("hello, world", str);
}

View File

@ -107,6 +107,25 @@ pub const Page = struct {
self.* = undefined;
}
/// Get a single row. y must be valid.
pub fn getRow(self: *const Page, y: usize) *Row {
assert(y < self.capacity.rows);
return &self.rows.ptr(self.memory)[y];
}
/// Get the cells for a row.
pub fn getCells(self: *const Page, row: *Row) []Cell {
if (comptime std.debug.runtime_safety) {
const rows = self.rows.ptr(self.memory);
const cells = self.cells.ptr(self.memory);
assert(@intFromPtr(row) >= @intFromPtr(rows));
assert(@intFromPtr(row) < @intFromPtr(cells));
}
const cells = row.cells.ptr(self.memory);
return cells[0..self.capacity.cols];
}
/// Get the row and cell for the given X/Y within this page.
pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct {
row: *Row,

View File

@ -0,0 +1,71 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
/// The possible reference locations for a point. When someone says
/// "(42, 80)" in the context of a terminal, that could mean multiple
/// things: it is in the current visible viewport? the current active
/// area of the screen where the cursor is? the entire scrollback history?
/// etc. This tag is used to differentiate those cases.
pub const Tag = enum {
/// Top-left is part of the active area where a running program can
/// jump the cursor and make changes. The active area is the "editable"
/// part of the screen.
///
/// The bottom-right of the active tag differs from all other tags
/// because it includes the full height (rows) of the screen, including
/// rows that may not be written yet. This is required because the active
/// area is fully "addressable" by the running program (see below) whereas
/// the other tags are used primarliy for reading/modifying past-written
/// data so they can't address unwritten rows.
///
/// Note for those less familiar with terminal functionality: there
/// are escape sequences to move the cursor to any position on
/// the screen, but it is limited to the size of the viewport and
/// the bottommost part of the screen. Terminal programs can't --
/// with sequences at the time of writing this comment -- modify
/// anything in the scrollback, visible viewport (if it differs
/// from the active area), etc.
active,
/// Top-left is the visible viewport. This means that if the user
/// has scrolled in any direction, top-left changes. The bottom-right
/// is the last written row from the top-left.
viewport,
/// Top-left is the furthest back in the scrollback history
/// supported by the screen and the bottom-right is the bottom-right
/// of the last written row. Note this last point is important: the
/// bottom right is NOT necessarilly the same as "active" because
/// "active" always allows referencing the full rows tall of the
/// screen whereas "screen" only contains written rows.
screen,
/// The top-left is the same as "screen" but the bottom-right is
/// the line just before the top of "active". This contains only
/// the scrollback history.
history,
};
/// An x/y point in the terminal for some definition of location (tag).
pub const Point = union(Tag) {
active: Coordinate,
viewport: Coordinate,
screen: Coordinate,
history: Coordinate,
pub const Coordinate = struct {
x: usize = 0,
y: usize = 0,
};
pub fn coord(self: Point) Coordinate {
return switch (self) {
.active,
.viewport,
.screen,
.history,
=> |v| v,
};
}
};