mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: support cell_map for encodeUtf8
This commit is contained in:
@ -2553,6 +2553,9 @@ pub const EncodeUtf8Options = struct {
|
|||||||
/// If true, this will unwrap soft-wrapped lines. If false, this will
|
/// If true, this will unwrap soft-wrapped lines. If false, this will
|
||||||
/// dump the screen as it is visually seen in a rendered window.
|
/// dump the screen as it is visually seen in a rendered window.
|
||||||
unwrap: bool = true,
|
unwrap: bool = true,
|
||||||
|
|
||||||
|
/// See Page.EncodeUtf8Options.
|
||||||
|
cell_map: ?*Page.CellMap = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Encode the pagelist to utf8 to the given writer.
|
/// Encode the pagelist to utf8 to the given writer.
|
||||||
@ -2572,7 +2575,10 @@ pub fn encodeUtf8(
|
|||||||
// need state on here so... letting it go.
|
// need state on here so... letting it go.
|
||||||
_ = self;
|
_ = 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);
|
var iter = opts.tl.pageIterator(.right_down, opts.br);
|
||||||
while (iter.next()) |chunk| {
|
while (iter.next()) |chunk| {
|
||||||
const page: *const Page = &chunk.node.data;
|
const page: *const Page = &chunk.node.data;
|
||||||
|
@ -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]);
|
||||||
|
}
|
||||||
|
@ -1496,6 +1496,13 @@ pub const Page = struct {
|
|||||||
/// blanks properly across multiple pages.
|
/// blanks properly across multiple pages.
|
||||||
preceding: TrailingUtf8State = .{},
|
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.
|
/// Trailing state for UTF-8 encoding.
|
||||||
pub const TrailingUtf8State = struct {
|
pub const TrailingUtf8State = struct {
|
||||||
rows: usize = 0,
|
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.
|
/// Encode the page contents as UTF-8.
|
||||||
///
|
///
|
||||||
/// If preceding is non-null, then it will be used to initialize our
|
/// If preceding is non-null, then it will be used to initialize our
|
||||||
/// blank rows/cells count so that we can accumulate blanks across
|
/// blank rows/cells count so that we can accumulate blanks across
|
||||||
/// multiple pages.
|
/// 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
|
/// tests since that function is a thin wrapper around this one and
|
||||||
/// it makes it easier to test input contents.
|
/// it makes it easier to test input contents.
|
||||||
pub fn encodeUtf8(
|
pub fn encodeUtf8(
|
||||||
@ -1522,7 +1538,18 @@ pub const Page = struct {
|
|||||||
|
|
||||||
const start_y: size.CellCountInt = opts.start_y;
|
const start_y: size.CellCountInt = opts.start_y;
|
||||||
const end_y: size.CellCountInt = opts.end_y orelse self.size.rows;
|
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 row: *Row = self.getRow(y);
|
||||||
const cells: []const Cell = self.getCells(row);
|
const cells: []const Cell = self.getCells(row);
|
||||||
|
|
||||||
@ -1533,7 +1560,19 @@ pub const Page = struct {
|
|||||||
blank_rows += 1;
|
blank_rows += 1;
|
||||||
continue;
|
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;
|
blank_rows = 0;
|
||||||
|
|
||||||
// If we're not wrapped, we always add a newline so after
|
// 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;
|
if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0;
|
||||||
|
|
||||||
// Go through each cell and print it
|
// 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
|
// Skip spacers
|
||||||
switch (cell.wide) {
|
switch (cell.wide) {
|
||||||
.narrow, .wide => {},
|
.narrow, .wide => {},
|
||||||
@ -1561,18 +1602,44 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
if (blank_cells > 0) {
|
if (blank_cells > 0) {
|
||||||
try writer.writeByteNTimes(' ', blank_cells);
|
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;
|
blank_cells = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (cell.content_tag) {
|
switch (cell.content_tag) {
|
||||||
.codepoint => {
|
.codepoint => {
|
||||||
try writer.print("{u}", .{cell.content.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 => {
|
.codepoint_grapheme => {
|
||||||
try writer.print("{u}", .{cell.content.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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (self.lookupGrapheme(cell).?) |cp| {
|
for (self.lookupGrapheme(cell).?) |cp| {
|
||||||
try writer.print("{u}", .{cp});
|
try writer.print("{u}", .{cp});
|
||||||
|
if (opts.cell_map) |cell_map| try cell_map.append(.{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user