//! Helper function collection to provide ascii encodings for styling outputs. //! Stylings are implemented such that they can be nested in anyway to support //! multiple styles (i.e. bold and italic). //! //! Stylings however also include highlighting for specific terminal capabilities. //! For example url highlighting. // taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License) // with slight modifications const std = @import("std"); const ctlseqs = @import("ctlseqs.zig"); pub const Underline = enum { off, single, double, curly, dotted, dashed, }; pub const Color = union(enum) { default, index: u8, rgb: [3]u8, pub fn eql(a: @This(), b: @This()) bool { switch (a) { .default => return b == .default, .index => |a_idx| { switch (b) { .index => |b_idx| return a_idx == b_idx, else => return false, } }, .rgb => |a_rgb| { switch (b) { .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and a_rgb[1] == b_rgb[1] and a_rgb[2] == b_rgb[2], else => return false, } }, } } pub fn rgbFromUint(val: u24) Color { const r_bits = val & 0b11111111_00000000_00000000; const g_bits = val & 0b00000000_11111111_00000000; const b_bits = val & 0b00000000_00000000_11111111; const rgb = [_]u8{ @truncate(r_bits >> 16), @truncate(g_bits >> 8), @truncate(b_bits), }; return .{ .rgb = rgb }; } /// parse an XParseColor-style rgb specification into an rgb Color. The spec /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always /// be the same as the low two bits. pub fn rgbFromSpec(spec: []const u8) !Color { var iter = std.mem.splitScalar(u8, spec, ':'); const prefix = iter.next() orelse return error.InvalidColorSpec; if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; const spec_str = iter.next() orelse return error.InvalidColorSpec; var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; if (r_raw.len != 4) return error.InvalidColorSpec; const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; if (g_raw.len != 4) return error.InvalidColorSpec; const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; if (b_raw.len != 4) return error.InvalidColorSpec; const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); return .{ .rgb = [_]u8{ r, g, b }, }; } test "rgbFromSpec" { const spec = "rgb:aaaa/bbbb/cccc"; const actual = try rgbFromSpec(spec); switch (actual) { .rgb => |rgb| { try std.testing.expectEqual(0xAA, rgb[0]); try std.testing.expectEqual(0xBB, rgb[1]); try std.testing.expectEqual(0xCC, rgb[2]); }, else => try std.testing.expect(false), } } }; fg: Color = .default, bg: Color = .default, ul: Color = .default, ul_style: Underline = .off, bold: bool = false, dim: bool = false, italic: bool = false, blink: bool = false, reverse: bool = false, invisible: bool = false, strikethrough: bool = false, /// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value /// if the _other_ value differs from the default value. pub fn merge(this: *@This(), other: @This()) void { if (other.fg != .default) this.fg = other.fg; if (other.bg != .default) this.bg = other.bg; if (other.ul != .default) this.ul = other.ul; if (other.ul_style != .off) this.ul_style = other.ul_style; if (other.bold != false) this.bold = other.bold; if (other.dim != false) this.dim = other.dim; if (other.italic != false) this.italic = other.italic; if (other.blink != false) this.blink = other.blink; if (other.reverse != false) this.reverse = other.reverse; if (other.invisible != false) this.invisible = other.invisible; if (other.strikethrough != false) this.strikethrough = other.strikethrough; } fn start(this: @This(), writer: anytype) !void { // foreground switch (this.fg) { .default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), .index => |idx| { switch (idx) { 0...7 => { try std.fmt.format(writer, ctlseqs.fg_base, .{idx}); }, 8...15 => { try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}); }, else => { try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}); }, } }, .rgb => |rgb| { try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); }, } // background switch (this.bg) { .default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), .index => |idx| { switch (idx) { 0...7 => { try std.fmt.format(writer, ctlseqs.bg_base, .{idx}); }, 8...15 => { try std.fmt.format(writer, ctlseqs.bg_bright, .{idx}); }, else => { try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}); }, } }, .rgb => |rgb| { try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); }, } // underline color switch (this.ul) { .default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), .index => |idx| { try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); }, .rgb => |rgb| { try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); }, } // underline style switch (this.ul_style) { .off => try std.fmt.format(writer, ctlseqs.ul_off, .{}), .single => try std.fmt.format(writer, ctlseqs.ul_single, .{}), .double => try std.fmt.format(writer, ctlseqs.ul_double, .{}), .curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}), .dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}), .dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}), } // bold switch (this.bold) { true => try std.fmt.format(writer, ctlseqs.bold_set, .{}), false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), } // dim switch (this.dim) { true => try std.fmt.format(writer, ctlseqs.dim_set, .{}), false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), } // italic switch (this.italic) { true => try std.fmt.format(writer, ctlseqs.italic_set, .{}), false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), } // blink switch (this.blink) { true => try std.fmt.format(writer, ctlseqs.blink_set, .{}), false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), } // reverse switch (this.reverse) { true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}), false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), } // invisible switch (this.invisible) { true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}), false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), } // strikethrough switch (this.strikethrough) { true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}), false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), } } fn end(this: @This(), writer: anytype) !void { // foreground switch (this.fg) { .default => {}, else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), } // background switch (this.bg) { .default => {}, else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), } // underline color switch (this.ul) { .default => {}, else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), } // underline style switch (this.ul_style) { .off => {}, else => try std.fmt.format(writer, ctlseqs.ul_off, .{}), } // bold switch (this.bold) { true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), false => {}, } // dim switch (this.dim) { true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), false => {}, } // italic switch (this.italic) { true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), false => {}, } // blink switch (this.blink) { true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), false => {}, } // reverse switch (this.reverse) { true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), false => {}, } // invisible switch (this.invisible) { true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), false => {}, } // strikethrough switch (this.strikethrough) { true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), false => {}, } } pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void { try this.start(writer); try std.fmt.format(writer, content, args); try this.end(writer); } pub fn value(this: @This(), writer: anytype, content: []const u8) !void { try this.start(writer); _ = try writer.write(content); try this.end(writer); } // TODO: implement helper functions for terminal capabilities: // - links / url display (osc 8) // - show / hide cursor? test { _ = Color; }