From a8b1498a2be672239a51fc5a0921ed45b02c34e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Feb 2024 14:31:35 -0800 Subject: [PATCH] terminal/new: screen has more logic, eraseActive --- src/terminal/new/PageList.zig | 6 ++ src/terminal/new/Screen.zig | 186 +++++++++++++++++++++++++++++++--- src/terminal/new/Terminal.zig | 77 ++------------ 3 files changed, 190 insertions(+), 79 deletions(-) diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index afdecfb03..2fc02005b 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -12,6 +12,7 @@ const stylepkg = @import("style.zig"); const size = @import("size.zig"); const OffsetBuf = size.OffsetBuf; const Page = pagepkg.Page; +const Row = pagepkg.Row; /// 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 @@ -436,6 +437,11 @@ pub const RowChunkIterator = struct { page: *List.Node, start: usize, end: usize, + + pub fn rows(self: Chunk) []Row { + const rows_ptr = self.page.data.rows.ptr(self.page.data.memory); + return rows_ptr[self.start..self.end]; + } }; }; diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 3b5d6ec6f..d0efdb5e4 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -12,6 +12,7 @@ const point = @import("point.zig"); const size = @import("size.zig"); const style = @import("style.zig"); const Page = pagepkg.Page; +const Row = pagepkg.Row; const Cell = pagepkg.Cell; /// The general purpose allocator to use for all memory allocations. @@ -247,6 +248,16 @@ pub fn cursorDownScroll(self: *Screen) !void { } } +/// Move the cursor down if we're not at the bottom of the screen. Otherwise +/// scroll. Currently only used for testing. +fn cursorDownOrScroll(self: *Screen) !void { + if (self.cursor.y + 1 < self.pages.rows) { + self.cursorDown(1); + } else { + try self.cursorDownScroll(); + } +} + /// Options for scrolling the viewport of the terminal grid. The reason /// we have this in addition to PageList.Scroll is because we have additional /// scroll behaviors that are not part of the PageList.Scroll enum. @@ -268,13 +279,77 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { /// Erase the active area of the screen from y=0 to rows-1. The cells /// are blanked using the given blank cell. -pub fn eraseActive(self: *Screen, blank: Cell) void { - // We use rowIterator because it handles the case where the active - // area spans multiple underlying pages. This is slightly slower to - // calculate but erasing isn't a high-frequency operation. We can - // optimize this later, too. - _ = self; - _ = blank; +pub fn eraseActive(self: *Screen) void { + var it = self.pages.rowChunkIterator(.{ .active = .{} }); + while (it.next()) |chunk| { + for (chunk.rows()) |*row| { + const cells_offset = row.cells; + const cells_multi: [*]Cell = row.cells.ptr(chunk.page.data.memory); + const cells = cells_multi[0..self.pages.cols]; + + // Erase all cells + self.eraseCells(&chunk.page.data, row, cells); + + // Reset our row to point to the proper memory but everything + // else is zeroed. + row.* = .{ .cells = cells_offset }; + } + } +} + +/// Erase the cells with the blank cell. This takes care to handle +/// cleaning up graphemes and styles. +pub fn eraseCells( + self: *Screen, + page: *Page, + row: *Row, + cells: []Cell, +) void { + // If this row has graphemes, then we need go through a slow path + // and delete the cell graphemes. + if (row.grapheme) { + for (cells) |*cell| { + if (cell.hasGrapheme()) page.clearGrapheme(row, cell); + } + } + + if (row.styled) { + for (cells) |*cell| { + if (cell.style_id == style.default_id) continue; + + // Fast-path, the style ID matches, in this case we just update + // our own ref and continue. We never delete because our style + // is still active. + if (cell.style_id == self.cursor.style_id) { + self.cursor.style_ref.?.* -= 1; + continue; + } + + // Slow path: we need to lookup this style so we can decrement + // the ref count. Since we've already loaded everything, we also + // just go ahead and GC it if it reaches zero, too. + if (page.styles.lookupId(page.memory, cell.style_id)) |prev_style| { + // Below upsert can't fail because it should already be present + const md = page.styles.upsert(page.memory, prev_style.*) catch unreachable; + assert(md.ref > 0); + md.ref -= 1; + if (md.ref == 0) page.styles.remove(page.memory, cell.style_id); + } + } + + // If we have no left/right scroll region we can be sure that + // the row is no longer styled. + if (cells.len == self.pages.cols) row.styled = false; + } + + @memset(cells, self.blankCell()); +} + +/// Returns the blank cell to use when doing terminal operations that +/// require preserving the bg color. +fn blankCell(self: *const Screen) Cell { + if (self.cursor.style_id == style.default_id) return .{}; + return self.cursor.style.bgCell() orelse .{}; } /// Set a style attribute for the current cursor. @@ -527,10 +602,20 @@ pub fn dumpStringAlloc( return try builder.toOwnedSlice(); } +/// This is basically a really jank version of Terminal.printString. We +/// have to reimplement it here because we want a way to print to the screen +/// to test it but don't want all the features of Terminal. fn testWriteString(self: *Screen, text: []const u8) !void { const view = try std.unicode.Utf8View.init(text); var iter = view.iterator(); while (iter.nextCodepoint()) |c| { + // Explicit newline forces a new row + if (c == '\n') { + try self.cursorDownOrScroll(); + self.cursorHorizontalAbsolute(0); + continue; + } + if (self.cursor.x == self.pages.cols) { @panic("wrap not implemented"); } @@ -543,12 +628,20 @@ fn testWriteString(self: *Screen, text: []const u8) !void { assert(width == 1 or width == 2); switch (width) { 1 => { - self.cursor.page_cell.content_tag = .codepoint; - self.cursor.page_cell.content = .{ .codepoint = c }; - self.cursor.x += 1; - if (self.cursor.x < self.pages.cols) { - const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); - self.cursor.page_cell = @ptrCast(cell + 1); + self.cursor.page_cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = c }, + .style_id = self.cursor.style_id, + }; + + // If we have a ref-counted style, increase. + if (self.cursor.style_ref) |ref| { + ref.* += 1; + self.cursor.page_row.styled = true; + } + + if (self.cursor.x + 1 < self.pages.cols) { + self.cursorRight(1); } else { @panic("wrap not implemented"); } @@ -574,6 +667,20 @@ test "Screen read and write" { try testing.expectEqualStrings("hello, world", str); } +test "Screen read and write newline" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + try testing.expectEqual(@as(style.Id, 0), s.cursor.style_id); + + try s.testWriteString("hello\nworld"); + const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(str); + try testing.expectEqualStrings("hello\nworld", str); +} + test "Screen style basics" { const testing = std.testing; const alloc = testing.allocator; @@ -635,3 +742,56 @@ test "Screen style reset with unset" { try testing.expect(s.cursor.style_id == 0); try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); } + +test "Screen eraseActive one line" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + try s.testWriteString("hello, world"); + s.eraseActive(); + const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(str); + try testing.expectEqualStrings("", str); +} + +test "Screen eraseActive multi line" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + try s.testWriteString("hello\nworld"); + s.eraseActive(); + const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(str); + try testing.expectEqualStrings("", str); +} + +test "Screen eraseActive styled line" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + + try s.setAttribute(.{ .bold = {} }); + try s.testWriteString("hello world"); + try s.setAttribute(.{ .unset = {} }); + + // We should have one style + const page = s.cursor.page_offset.page.data; + try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); + + s.eraseActive(); + + // We should have none because active cleared it + try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); + + const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(str); + try testing.expectEqualStrings("", str); +} diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index b3f8dc35e..b9a9ac590 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -1192,7 +1192,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { var page = &self.screen.cursor.page_offset.page.data; const cells = page.getCells(row); const cells_write = cells[self.scrolling_region.left .. self.scrolling_region.right + 1]; - self.blankCells(page, row, cells_write); + self.screen.eraseCells(page, row, cells_write); } // Move the cursor to the left margin. But importantly this also @@ -1286,7 +1286,7 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { var page = &self.screen.cursor.page_offset.page.data; const cells = page.getCells(row); const cells_write = cells[self.scrolling_region.left .. self.scrolling_region.right + 1]; - self.blankCells(page, row, cells_write); + self.screen.eraseCells(page, row, cells_write); } // Move the cursor to the left margin. But importantly this also @@ -1350,7 +1350,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // it to be empty so we don't split the multi-cell char. const end: *Cell = @ptrCast(x); if (end.wide == .wide) { - self.blankCells(page, self.screen.cursor.page_row, end[0..1]); + self.screen.eraseCells(page, self.screen.cursor.page_row, end[0..1]); } // We work backwards so we don't overwrite data. @@ -1380,7 +1380,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { } // Insert blanks. The blanks preserve the background color. - self.blankCells(page, self.screen.cursor.page_row, left[0..adjusted_count]); + self.screen.eraseCells(page, self.screen.cursor.page_row, left[0..adjusted_count]); } /// Removes amount characters from the current cursor position to the right. @@ -1410,7 +1410,7 @@ pub fn deleteChars(self: *Terminal, count: usize) void { // previous cell too so we don't split a multi-cell character. if (self.screen.cursor.page_cell.wide == .spacer_tail) { assert(self.screen.cursor.x > 0); - self.blankCells(page, self.screen.cursor.page_row, (left - 1)[0..2]); + self.screen.eraseCells(page, self.screen.cursor.page_row, (left - 1)[0..2]); } // Remaining cols from our cursor to the right margin. @@ -1433,7 +1433,7 @@ pub fn deleteChars(self: *Terminal, count: usize) void { if (end.wide == .spacer_tail) { const wide: [*]Cell = right + count - 1; assert(wide[0].wide == .wide); - self.blankCells(page, self.screen.cursor.page_row, wide[0..2]); + self.screen.eraseCells(page, self.screen.cursor.page_row, wide[0..2]); } while (@intFromPtr(x) <= @intFromPtr(right)) : (x += 1) { @@ -1462,7 +1462,7 @@ pub fn deleteChars(self: *Terminal, count: usize) void { } // Insert blanks. The blanks preserve the background color. - self.blankCells(page, self.screen.cursor.page_row, x[0 .. rem - scroll_amount]); + self.screen.eraseCells(page, self.screen.cursor.page_row, x[0 .. rem - scroll_amount]); } pub fn eraseChars(self: *Terminal, count_req: usize) void { @@ -1497,7 +1497,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { // are protected and go with the fast path. If the last protection // mode was not ISO we also always ignore protection attributes. if (self.screen.protected_mode != .iso) { - self.blankCells( + self.screen.eraseCells( &self.screen.cursor.page_offset.page.data, self.screen.cursor.page_row, cells[0..end], @@ -1512,7 +1512,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { const cell_multi: [*]Cell = @ptrCast(cells + x); const cell: *Cell = @ptrCast(&cell_multi[0]); if (cell.protected) continue; - self.blankCells( + self.screen.eraseCells( &self.screen.cursor.page_offset.page.data, self.screen.cursor.page_row, cell_multi[0..1], @@ -1582,7 +1582,7 @@ pub fn eraseLine( // If we're not respecting protected attributes, we can use a fast-path // to fill the entire line. if (!protected) { - self.blankCells( + self.screen.eraseCells( &self.screen.cursor.page_offset.page.data, self.screen.cursor.page_row, cells[start..end], @@ -1594,7 +1594,7 @@ pub fn eraseLine( const cell_multi: [*]Cell = @ptrCast(cells + x); const cell: *Cell = @ptrCast(&cell_multi[0]); if (cell.protected) continue; - self.blankCells( + self.screen.eraseCells( &self.screen.cursor.page_offset.page.data, self.screen.cursor.page_row, cell_multi[0..1], @@ -1602,54 +1602,6 @@ pub fn eraseLine( } } -/// Blank the given cells. The cells must be long to the given row and page. -/// This will handle refcounted styles properly as well as graphemes. -fn blankCells( - self: *const Terminal, - page: *Page, - row: *Row, - cells: []Cell, -) void { - // If this row has graphemes, then we need go through a slow path - // and delete the cell graphemes. - if (row.grapheme) { - for (cells) |*cell| { - if (cell.hasGrapheme()) page.clearGrapheme(row, cell); - } - } - - if (row.styled) { - for (cells) |*cell| { - if (cell.style_id == style.default_id) continue; - - // Fast-path, the style ID matches, in this case we just update - // our own ref and continue. We never delete because our style - // is still active. - if (cell.style_id == self.screen.cursor.style_id) { - self.screen.cursor.style_ref.?.* -= 1; - continue; - } - - // Slow path: we need to lookup this style so we can decrement - // the ref count. Since we've already loaded everything, we also - // just go ahead and GC it if it reaches zero, too. - if (page.styles.lookupId(page.memory, cell.style_id)) |prev_style| { - // Below upsert can't fail because it should already be present - const md = page.styles.upsert(page.memory, prev_style.*) catch unreachable; - assert(md.ref > 0); - md.ref -= 1; - if (md.ref == 0) page.styles.remove(page.memory, cell.style_id); - } - } - - // If we have no left/right scroll region we can be sure that - // the row is no longer styled. - if (cells.len == self.cols) row.styled = false; - } - - @memset(cells, self.blankCell()); -} - /// Resets all margins and fills the whole screen with the character 'E' /// /// Sets the cursor to the top left corner. @@ -1802,13 +1754,6 @@ pub fn plainString(self: *Terminal, alloc: Allocator) ![]const u8 { return try self.screen.dumpStringAlloc(alloc, .{ .viewport = .{} }); } -/// Returns the blank cell to use when doing terminal operations that -/// require preserving the bg color. -fn blankCell(self: *const Terminal) Cell { - if (self.screen.cursor.style_id == style.default_id) return .{}; - return self.screen.cursor.style.bgCell() orelse .{}; -} - test "Terminal: input with no control characters" { const alloc = testing.allocator; var t = try init(alloc, 40, 40);