From 001ec979a263ec212467582c07aca15afc9cf6a1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Aug 2022 17:33:25 -0700 Subject: [PATCH] big API surface for screen2, can test write/read now --- src/terminal/Screen2.zig | 327 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 325 insertions(+), 2 deletions(-) diff --git a/src/terminal/Screen2.zig b/src/terminal/Screen2.zig index a63865e21..16995f00b 100644 --- a/src/terminal/Screen2.zig +++ b/src/terminal/Screen2.zig @@ -20,6 +20,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const utf8proc = @import("utf8proc"); const color = @import("color.zig"); const CircBuf = @import("circ_buf.zig").CircBuf; @@ -31,7 +32,7 @@ const log = std.log.scoped(.screen); /// Note: the union is extern so that it follows the same memory layout /// semantics as C, which allows us to have a tightly packed union. const StorageCell = extern union { - row_header: RowHeader, + header: RowHeader, cell: Cell, test { @@ -62,7 +63,7 @@ const RowHeader = struct { }; /// Cell is a single cell within the screen. -const Cell = struct { +pub const Cell = struct { /// The primary unicode codepoint for this cell. Most cells (almost all) /// contain exactly one unicode codepoint. However, it is possible for /// cells to contain multiple if multiple codepoints are used to create @@ -117,6 +118,158 @@ const Cell = struct { } }; +/// A row is a single row in the screen. +pub const Row = struct { + /// Raw internal storage, do NOT write to this, use only the + /// helpers. Writing directly to this can easily mess up state + /// causing future crashes or misrendering. + storage: []StorageCell, + + /// Set that this row is soft-wrapped. This doesn't change the contents + /// of this row so the row won't be marked dirty. + pub fn setWrapped(self: Row, v: bool) void { + self.storage[0].header.wrap = v; + } + + /// Get a pointr to the cell at column x (0-indexed). This always + /// assumes that the cell was modified, notifying the renderer on the + /// next call to re-render this cell. Any change detection to avoid + /// this should be done prior. + pub fn getCellPtr(self: Row, x: usize) *Cell { + assert(x < self.storage.len - 1); + return &self.storage[x + 1].cell; + } + + /// Read-only iterator for the cells in the row. + pub fn cellIterator(self: Row) CellIterator { + return .{ .row = self }; + } +}; + +/// Used to iterate through the rows of a specific region. +pub const RowIterator = struct { + screen: *Screen, + tag: RowIndexTag, + value: usize = 0, + + pub fn next(self: *RowIterator) ?Row { + if (self.value >= self.tag.maxLen(self.screen)) return null; + const idx = self.tag.index(self.value); + const res = self.screen.getRow(idx); + self.value += 1; + return res; + } +}; + +/// Used to iterate through the rows of a specific region. +pub const CellIterator = struct { + row: Row, + i: usize = 0, + + pub fn next(self: *CellIterator) ?Cell { + if (self.i >= self.row.storage.len - 1) return null; + const res = self.row.storage[self.i + 1].cell; + self.i += 1; + return res; + } +}; + +/// RowIndex represents a row within the screen. There are various meanings +/// of a row index and this union represents the available types. For example, +/// when talking about row "0" you may want the first row in the viewport, +/// the first row in the scrollback, or the first row in the active area. +/// +/// All row indexes are 0-indexed. +pub const RowIndex = union(RowIndexTag) { + /// The index is from the top of the screen. The screen includes all + /// the history. + screen: usize, + + /// The index is from the top of the viewport. Therefore, depending + /// on where the user has scrolled the viewport, "0" is different. + viewport: usize, + + /// The index is from the top of the active area. The active area is + /// always "rows" tall, and 0 is the top row. The active area is the + /// "edit-able" area where the terminal cursor is. + active: usize, + + /// The index is from the top of the history (scrollback) to just + /// prior to the active area. + history: usize, + + /// Convert this row index into a screen offset. This will validate + /// the value so even if it is already a screen value, this may error. + pub fn toScreen(self: RowIndex, screen: *const Screen) RowIndex { + const y = switch (self) { + .screen => |y| y: { + assert(y < RowIndexTag.screen.maxLen(screen)); + break :y y; + }, + + .viewport => |y| y: { + assert(y < RowIndexTag.viewport.maxLen(screen)); + break :y y + screen.viewport; + }, + + .active => |y| y: { + assert(y < RowIndexTag.active.maxLen(screen)); + break :y RowIndexTag.history.maxLen(screen) + y; + }, + + .history => |y| y: { + assert(y < RowIndexTag.history.maxLen(screen)); + break :y y; + }, + }; + + return .{ .screen = y }; + } +}; + +/// The tags of RowIndex +pub const RowIndexTag = enum { + screen, + viewport, + active, + history, + + /// The max length for a given tag. This is a length, not an index, + /// so it is 1-indexed. If the value is zero, it means that this + /// section of the screen is empty or disabled. + pub fn maxLen(self: RowIndexTag, screen: *const Screen) usize { + const rows_written = screen.rowsWritten(); + + return switch (self) { + // Screen can be any of the written rows + .screen => rows_written, + + // Viewport can be any of the written rows or the max size + // of a viewport. + .viewport => @minimum(screen.rows, rows_written), + + // History is all the way up to the top of our active area. If + // we haven't filled our active area, there is no history. + .history => if (rows_written > screen.rows) rows_written - screen.rows else 0, + + // Active area can be any number of rows. We ignore rows + // written here because this is the only row index that can + // actively grow our rows. + .active => screen.rows, + }; + } + + /// Construct a RowIndex from a tag. + pub fn index(self: RowIndexTag, value: usize) RowIndex { + return switch (self) { + .screen => .{ .screen = value }, + .viewport => .{ .viewport = value }, + .active => .{ .active = value }, + .history => .{ .history = value }, + }; + } +}; + const StorageBuf = CircBuf(StorageCell); /// The allocator used for all the storage operations @@ -133,6 +286,9 @@ cols: usize, /// is in addition to the number of visible rows. max_scrollback: usize, +/// The row (offset from the top) where the viewport currently is. +viewport: usize, + /// Initialize a new screen. pub fn init( alloc: Allocator, @@ -151,6 +307,7 @@ pub fn init( .rows = rows, .cols = cols, .max_scrollback = max_scrollback, + .viewport = 0, }; } @@ -158,10 +315,176 @@ pub fn deinit(self: *Screen) void { self.storage.deinit(self.alloc); } +/// Returns an iterator that can be used to iterate over all of the rows +/// from index zero of the given row index type. This can therefore iterate +/// from row 0 of the active area, history, viewport, etc. +pub fn rowIterator(self: *Screen, tag: RowIndexTag) RowIterator { + return .{ .screen = self, .tag = tag }; +} + +/// Returns the row at the given index. This row is writable, although +/// only the active area should probably be written to. +pub fn getRow(self: *Screen, index: RowIndex) Row { + // Get our offset into storage + const offset = index.toScreen(self).screen * (self.cols + 1); + + // Get the slices into the storage. This should never wrap because + // we're perfectly aligned on row boundaries. + const slices = self.storage.getPtrSlice(offset, self.cols + 1); + assert(slices[0].len == self.cols + 1 and slices[1].len == 0); + + return .{ .storage = slices[0] }; +} + +/// Returns the offset into the storage buffer that the given row can +/// be found. This assumes valid input and will crash if the input is +/// invalid. +fn rowOffset(self: Screen, index: RowIndex) usize { + // +1 for row header + return index.toScreen().screen * (self.cols + 1); +} + +fn rowsWritten(self: Screen) usize { + // The number of rows we've actually written into our buffer + // This should always be cleanly divisible since we only request + // data in row chunks from the buffer. + assert(@mod(self.storage.len(), self.cols + 1) == 0); + return self.storage.len() / (self.cols + 1); +} + +/// Writes a basic string into the screen for testing. Newlines (\n) separate +/// each row. If a line is longer than the available columns, soft-wrapping +/// will occur. This will automatically handle basic wide chars. +pub fn testWriteString(self: *Screen, text: []const u8) void { + var y: usize = 0; + var x: usize = 0; + + const view = std.unicode.Utf8View.init(text) catch unreachable; + var iter = view.iterator(); + while (iter.nextCodepoint()) |c| { + // Explicit newline forces a new row + if (c == '\n') { + y += 1; + x = 0; + continue; + } + + // If we're writing past the end of the active area, scroll. + if (y >= self.rows) { + y -= 1; + @panic("TODO"); + //self.scroll(.{ .delta = 1 }); + } + + // Get our row + var row = self.getRow(.{ .active = y }); + + // If we're writing past the end, we need to soft wrap. + if (x == self.cols) { + row.setWrapped(true); + y += 1; + x = 0; + if (y >= self.rows) { + y -= 1; + @panic("TODO"); + //self.scroll(.{ .delta = 1 }); + } + row = self.getRow(.{ .active = y }); + } + + // If our character is double-width, handle it. + const width = utf8proc.charwidth(c); + assert(width == 1 or width == 2); + switch (width) { + 1 => { + const cell = row.getCellPtr(x); + cell.char = @intCast(u32, c); + }, + + 2 => { + if (x == self.cols - 1) { + const cell = row.getCellPtr(x); + cell.char = ' '; + cell.attrs.wide_spacer_head = true; + + // wrap + row.setWrapped(true); + y += 1; + x = 0; + if (y >= self.rows) { + y -= 1; + @panic("TODO"); + //self.scroll(.{ .delta = 1 }); + } + row = self.getRow(.{ .active = y }); + } + + { + const cell = row.getCellPtr(x); + cell.char = @intCast(u32, c); + cell.attrs.wide = true; + } + + { + x += 1; + const cell = row.getCellPtr(x); + cell.char = ' '; + cell.attrs.wide_spacer_tail = true; + } + }, + + else => unreachable, + } + + x += 1; + } +} + +/// Turns the screen into a string. Different regions of the screen can +/// be selected using the "tag", i.e. if you want to output the viewport, +/// the scrollback, the full screen, etc. +/// +/// This is only useful for testing. +pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 { + const buf = try alloc.alloc(u8, self.storage.len() * 4); + + var i: usize = 0; + var y: usize = 0; + var rows = self.rowIterator(tag); + while (rows.next()) |row| { + defer y += 1; + + if (y > 0) { + buf[i] = '\n'; + i += 1; + } + + var cells = row.cellIterator(); + while (cells.next()) |cell| { + // TODO: handle character after null + if (cell.char > 0) { + i += try std.unicode.utf8Encode(@intCast(u21, cell.char), buf[i..]); + } + } + } + + // Never render the final newline + const str = std.mem.trimRight(u8, buf[0..i], "\n"); + return try alloc.realloc(buf, str.len); +} + test { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 3, 5, 0); defer s.deinit(); + + const str = "1ABCD\n2EFGH\n3IJKL"; + s.testWriteString(str); + { + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings(str, contents); + } }