diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 56ea71e63..79e424b93 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -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"); } diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index 2d684acb2..92b7f4c5e 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -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; diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index fe5cd660c..6d35653d3 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -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); } diff --git a/src/terminal/new/page.zig b/src/terminal/new/page.zig index ad9d49326..de88a820b 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal/new/page.zig @@ -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, diff --git a/src/terminal/new/point.zig b/src/terminal/new/point.zig new file mode 100644 index 000000000..e0961e201 --- /dev/null +++ b/src/terminal/new/point.zig @@ -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, + }; + } +};