a modern tui library written in zig
1const std = @import("std");
2const Image = @import("Image.zig");
3
4char: Character = .{},
5style: Style = .{},
6link: Hyperlink = .{},
7image: ?Image.Placement = null,
8default: bool = false,
9/// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if
10/// it should rely on the terminal's autowrap feature which can help with primary screen resizes
11wrapped: bool = false,
12
13/// Segment is a contiguous run of text that has a constant style
14pub const Segment = struct {
15 text: []const u8,
16 style: Style = .{},
17 link: Hyperlink = .{},
18};
19
20pub const Character = struct {
21 grapheme: []const u8 = " ",
22 /// width should only be provided when the application is sure the terminal
23 /// will measure the same width. This can be ensure by using the gwidth method
24 /// included in libvaxis. If width is 0, libvaxis will measure the glyph at
25 /// render time
26 width: usize = 1,
27};
28
29pub const CursorShape = enum {
30 default,
31 block_blink,
32 block,
33 underline_blink,
34 underline,
35 beam_blink,
36 beam,
37};
38
39pub const Hyperlink = struct {
40 uri: []const u8 = "",
41 /// ie "id=app-1234"
42 params: []const u8 = "",
43};
44
45pub const Style = struct {
46 pub const Underline = enum {
47 off,
48 single,
49 double,
50 curly,
51 dotted,
52 dashed,
53 };
54
55 fg: Color = .default,
56 bg: Color = .default,
57 ul: Color = .default,
58 ul_style: Underline = .off,
59
60 bold: bool = false,
61 dim: bool = false,
62 italic: bool = false,
63 blink: bool = false,
64 reverse: bool = false,
65 invisible: bool = false,
66 strikethrough: bool = false,
67
68 pub fn eql(a: Style, b: Style) bool {
69 const SGRBits = packed struct {
70 bold: bool,
71 dim: bool,
72 italic: bool,
73 blink: bool,
74 reverse: bool,
75 invisible: bool,
76 strikethrough: bool,
77 };
78 const a_sgr: SGRBits = .{
79 .bold = a.bold,
80 .dim = a.dim,
81 .italic = a.italic,
82 .blink = a.blink,
83 .reverse = a.reverse,
84 .invisible = a.invisible,
85 .strikethrough = a.strikethrough,
86 };
87 const b_sgr: SGRBits = .{
88 .bold = b.bold,
89 .dim = b.dim,
90 .italic = b.italic,
91 .blink = b.blink,
92 .reverse = b.reverse,
93 .invisible = b.invisible,
94 .strikethrough = b.strikethrough,
95 };
96 const a_cast: u7 = @bitCast(a_sgr);
97 const b_cast: u7 = @bitCast(b_sgr);
98 return a_cast == b_cast and
99 Color.eql(a.fg, b.fg) and
100 Color.eql(a.bg, b.bg) and
101 Color.eql(a.ul, b.ul) and
102 a.ul_style == b.ul_style;
103 }
104};
105
106pub const Color = union(enum) {
107 default,
108 index: u8,
109 rgb: [3]u8,
110
111 pub const Kind = union(enum) {
112 fg,
113 bg,
114 cursor,
115 index: u8,
116 };
117
118 /// Returned when querying a color from the terminal
119 pub const Report = struct {
120 kind: Kind,
121 value: [3]u8,
122 };
123
124 pub const Scheme = enum {
125 dark,
126 light,
127 };
128
129 pub fn eql(a: Color, b: Color) bool {
130 switch (a) {
131 .default => return b == .default,
132 .index => |a_idx| {
133 switch (b) {
134 .index => |b_idx| return a_idx == b_idx,
135 else => return false,
136 }
137 },
138 .rgb => |a_rgb| {
139 switch (b) {
140 .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
141 a_rgb[1] == b_rgb[1] and
142 a_rgb[2] == b_rgb[2],
143 else => return false,
144 }
145 },
146 }
147 }
148
149 pub fn rgbFromUint(val: u24) Color {
150 const r_bits = val & 0b11111111_00000000_00000000;
151 const g_bits = val & 0b00000000_11111111_00000000;
152 const b_bits = val & 0b00000000_00000000_11111111;
153 const rgb = [_]u8{
154 @truncate(r_bits >> 16),
155 @truncate(g_bits >> 8),
156 @truncate(b_bits),
157 };
158 return .{ .rgb = rgb };
159 }
160
161 /// parse an XParseColor-style rgb specification into an rgb Color. The spec
162 /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
163 /// be the same as the low two bits.
164 pub fn rgbFromSpec(spec: []const u8) !Color {
165 var iter = std.mem.splitScalar(u8, spec, ':');
166 const prefix = iter.next() orelse return error.InvalidColorSpec;
167 if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
168
169 const spec_str = iter.next() orelse return error.InvalidColorSpec;
170
171 var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
172
173 const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
174 if (r_raw.len != 4) return error.InvalidColorSpec;
175
176 const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
177 if (g_raw.len != 4) return error.InvalidColorSpec;
178
179 const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
180 if (b_raw.len != 4) return error.InvalidColorSpec;
181
182 const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
183 const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
184 const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
185
186 return .{
187 .rgb = [_]u8{ r, g, b },
188 };
189 }
190
191 test "rgbFromSpec" {
192 const spec = "rgb:aaaa/bbbb/cccc";
193 const actual = try rgbFromSpec(spec);
194 switch (actual) {
195 .rgb => |rgb| {
196 try std.testing.expectEqual(0xAA, rgb[0]);
197 try std.testing.expectEqual(0xBB, rgb[1]);
198 try std.testing.expectEqual(0xCC, rgb[2]);
199 },
200 else => try std.testing.expect(false),
201 }
202 }
203};