a modern tui library written in zig
1const std = @import("std");
2const assert = std.debug.assert;
3const Style = @import("Cell.zig").Style;
4const Cell = @import("Cell.zig");
5const MouseShape = @import("Mouse.zig").Shape;
6const CursorShape = Cell.CursorShape;
7
8const log = std.log.scoped(.vaxis);
9
10const InternalScreen = @This();
11
12pub const InternalCell = struct {
13 char: std.ArrayListUnmanaged(u8) = .empty,
14 style: Style = .{},
15 uri: std.ArrayListUnmanaged(u8) = .empty,
16 uri_id: std.ArrayListUnmanaged(u8) = .empty,
17 // if we got skipped because of a wide character
18 skipped: bool = false,
19 default: bool = true,
20
21 // If we should skip rendering *this* round due to being printed over previously (from a scaled
22 // cell, for example)
23 skip: bool = false,
24
25 scale: Cell.Scale = .{},
26
27 pub fn eql(self: InternalCell, cell: Cell) bool {
28
29 // fastpath when both cells are default
30 if (self.default and cell.default) return true;
31
32 return std.mem.eql(u8, self.char.items, cell.char.grapheme) and
33 Style.eql(self.style, cell.style) and
34 std.mem.eql(u8, self.uri.items, cell.link.uri) and
35 std.mem.eql(u8, self.uri_id.items, cell.link.params);
36 }
37};
38
39arena: *std.heap.ArenaAllocator,
40width: u16 = 0,
41height: u16 = 0,
42
43buf: []InternalCell,
44
45cursor_row: u16 = 0,
46cursor_col: u16 = 0,
47cursor_vis: bool = false,
48cursor_shape: CursorShape = .default,
49
50mouse_shape: MouseShape = .default,
51
52/// sets each cell to the default cell
53pub fn init(alloc: std.mem.Allocator, w: u16, h: u16) !InternalScreen {
54 const arena = try alloc.create(std.heap.ArenaAllocator);
55 arena.* = .init(alloc);
56 var screen = InternalScreen{
57 .arena = arena,
58 .buf = try arena.allocator().alloc(InternalCell, @as(usize, @intCast(w)) * h),
59 };
60 for (screen.buf, 0..) |_, i| {
61 screen.buf[i] = .{
62 .char = try std.ArrayListUnmanaged(u8).initCapacity(arena.allocator(), 1),
63 .uri = .empty,
64 .uri_id = .empty,
65 };
66 screen.buf[i].char.appendAssumeCapacity(' ');
67 }
68 screen.width = w;
69 screen.height = h;
70 return screen;
71}
72
73pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void {
74 self.arena.deinit();
75 alloc.destroy(self.arena);
76 self.* = undefined;
77}
78
79/// writes a cell to a location. 0 indexed
80pub fn writeCell(
81 self: *InternalScreen,
82 col: u16,
83 row: u16,
84 cell: Cell,
85) void {
86 if (self.width <= col) {
87 // column out of bounds
88 return;
89 }
90 if (self.height <= row) {
91 // height out of bounds
92 return;
93 }
94 const i = (@as(usize, @intCast(row)) * self.width) + col;
95 assert(i < self.buf.len);
96 self.buf[i].char.clearRetainingCapacity();
97 self.buf[i].char.appendSlice(self.arena.allocator(), cell.char.grapheme) catch {
98 log.warn("couldn't write grapheme", .{});
99 };
100 self.buf[i].uri.clearRetainingCapacity();
101 self.buf[i].uri.appendSlice(self.arena.allocator(), cell.link.uri) catch {
102 log.warn("couldn't write uri", .{});
103 };
104 self.buf[i].uri_id.clearRetainingCapacity();
105 self.buf[i].uri_id.appendSlice(self.arena.allocator(), cell.link.params) catch {
106 log.warn("couldn't write uri_id", .{});
107 };
108 self.buf[i].style = cell.style;
109 self.buf[i].default = cell.default;
110}
111
112pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell {
113 if (self.width <= col) {
114 // column out of bounds
115 return null;
116 }
117 if (self.height <= row) {
118 // height out of bounds
119 return null;
120 }
121 const i = (row * self.width) + col;
122 assert(i < self.buf.len);
123 const cell = self.buf[i];
124 return .{
125 .char = .{ .grapheme = cell.char.items },
126 .style = cell.style,
127 .link = .{
128 .uri = cell.uri.items,
129 .params = cell.uri_id.items,
130 },
131 .default = cell.default,
132 };
133}
134
135test "InternalScreen: out-of-bounds read/write are ignored" {
136 var screen = try InternalScreen.init(std.testing.allocator, 2, 2);
137 defer screen.deinit(std.testing.allocator);
138
139 const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } };
140 screen.writeCell(0, 1, sentinel);
141
142 const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } };
143 screen.writeCell(2, 0, oob_cell);
144 const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult;
145 try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A"));
146 try std.testing.expect(screen.readCell(2, 0) == null);
147}