Skip to content

Commit a4d54dc

Browse files
authored
terminal: remove all legacy encodeUtf8 functions, replace with formatter (ghostty-org#9392)
This removes all existing functionality that I know of that encodes a terminal, screen, pagelist, or page as plaintext and unifies all logic onto the formatter system.
1 parent 028ce83 commit a4d54dc

4 files changed

Lines changed: 69 additions & 343 deletions

File tree

src/terminal/PageList.zig

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3216,50 +3216,6 @@ pub fn getCell(self: *const PageList, pt: point.Point) ?Cell {
32163216
};
32173217
}
32183218

3219-
pub const EncodeUtf8Options = struct {
3220-
/// The start and end points of the dump, both inclusive. The x will
3221-
/// be ignored and the full row will always be dumped.
3222-
tl: Pin,
3223-
br: ?Pin = null,
3224-
3225-
/// If true, this will unwrap soft-wrapped lines. If false, this will
3226-
/// dump the screen as it is visually seen in a rendered window.
3227-
unwrap: bool = true,
3228-
3229-
/// See Page.EncodeUtf8Options.
3230-
cell_map: ?*Page.CellMap = null,
3231-
};
3232-
3233-
/// Encode the pagelist to utf8 to the given writer.
3234-
///
3235-
/// The writer should be buffered; this function does not attempt to
3236-
/// efficiently write and often writes one byte at a time.
3237-
///
3238-
/// Note: this is tested using Screen.dumpString. This is a function that
3239-
/// predates this and is a thin wrapper around it so the tests all live there.
3240-
pub fn encodeUtf8(
3241-
self: *const PageList,
3242-
writer: *std.Io.Writer,
3243-
opts: EncodeUtf8Options,
3244-
) anyerror!void {
3245-
// We don't currently use self at all. There is an argument that this
3246-
// function should live on Pin instead but there is some future we might
3247-
// need state on here so... letting it go.
3248-
_ = self;
3249-
3250-
var page_opts: Page.EncodeUtf8Options = .{
3251-
.unwrap = opts.unwrap,
3252-
.cell_map = opts.cell_map,
3253-
};
3254-
var iter = opts.tl.pageIterator(.right_down, opts.br);
3255-
while (iter.next()) |chunk| {
3256-
const page: *const Page = &chunk.node.data;
3257-
page_opts.start_y = chunk.start;
3258-
page_opts.end_y = chunk.end;
3259-
page_opts.preceding = try page.encodeUtf8(writer, page_opts);
3260-
}
3261-
}
3262-
32633219
/// Log a debug diagram of the page list to the provided writer.
32643220
///
32653221
/// EXAMPLE:
@@ -3857,13 +3813,17 @@ pub fn getBottomRight(self: *const PageList, tag: point.Tag) ?Pin {
38573813
},
38583814

38593815
.viewport => viewport: {
3860-
const tl = self.getTopLeft(.viewport);
3861-
break :viewport tl.down(self.rows - 1).?;
3816+
var br = self.getTopLeft(.viewport);
3817+
br = br.down(self.rows - 1).?;
3818+
br.x = br.node.data.size.cols - 1;
3819+
break :viewport br;
38623820
},
38633821

38643822
.history => active: {
3865-
const tl = self.getTopLeft(.active);
3866-
break :active tl.up(1);
3823+
var br = self.getTopLeft(.active);
3824+
br = br.up(1) orelse return null;
3825+
br.x = br.node.data.size.cols - 1;
3826+
break :active br;
38673827
},
38683828
};
38693829
}

src/terminal/Screen.zig

