Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291//! A row of cells.const std = @import("std");const ir = @import("root.zig");
const Allocator = std.mem.Allocator;const ArrayList = std.ArrayList;const Writer = std.Io.Writer;const t = std.testing;const Cell = ir.Cell;const Style = ir.Style;const Link = ir.Link;const Method = ir.width.Method;
const Line = @This();
cells: []Cell = &.{},
pub fn init(allocator: Allocator, size: usize) !Line { const cells = try allocator.alloc(Cell, size); for (cells) |*cell| cell.* = .space; return .{ .cells = cells };}
// this might be broken in actual usagepub fn deinit(self: *Line, allocator: Allocator) void { allocator.free(self.cells);}
// pub fn new(allocator: Allocator, w: usize) !Line {// const cells = try allocator.alloc(Cell, w);// for (cells) |*c| c.* = .blank;// return .{ .cells = cells };// }
pub fn width(self: Line) usize { return self.cells.len;}
/// Returns the cell at x, or null when out of bounds.pub fn at(self: Line, x: usize) ?*Cell { if (x >= self.cells.len) return null; return &self.cells[x];}
/// Sets the cell at x. Nil cells are treated as blank empty cells./// Wide cells are clamped to the line and their continuation columns are/// zero-width placeholders; partially overwriting a wide cell blanks it.pub fn set(self: Line, x: usize, cell: ?Cell) void { const line_width = self.cells.len; if (x >= line_width) return;
const prev = self.at(x); if (prev) |p| { const pw = @as(usize, p.width); if (pw > 1) { // Writing to the first wide cell: clear it. var j: usize = 0; while (j < pw and x + j < line_width) : (j += 1) { self.cells[x + j] = p.*; self.cells[x + j].emptyize(); } } else if (pw == 0) { // Writing to wide cell placeholders: clear the wide cell. var j: usize = 1; while (j <= x) : (j += 1) { if (self.at(x - j)) |wide| { const ww = @as(usize, wide.width); if (ww > 1 and j < ww) { var k: usize = 0; while (k < ww) : (k += 1) { self.cells[x - j + k] = wide.*; self.cells[x - j + k].emptyize(); } break; } } } } }
const cc = cell orelse { self.cells[x] = .space; return; };
self.cells[x] = cc; const cw = @as(usize, cc.width); if (x + cw > line_width) { // The cell doesn't fit: write blanks with the same style. var i: usize = 0; while (i < cw and x + i < line_width) : (i += 1) { self.cells[x + i] = cc; self.cells[x + i].emptyize(); } return; }
if (cw > 1) { // Mark wide cells with zero-width placeholder cells. var j: usize = 1; while (j < cw and x + j < line_width) : (j += 1) { self.cells[x + j] = .empty; } }}
/// Plain string representation; trailing (unstyled) spaces are removed.pub fn string(self: Line, allocator: Allocator) ![]u8 { var out: ArrayList(u8) = .empty; defer out.deinit(allocator); var pending: usize = 0; for (self.cells) |c| { if (c.isWidePlaceholder()) continue; if (c.equal(.space)) { pending += 1; continue; } while (pending > 0) : (pending -= 1) try out.append(allocator, ' '); try out.appendSlice(allocator, c.content); } return out.toOwnedSlice(allocator);}
/// Renders the line with styles, preserving trailing spaces.pub fn renderAlloc(self: Line, allocator: Allocator) ![]u8 { var aw: Writer.Allocating = .init(allocator); defer aw.deinit(); try self.render(&aw.writer); var list = aw.toArrayList(); return list.toOwnedSlice(allocator);}
/// Renders a single line with the required SGR attributes and hyperlinks./// Empty (space) cells are emitted as spaces without styling; trailing spaces/// are preserved (unlike `Line.string`).fn render(self: Line, writer: *Writer) Writer.Error!void { var pen: Style = .{}; var link: Link = .{};
for (self.cells) |cell| { if (cell.isWidePlaceholder()) continue;
if (cell.equal(.space)) { if (!pen.isZero()) { try writer.writeAll(ir.ansi.resetStyle); pen = .{}; } if (!link.isZero()) { _ = try ir.ansi.resetHyperlink(writer, ""); link = .{}; } try writer.writeAll(" "); continue; }
if (cell.style.isZero() and !pen.isZero()) { try writer.writeAll(ir.ansi.resetStyle); pen = .{}; } if (!cell.style.equal(pen)) { _ = try Style.writeDiff(pen, cell.style, writer); pen = cell.style; }
// Reset a stale hyperlink before switching. if (!cell.link.equal(link) and link.url.len != 0) { _ = try ir.ansi.resetHyperlink(writer, ""); link = .{}; } if (!cell.link.equal(link)) { _ = try ir.ansi.setHyperlink(writer, cell.link.url, cell.link.params); link = cell.link; }
try writer.writeAll(cell.content); }
if (link.url.len != 0) _ = try ir.ansi.resetHyperlink(writer, ""); if (!pen.isZero()) try writer.writeAll(ir.ansi.resetStyle);}
/// Whether the line contains a cell that a cell-level diff cannot safely/// reposition across: a wide cell (width > 1), or a cell whose width the/// terminal may measure differently than the model.pub fn hasDrift(self: Line, method: Method) bool { for (self.cells) |c| { if (c.width == 0 or c.content.len == 0) continue; if (c.width > 1 or method.stringWidth(c.content) != ir.width.wcWidth(c.content)) return true; } return false;}
/// FNV-1a over the concatenated cell contents. Deterministic stand-in for/// Go's `hash/maphash`.pub fn hash(self: *const Line) u64 { var h: u64 = 1469598103934665603; for (self.cells) |c| { for (c.content) |b| { h ^= b; h *%= 1099511628211; } } return h;}
test "line string trims trailing unstyled spaces" { var l: Line = try .init(t.allocator, 6); defer l.deinit(t.allocator); l.cells[0] = .{ .content = "你", .width = 2 }; l.cells[1] = .empty; // continuation l.cells[2] = .{ .content = "好", .width = 2 }; l.cells[3] = .empty; // continuation l.cells[4] = .of("!", 1); // Pos 5 left as empty space -> trimmed. const slice = try l.string(t.allocator); defer t.allocator.free(slice); try t.expectEqualStrings("你好!", slice);}
test "render line preserves trailing spaces" { var l: Line = try .init(t.allocator, 10); defer l.deinit(t.allocator); l.cells[0] = .of("H", 1); l.cells[1] = .of("i", 1); const slice = try l.renderAlloc(t.allocator); defer t.allocator.free(slice); try t.expectEqualStrings("Hi ", slice);}
test "render line styled trailing spaces" { var l: Line = try .init(t.allocator, 5); defer l.deinit(t.allocator); l.cells[0] = .of("A", 1); for (l.cells[1..]) |*cell| { cell.* = .{ .content = " ", .width = 1, .style = .{ .bg = .{ .ansi16 = 1 } }, }; } const slice = try l.renderAlloc(t.allocator); defer t.allocator.free(slice); try t.expectEqualStrings("A\x1b[41m \x1b[m", slice);}
test "render line with hyperlink" { var l: Line = try .init(t.allocator, 4); defer l.deinit(t.allocator); const link: Link = .init("http://example.com", ""); const contents = [_][]const u8{ "L", "i", "n", "k" }; for (l.cells, 0..) |*c, i| { c.* = .{ .content = contents[i], .width = 1, .link = link, }; } const slice = try l.renderAlloc(t.allocator); defer t.allocator.free(slice); try t.expectEqualStrings( "\x1b]8;;http://example.com\x07Link\x1b]8;;\x07", slice, );}
test "line set semantics" { var l: Line = try .init(t.allocator, 10); defer l.deinit(t.allocator);
l.set(5, .of("a", 1)); try t.expectEqualStrings("a", l.cells[5].content);
// Wide cell then overwrite first column. l.set(2, .of("世", 2)); try t.expectEqualStrings("世", l.cells[2].content); try t.expect(l.cells[3].isWidePlaceholder()); l.set(2, .of("a", 1)); try t.expectEqualStrings("a", l.cells[2].content);
// Overwrite middle of a wide cell blanks it. l.set(4, .of("世", 2)); l.set(5, .of("a", 1)); try t.expectEqualStrings("a", l.cells[5].content); try t.expect(l.cells[4].equal(.space));
// Wide cell at the end doesn't fit: written as styled spaces. for (l.cells) |*cell| cell.* = .space; l.set(9, .of("世", 2)); try t.expect(l.cells[9].equal(.space));}