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