Lines changed: 32 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2799,9 +2799,38 @@ pub fn promptPath(
27992799
pub fn dumpString(
28002800
self: *const Screen,
28012801
writer: *std.Io.Writer,
2802-
opts: PageList.EncodeUtf8Options,
2803-
) anyerror!void {
2804-
try self.pages.encodeUtf8(writer, opts);
2802+
opts: struct {
2803+
/// The start and end points of the dump, both inclusive. The x will
2804+
/// be ignored and the full row will always be dumped.
2805+
tl: Pin,
2806+
br: ?Pin = null,
2807+
2808+
/// If true, this will unwrap soft-wrapped lines. If false, this will
2809+
/// dump the screen as it is visually seen in a rendered window.
2810+
unwrap: bool = true,
2811+
},
2812+
) std.Io.Writer.Error!void {
2813+
// Create a formatter and use that to emit our text.
2814+
var formatter: ScreenFormatter = .init(self, .{
2815+
.emit = .plain,
2816+
.unwrap = opts.unwrap,
2817+
.trim = false,
2818+
});
2819+
2820+
// Set up the selection based on the pins
2821+
const tl = opts.tl;
2822+
const br = opts.br orelse self.pages.getBottomRight(.screen).?;
2823+
2824+
formatter.content = .{
2825+
.selection = Selection.init(
2826+
tl,
2827+
br,
2828+
false, // not rectangle
2829+
),
2830+
};
2831+
2832+
// Emit
2833+
try formatter.format(writer);
28052834
}
28062835

28072836
/// You should use dumpString, this is a restricted version mostly for
@@ -8916,81 +8945,3 @@ test "Screen: adjustCapacity cursor style exceeds style set capacity" {
89168945
try testing.expect(s.cursor.style.default());
89178946
try testing.expectEqual(style.default_id, s.cursor.style_id);
89188947
}
8919-
8920-
test "Screen UTF8 cell map with newlines" {
8921-
const testing = std.testing;
8922-
const alloc = testing.allocator;
8923-
8924-
var s = try Screen.init(alloc, 80, 24, 0);
8925-
defer s.deinit();
8926-
try s.testWriteString("A\n\nB\n\nC");
8927-
8928-
var cell_map = Page.CellMap.init(alloc);
8929-
defer cell_map.deinit();
8930-
var builder: std.Io.Writer.Allocating = .init(alloc);
8931-
defer builder.deinit();
8932-
try s.dumpString(&builder.writer, .{
8933-
.tl = s.pages.getTopLeft(.screen),
8934-
.br = s.pages.getBottomRight(.screen),
8935-
.cell_map = &cell_map,
8936-
});
8937-
8938-
try testing.expectEqual(7, builder.written().len);
8939-
try testing.expectEqualStrings("A\n\nB\n\nC", builder.written());
8940-
try testing.expectEqual(builder.written().len, cell_map.map.items.len);
8941-
try testing.expectEqual(Page.CellMapEntry{
8942-
.x = 0,
8943-
.y = 0,
8944-
}, cell_map.map.items[0]);
8945-
try testing.expectEqual(Page.CellMapEntry{
8946-
.x = 1,
8947-
.y = 0,
8948-
}, cell_map.map.items[1]);
8949-
try testing.expectEqual(Page.CellMapEntry{
8950-
.x = 0,
8951-
.y = 1,
8952-
}, cell_map.map.items[2]);
8953-
try testing.expectEqual(Page.CellMapEntry{
8954-
.x = 0,
8955-
.y = 2,
8956-
}, cell_map.map.items[3]);
8957-
}
8958-
8959-
test "Screen UTF8 cell map with blank prefix" {
8960-
const testing = std.testing;
8961-
const alloc = testing.allocator;
8962-
8963-
var s = try Screen.init(alloc, 80, 24, 0);
8964-
defer s.deinit();
8965-
s.cursorAbsolute(2, 1);
8966-
try s.testWriteString("B");
8967-
8968-
var cell_map: Page.CellMap = .init(alloc);
8969-
defer cell_map.deinit();
8970-
var builder: std.Io.Writer.Allocating = .init(alloc);
8971-
defer builder.deinit();
8972-
try s.dumpString(&builder.writer, .{
8973-
.tl = s.pages.getTopLeft(.screen),
8974-
.br = s.pages.getBottomRight(.screen),
8975-
.cell_map = &cell_map,
8976-
});
8977-
8978-
try testing.expectEqualStrings("\n B", builder.written());
8979-
try testing.expectEqual(builder.written().len, cell_map.map.items.len);
8980-
try testing.expectEqual(Page.CellMapEntry{
8981-
.x = 0,
8982-
.y = 0,
8983-
}, cell_map.map.items[0]);
8984-
try testing.expectEqual(Page.CellMapEntry{
8985-
.x = 0,
8986-
.y = 1,
8987-
}, cell_map.map.items[1]);
8988-
try testing.expectEqual(Page.CellMapEntry{
8989-
.x = 1,
8990-
.y = 1,
8991-
}, cell_map.map.items[2]);
8992-
try testing.expectEqual(Page.CellMapEntry{
8993-
.x = 2,
8994-
.y = 1,
8995-
}, cell_map.map.items[3]);
8996-
}

src/terminal/page.zig

Lines changed: 0 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,193 +1501,6 @@ pub const Page = struct {
15011501
return self.grapheme_map.map(self.memory).capacity();
15021502
}
15031503

1504-
/// Options for encoding the page as UTF-8.
1505-
pub const EncodeUtf8Options = struct {
1506-
/// The range of rows to encode. If end_y is null, then it will
1507-
/// encode to the end of the page.
1508-
start_y: size.CellCountInt = 0,
1509-
end_y: ?size.CellCountInt = null,
1510-
1511-
/// If true, this will unwrap soft-wrapped lines. If false, this will
1512-
/// dump the screen as it is visually seen in a rendered window.
1513-
unwrap: bool = true,
1514-
1515-
/// Preceding state from encoding the prior page. Used to preserve
1516-
/// blanks properly across multiple pages.
1517-
preceding: TrailingUtf8State = .{},
1518-
1519-
/// If non-null, this will be cleared and filled with the x/y
1520-
/// coordinates of each byte in the UTF-8 encoded output.
1521-
/// The index in the array is the byte offset in the output
1522-
/// where 0 is the cursor of the writer when the function is
1523-
/// called.
1524-
cell_map: ?*CellMap = null,
1525-
1526-
/// Trailing state for UTF-8 encoding.
1527-
pub const TrailingUtf8State = struct {
1528-
rows: usize = 0,
1529-
cells: usize = 0,
1530-
};
1531-
};
1532-
1533-
/// See cell_map
1534-
pub const CellMap = struct {
1535-
alloc: Allocator,
1536-
map: std.ArrayList(CellMapEntry),
1537-
1538-
pub fn init(alloc: Allocator) CellMap {
1539-
return .{
1540-
.alloc = alloc,
1541-
.map = .empty,
1542-
};
1543-
}
1544-
1545-
pub fn deinit(self: *CellMap) void {
1546-
self.map.deinit(self.alloc);
1547-
}
1548-
};
1549-
1550-
/// The x/y coordinate of a single cell in the cell map.
1551-
pub const CellMapEntry = struct {
1552-
y: size.CellCountInt,
1553-
x: size.CellCountInt,
1554-
};
1555-
1556-
/// Encode the page contents as UTF-8.
1557-
///
1558-
/// If preceding is non-null, then it will be used to initialize our
1559-
/// blank rows/cells count so that we can accumulate blanks across
1560-
/// multiple pages.
1561-
///
1562-
/// Note: Many tests for this function are done via Screen.dumpString
1563-
/// tests since that function is a thin wrapper around this one and
1564-
/// it makes it easier to test input contents.
1565-
pub fn encodeUtf8(
1566-
self: *const Page,
1567-
writer: *std.Io.Writer,
1568-
opts: EncodeUtf8Options,
1569-
) anyerror!EncodeUtf8Options.TrailingUtf8State {
1570-
var blank_rows: usize = opts.preceding.rows;
1571-
var blank_cells: usize = opts.preceding.cells;
1572-
1573-
const start_y: size.CellCountInt = opts.start_y;
1574-
const end_y: size.CellCountInt = opts.end_y orelse self.size.rows;
1575-
1576-
// We can probably avoid this by doing the logic below in a different
1577-
// way. The reason this exists is so that when we end a non-blank
1578-
// line with a newline, we can correctly map the cell map over to
1579-
// the correct x value.
1580-
//
1581-
// For example "A\nB". The cell map for "\n" should be (1, 0).
1582-
// This is tested in Screen.zig so feel free to refactor this.
1583-
var last_x: size.CellCountInt = 0;
1584-
1585-
for (start_y..end_y) |y_usize| {
1586-
const y: size.CellCountInt = @intCast(y_usize);
1587-
const row: *Row = self.getRow(y);
1588-
const cells: []const Cell = self.getCells(row);
1589-
1590-
// If this row is blank, accumulate to avoid a bunch of extra
1591-
// work later. If it isn't blank, make sure we dump all our
1592-
// blanks.
1593-
if (!Cell.hasTextAny(cells)) {
1594-
blank_rows += 1;
1595-
continue;
1596-
}
1597-
for (1..blank_rows + 1) |i| {
1598-
try writer.writeByte('\n');
1599-
1600-
// This is tested in Screen.zig, i.e. one test is
1601-
// "cell map with newlines"
1602-
if (opts.cell_map) |cell_map| {
1603-
try cell_map.map.append(cell_map.alloc, .{
1604-
.x = last_x,
1605-
.y = @intCast(y - blank_rows + i - 1),
1606-
});
1607-
last_x = 0;
1608-
}
1609-
}
1610-
blank_rows = 0;
1611-
1612-
// If we're not wrapped, we always add a newline so after
1613-
// the row is printed we can add a newline.
1614-
if (!row.wrap or !opts.unwrap) blank_rows += 1;
1615-
1616-
// If the row doesn't continue a wrap then we need to reset
1617-
// our blank cell count.
1618-
if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0;
1619-
1620-
// Go through each cell and print it
1621-
for (cells, 0..) |*cell, x_usize| {
1622-
const x: size.CellCountInt = @intCast(x_usize);
1623-
1624-
// Skip spacers
1625-
switch (cell.wide) {
1626-
.narrow, .wide => {},
1627-
.spacer_head, .spacer_tail => continue,
1628-
}
1629-
1630-
// If we have a zero value, then we accumulate a counter. We
1631-
// only want to turn zero values into spaces if we have a non-zero
1632-
// char sometime later.
1633-
if (!cell.hasText()) {
1634-
blank_cells += 1;
1635-
continue;
1636-
}
1637-
if (blank_cells > 0) {
1638-
try writer.splatByteAll(' ', blank_cells);
1639-
if (opts.cell_map) |cell_map| {
1640-
for (0..blank_cells) |i| try cell_map.map.append(cell_map.alloc, .{
1641-
.x = @intCast(x - blank_cells + i),
1642-
.y = y,
1643-
});
1644-
}
1645-
1646-
blank_cells = 0;
1647-
}
1648-
1649-
switch (cell.content_tag) {
1650-
.codepoint => {
1651-
try writer.print("{u}", .{cell.content.codepoint});
1652-
if (opts.cell_map) |cell_map| {
1653-
last_x = x + 1;
1654-
try cell_map.map.append(cell_map.alloc, .{
1655-
.x = x,
1656-
.y = y,
1657-
});
1658-
}
1659-
},
1660-
1661-
.codepoint_grapheme => {
1662-
try writer.print("{u}", .{cell.content.codepoint});
1663-
if (opts.cell_map) |cell_map| {
1664-
last_x = x + 1;
1665-
try cell_map.map.append(cell_map.alloc, .{
1666-
.x = x,
1667-
.y = y,
1668-
});
1669-
}
1670-
1671-
for (self.lookupGrapheme(cell).?) |cp| {
1672-
try writer.print("{u}", .{cp});
1673-
if (opts.cell_map) |cell_map| try cell_map.map.append(cell_map.alloc, .{
1674-
.x = x,
1675-
.y = y,
1676-
});
1677-
}
1678-
},
1679-
1680-
// Unreachable since we do hasText() above
1681-
.bg_color_palette,
1682-
.bg_color_rgb,
1683-
=> unreachable,
1684-
}
1685-
}
1686-
}
1687-
1688-
return .{ .rows = blank_rows, .cells = blank_cells };
1689-
}
1690-
16911504
/// Returns the bitset for the dirty bits on this page.
16921505
///
16931506
/// The returned value is a DynamicBitSetUnmanaged but it is NOT

0 commit comments

Comments
 (0)