Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 42s
306 lines
10 KiB
Zig
306 lines
10 KiB
Zig
//! 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;
|
|
}
|