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