From 09f8c178002f96b6ba5bb3816479f6af4ea1d7f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 26 Feb 2024 13:10:28 -0800 Subject: [PATCH] terminal/new: erase according to bg sgr --- src/terminal/Terminal.zig | 2 + src/terminal/new/PageList.zig | 14 ++- src/terminal/new/Screen.zig | 41 +++++---- src/terminal/new/Terminal.zig | 164 ++++++++++++++++++++++++++++++++-- src/terminal/new/style.zig | 19 ++++ 5 files changed, 216 insertions(+), 24 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 2e5d94608..5f050e780 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -4373,6 +4373,7 @@ test "Terminal: index bottom of primary screen" { } } +// X test "Terminal: index bottom of primary screen background sgr" { const alloc = testing.allocator; var t = try init(alloc, 5, 5); @@ -5241,6 +5242,7 @@ test "Terminal: eraseChars beyond screen edge" { } } +// X test "Terminal: eraseChars preserves background sgr" { const alloc = testing.allocator; var t = try init(alloc, 10, 10); diff --git a/src/terminal/new/PageList.zig b/src/terminal/new/PageList.zig index f879df345..7a1e46330 100644 --- a/src/terminal/new/PageList.zig +++ b/src/terminal/new/PageList.zig @@ -8,6 +8,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const point = @import("point.zig"); const pagepkg = @import("page.zig"); +const stylepkg = @import("style.zig"); const size = @import("size.zig"); const OffsetBuf = size.OffsetBuf; const Page = pagepkg.Page; @@ -488,13 +489,24 @@ const Cell = struct { row_idx: usize, col_idx: usize, + /// Get the cell style. + /// + /// Not meant for non-test usage since this is inefficient. + pub fn style(self: Cell) stylepkg.Style { + if (self.cell.style_id == stylepkg.default_id) return .{}; + return self.page.data.styles.lookupId( + self.page.data.memory, + self.cell.style_id, + ).?.*; + } + /// Gets the screen point for the given cell. /// /// This is REALLY expensive/slow so it isn't pub. This was built /// for debugging and tests. If you have a need for this outside of /// this file then consider a different approach and ask yourself very /// carefully if you really need this. - fn screenPoint(self: Cell) point.Point { + pub fn screenPoint(self: Cell) point.Point { var x: usize = self.col_idx; var y: usize = self.row_idx; var page = self.page; diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 98210aed4..c0957f59d 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -199,10 +199,10 @@ pub fn cursorAbsolute(self: *Screen, x: size.CellCountInt, y: size.CellCountInt) pub fn cursorDownScroll(self: *Screen) !void { assert(self.cursor.y == self.pages.rows - 1); - // If we have cap space in our current cursor page then we can take - // a fast path: update the size, recalculate the row/cell cursor pointers. const cursor_page = self.cursor.page_offset.page; if (cursor_page.data.capacity.rows > cursor_page.data.size.rows) { + // If we have cap space in our current cursor page then we can take + // a fast path: update the size, recalculate the row/cell cursor pointers. cursor_page.data.size.rows += 1; const page_offset = self.cursor.page_offset.forward(1).?; @@ -210,23 +210,30 @@ pub fn cursorDownScroll(self: *Screen) !void { self.cursor.page_offset = page_offset; self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; - return; + } else { + // No space, we need to allocate a new page and move the cursor to it. + const new_page = try self.pages.grow(); + assert(new_page.data.size.rows == 0); + new_page.data.size.rows = 1; + const page_offset: PageList.RowOffset = .{ + .page = new_page, + .row_offset = 0, + }; + const page_rac = page_offset.rowAndCell(self.cursor.x); + self.cursor.page_offset = page_offset; + self.cursor.page_row = page_rac.row; + self.cursor.page_cell = page_rac.cell; } - // No space, we need to allocate a new page and move the cursor to it. - // TODO: copy style over - - const new_page = try self.pages.grow(); - assert(new_page.data.size.rows == 0); - new_page.data.size.rows = 1; - const page_offset: PageList.RowOffset = .{ - .page = new_page, - .row_offset = 0, - }; - const page_rac = page_offset.rowAndCell(self.cursor.x); - self.cursor.page_offset = page_offset; - self.cursor.page_row = page_rac.row; - self.cursor.page_cell = page_rac.cell; + // The newly created line needs to be styled according to the bg color + // if it is set. + if (self.cursor.style_id != style.default_id) { + if (self.cursor.style.bgCell()) |blank_cell| { + const cell_current: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell); + const cells = cell_current - self.cursor.x; + @memset(cells[0..self.pages.cols], blank_cell); + } + } } /// Options for scrolling the viewport of the terminal grid. The reason diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index ee4ed8148..2fbe30c71 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -1084,6 +1084,8 @@ pub fn insertLines(self: *Terminal, count: usize) void { } } + // Inserted lines should keep our bg color + const blank_cell = self.blankCell(); for (0..adjusted_count) |i| { const row: *Row = @ptrCast(top + i); @@ -1100,8 +1102,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { assert(!row.grapheme); } - // TODO: cells should keep bg style of pen - @memset(cells, .{}); + @memset(cells, blank_cell); } // Move the cursor to the left margin. But importantly this also @@ -1177,6 +1178,7 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { } const bottom: [*]Row = top + (rem - 1); + const blank_cell = self.blankCell(); while (@intFromPtr(y) <= @intFromPtr(bottom)) : (y += 1) { const row: *Row = @ptrCast(y); @@ -1193,8 +1195,7 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { assert(!row.grapheme); } - // TODO: cells should keep bg style of pen - @memset(cells, .{}); + @memset(cells, blank_cell); } // Move the cursor to the left margin. But importantly this also @@ -1230,9 +1231,8 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { }; // Clear the cells - // TODO: clear with current bg color const cells: [*]Cell = @ptrCast(self.screen.cursor.page_cell); - @memset(cells[0..end], .{}); + @memset(cells[0..end], self.blankCell()); // This resets the soft-wrap of this line self.screen.cursor.page_row.wrap = false; @@ -1275,6 +1275,13 @@ 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); @@ -2475,6 +2482,44 @@ test "Terminal: insertLines simple" { } } +test "Terminal: insertLines colors with bg color" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.setCursorPos(2, 2); + + try t.setAttribute(.{ .direct_color_bg = .{ + .r = 0xFF, + .g = 0, + .b = 0, + } }); + t.insertLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\n\nDEF\nGHI", str); + } + + for (0..t.cols) |x| { + const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?; + try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); + try testing.expectEqual(Cell.RGB{ + .r = 0xFF, + .g = 0, + .b = 0, + }, list_cell.cell.content.color_rgb); + } +} + test "Terminal: insertLines outside of scroll region" { const alloc = testing.allocator; var t = try init(alloc, 5, 5); @@ -2958,6 +3003,45 @@ test "Terminal: eraseChars resets wrap" { } } +test "Terminal: eraseChars preserves background sgr" { + const alloc = testing.allocator; + var t = try init(alloc, 10, 10); + defer t.deinit(alloc); + + for ("ABC") |c| try t.print(c); + t.setCursorPos(1, 1); + try t.setAttribute(.{ .direct_color_bg = .{ + .r = 0xFF, + .g = 0, + .b = 0, + } }); + t.eraseChars(2); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" C", str); + { + const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = 0 } }).?; + try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); + try testing.expectEqual(Cell.RGB{ + .r = 0xFF, + .g = 0, + .b = 0, + }, list_cell.cell.content.color_rgb); + } + { + const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 1, .y = 0 } }).?; + try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); + try testing.expectEqual(Cell.RGB{ + .r = 0xFF, + .g = 0, + .b = 0, + }, list_cell.cell.content.color_rgb); + } + } +} + test "Terminal: reverseIndex" { const alloc = testing.allocator; var t = try init(alloc, 2, 5); @@ -3231,6 +3315,36 @@ test "Terminal: index bottom of primary screen" { } } +test "Terminal: index bottom of primary screen background sgr" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + t.setCursorPos(5, 1); + try t.print('A'); + try t.setAttribute(.{ .direct_color_bg = .{ + .r = 0xFF, + .g = 0, + .b = 0, + } }); + try t.index(); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n\n\nA", str); + for (0..5) |x| { + const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?; + try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); + try testing.expectEqual(Cell.RGB{ + .r = 0xFF, + .g = 0, + .b = 0, + }, list_cell.cell.content.color_rgb); + } + } +} + test "Terminal: index inside scroll region" { const alloc = testing.allocator; var t = try init(alloc, 5, 5); @@ -3794,6 +3908,44 @@ test "Terminal: deleteLines simple" { } } +test "Terminal: deleteLines colors with bg color" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.printString("ABC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("DEF"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("GHI"); + t.setCursorPos(2, 2); + + try t.setAttribute(.{ .direct_color_bg = .{ + .r = 0xFF, + .g = 0, + .b = 0, + } }); + t.deleteLines(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("ABC\nGHI", str); + } + + for (0..t.cols) |x| { + const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?; + try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); + try testing.expectEqual(Cell.RGB{ + .r = 0xFF, + .g = 0, + .b = 0, + }, list_cell.cell.content.color_rgb); + } +} + test "Terminal: deleteLines (legacy)" { const alloc = testing.allocator; var t = try init(alloc, 80, 80); diff --git a/src/terminal/new/style.zig b/src/terminal/new/style.zig index 9d867940d..56c2e936a 100644 --- a/src/terminal/new/style.zig +++ b/src/terminal/new/style.zig @@ -51,6 +51,25 @@ pub const Style = struct { return std.mem.eql(u8, std.mem.asBytes(&self), def); } + /// Returns a bg-color only cell from this style, if it exists. + pub fn bgCell(self: Style) ?page.Cell { + return switch (self.bg_color) { + .none => null, + .palette => |idx| .{ + .content_tag = .bg_color_palette, + .content = .{ .color_palette = idx }, + }, + .rgb => |rgb| .{ + .content_tag = .bg_color_rgb, + .content = .{ .color_rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + } }, + }, + }; + } + test { // The size of the struct so we can be aware of changes. const testing = std.testing;