a modern tui library written in zig
at main 6.3 kB view raw
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};