From 204e4f86634451422e4ba3a6e3d0f1f855af480d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2024 09:37:03 -0800 Subject: [PATCH] terminal: support cell_map for encodeUtf8 --- src/terminal/PageList.zig | 8 +++- src/terminal/Screen.zig | 78 +++++++++++++++++++++++++++++++++++++++ src/terminal/page.zig | 75 +++++++++++++++++++++++++++++++++++-- 3 files changed, 156 insertions(+), 5 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 175e3f64f..f8afc801a 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2553,6 +2553,9 @@ pub const EncodeUtf8Options = struct { /// If true, this will unwrap soft-wrapped lines. If false, this will /// dump the screen as it is visually seen in a rendered window. unwrap: bool = true, + + /// See Page.EncodeUtf8Options. + cell_map: ?*Page.CellMap = null, }; /// Encode the pagelist to utf8 to the given writer. @@ -2572,7 +2575,10 @@ pub fn encodeUtf8( // need state on here so... letting it go. _ = self; - var page_opts: Page.EncodeUtf8Options = .{ .unwrap = opts.unwrap }; + var page_opts: Page.EncodeUtf8Options = .{ + .unwrap = opts.unwrap, + .cell_map = opts.cell_map, + }; var iter = opts.tl.pageIterator(.right_down, opts.br); while (iter.next()) |chunk| { const page: *const Page = &chunk.node.data; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index bf63e7e05..ac9483742 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -8468,3 +8468,81 @@ test "Screen: adjustCapacity cursor style ref count" { ); } } + +test "Screen UTF8 cell map with newlines" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + try s.testWriteString("A\n\nB\n\nC"); + + var cell_map = Page.CellMap.init(alloc); + defer cell_map.deinit(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + try s.dumpString(builder.writer(), .{ + .tl = s.pages.getTopLeft(.screen), + .br = s.pages.getBottomRight(.screen), + .cell_map = &cell_map, + }); + + try testing.expectEqual(7, builder.items.len); + try testing.expectEqualStrings("A\n\nB\n\nC", builder.items); + try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 0, + }, cell_map.items[0]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 1, + .y = 0, + }, cell_map.items[1]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 1, + }, cell_map.items[2]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 2, + }, cell_map.items[3]); +} + +test "Screen UTF8 cell map with blank prefix" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 0); + defer s.deinit(); + s.cursorAbsolute(2, 1); + try s.testWriteString("B"); + + var cell_map = Page.CellMap.init(alloc); + defer cell_map.deinit(); + var builder = std.ArrayList(u8).init(alloc); + defer builder.deinit(); + try s.dumpString(builder.writer(), .{ + .tl = s.pages.getTopLeft(.screen), + .br = s.pages.getBottomRight(.screen), + .cell_map = &cell_map, + }); + + try testing.expectEqualStrings("\n B", builder.items); + try testing.expectEqual(builder.items.len, cell_map.items.len); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 0, + }, cell_map.items[0]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 0, + .y = 1, + }, cell_map.items[1]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 1, + .y = 1, + }, cell_map.items[2]); + try testing.expectEqual(Page.CellMapEntry{ + .x = 2, + .y = 1, + }, cell_map.items[3]); +} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index d41f37e8d..83164e163 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1496,6 +1496,13 @@ pub const Page = struct { /// blanks properly across multiple pages. preceding: TrailingUtf8State = .{}, + /// If non-null, this will be cleared and filled with the x/y + /// coordinates of each byte in the UTF-8 encoded output. + /// The index in the array is the byte offset in the output + /// where 0 is the cursor of the writer when the function is + /// called. + cell_map: ?*CellMap = null, + /// Trailing state for UTF-8 encoding. pub const TrailingUtf8State = struct { rows: usize = 0, @@ -1503,13 +1510,22 @@ pub const Page = struct { }; }; + /// See cell_map + pub const CellMap = std.ArrayList(CellMapEntry); + + /// The x/y coordinate of a single cell in the cell map. + pub const CellMapEntry = struct { + y: size.CellCountInt, + x: size.CellCountInt, + }; + /// Encode the page contents as UTF-8. /// /// If preceding is non-null, then it will be used to initialize our /// blank rows/cells count so that we can accumulate blanks across /// multiple pages. /// - /// Note: The tests for this function are done via Screen.dumpString + /// Note: Many tests for this function are done via Screen.dumpString /// tests since that function is a thin wrapper around this one and /// it makes it easier to test input contents. pub fn encodeUtf8( @@ -1522,7 +1538,18 @@ pub const Page = struct { const start_y: size.CellCountInt = opts.start_y; const end_y: size.CellCountInt = opts.end_y orelse self.size.rows; - for (start_y..end_y) |y| { + + // We can probably avoid this by doing the logic below in a different + // way. The reason this exists is so that when we end a non-blank + // line with a newline, we can correctly map the cell map over to + // the correct x value. + // + // For example "A\nB". The cell map for "\n" should be (1, 0). + // This is tested in Screen.zig so feel free to refactor this. + var last_x: size.CellCountInt = 0; + + for (start_y..end_y) |y_usize| { + const y: size.CellCountInt = @intCast(y_usize); const row: *Row = self.getRow(y); const cells: []const Cell = self.getCells(row); @@ -1533,7 +1560,19 @@ pub const Page = struct { blank_rows += 1; continue; } - for (0..blank_rows) |_| try writer.writeByte('\n'); + for (1..blank_rows + 1) |i| { + try writer.writeByte('\n'); + + // This is tested in Screen.zig, i.e. one test is + // "cell map with newlines" + if (opts.cell_map) |cell_map| { + try cell_map.append(.{ + .x = last_x, + .y = @intCast(y - blank_rows + i - 1), + }); + last_x = 0; + } + } blank_rows = 0; // If we're not wrapped, we always add a newline so after @@ -1545,7 +1584,9 @@ pub const Page = struct { if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0; // Go through each cell and print it - for (cells) |*cell| { + for (cells, 0..) |*cell, x_usize| { + const x: size.CellCountInt = @intCast(x_usize); + // Skip spacers switch (cell.wide) { .narrow, .wide => {}, @@ -1561,18 +1602,44 @@ pub const Page = struct { } if (blank_cells > 0) { try writer.writeByteNTimes(' ', blank_cells); + if (opts.cell_map) |cell_map| { + for (0..blank_cells) |i| try cell_map.append(.{ + .x = @intCast(x - blank_cells + i), + .y = y, + }); + } + blank_cells = 0; } switch (cell.content_tag) { .codepoint => { try writer.print("{u}", .{cell.content.codepoint}); + if (opts.cell_map) |cell_map| { + last_x = x + 1; + try cell_map.append(.{ + .x = x, + .y = y, + }); + } }, .codepoint_grapheme => { try writer.print("{u}", .{cell.content.codepoint}); + if (opts.cell_map) |cell_map| { + last_x = x + 1; + try cell_map.append(.{ + .x = x, + .y = y, + }); + } + for (self.lookupGrapheme(cell).?) |cp| { try writer.print("{u}", .{cp}); + if (opts.cell_map) |cell_map| try cell_map.append(.{ + .x = x, + .y = y, + }); } },