Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413//! The style of a cell. Nil colors mean "keep the terminal's current color";//! the zero style emits nothing.const std = @import("std");const Color = @import("color.zig").Color;const styled = @import("styled.zig");const Profile = @import("root.zig").Profile;
const Writer = std.Io.Writer;const t = std.testing;
const Style = @This();
fg: ?Color = null,bg: ?Color = null,underline_color: ?Color = null,underline: Underline = .none,attrs: Attrs = .{},
pub fn equal(self: Style, other: Style) bool { return self.attrs == other.attrs and self.underline == other.underline and std.meta.eql(self.fg, other.fg) and std.meta.eql(self.bg, other.bg) and std.meta.eql(self.underline_color, other.underline_color);}
pub fn isZero(self: Style) bool { return self.fg == null and self.bg == null and self.underline_color == null and self.underline == .none and !self.attrs.any();}
/// Writes the full SGR open sequence for this style (skips absent/// colors). Returns bytes written.pub fn writeSeq(self: Style, writer: *Writer) Writer.Error!usize { var count: usize = 0; const a = self.attrs; if (a.bold) count += try writer.write("\x1b[1m"); if (a.faint) count += try writer.write("\x1b[2m"); if (a.italic) count += try writer.write("\x1b[3m"); count += try writeUnderlineCode(writer, self.underline); if (a.blink) count += try writer.write("\x1b[5m"); if (a.rapid_blink) count += try writer.write("\x1b[6m"); if (a.reverse) count += try writer.write("\x1b[7m"); if (a.conceal) count += try writer.write("\x1b[8m"); if (a.strikethrough) count += try writer.write("\x1b[9m"); if (self.fg) |c| count += try c.writeSgr(writer, .fg); if (self.bg) |c| count += try c.writeSgr(writer, .bg); if (self.underline_color) |c| count += try c.writeSgr(writer, .underline); return count;}
/// Writes the SGR sequence transitioning the terminal from `from` to/// `to`, mirroring `StyleDiff` in uv `cell.go` (colors first, then/// attribute resets, then sets, special underline last). Returns bytes/// written.pub fn writeDiff(self: Style, other: Style, writer: *Writer) Writer.Error!usize { if (self.equal(other)) return 0; if (other.isZero()) { _ = try writer.write("\x1b[m"); return 3; }
var count: usize = 0; const f = self; const t_ = other;
if (!std.meta.eql(f.fg, t_.fg)) { if (t_.fg) |c| { count += try c.writeSgr(writer, .fg); } else { count += try writer.write("\x1b[39m"); } } if (!std.meta.eql(f.bg, t_.bg)) { if (t_.bg) |c| { count += try c.writeSgr(writer, .bg); } else { count += try writer.write("\x1b[49m"); } } if (!std.meta.eql(f.underline_color, t_.underline_color)) { if (t_.underline_color) |c| { count += try c.writeSgr(writer, .underline); } else { count += try writer.write("\x1b[59m"); } }
const from_bold = f.attrs.bold; const from_faint = f.attrs.faint; const from_italic = f.attrs.italic; const from_underline = f.underline != .none; const from_blink = f.attrs.blink; const from_rapid_b = f.attrs.rapid_blink; const from_reverse = f.attrs.reverse; const from_conceal = f.attrs.conceal; const from_strike = f.attrs.strikethrough;
const to_bold = t_.attrs.bold; const to_faint = t_.attrs.faint; const to_italic = t_.attrs.italic; const to_underline = t_.underline != .none; const to_blink = t_.attrs.blink; const to_rapid_b = t_.attrs.rapid_blink; const to_reverse = t_.attrs.reverse; const to_conceal = t_.attrs.conceal; const to_strike = t_.attrs.strikethrough;
var bold_changed = from_bold != to_bold; var faint_changed = from_faint != to_faint; if (bold_changed or faint_changed) { if ((from_bold and !to_bold) or (from_faint and !to_faint)) { count += try writer.write("\x1b[22m"); bold_changed = true; faint_changed = true; } }
const italic_changed = from_italic != to_italic; if (italic_changed and !to_italic) count += try writer.write("\x1b[23m");
const underline_changed = from_underline != to_underline or f.underline != t_.underline; if (underline_changed and !to_underline) count += try writer.write("\x1b[24m");
var blink_changed = from_blink != to_blink; var rapid_changed = from_rapid_b != to_rapid_b; if (blink_changed or rapid_changed) { if ((from_blink and !to_blink) or (from_rapid_b and !to_rapid_b)) { count += try writer.write("\x1b[25m"); blink_changed = true; rapid_changed = true; } }
const reverse_changed = from_reverse != to_reverse; if (reverse_changed and !to_reverse) count += try writer.write("\x1b[27m");
const conceal_changed = from_conceal != to_conceal; if (conceal_changed and !to_conceal) count += try writer.write("\x1b[28m");
const strike_changed = from_strike != to_strike; if (strike_changed and !to_strike) count += try writer.write("\x1b[29m");
if (bold_changed and to_bold) count += try writer.write("\x1b[1m"); if (faint_changed and to_faint) count += try writer.write("\x1b[2m"); if (italic_changed and to_italic) count += try writer.write("\x1b[3m"); if (underline_changed and to_underline and t_.underline == .single) count += try writer.write("\x1b[4m"); if (blink_changed and to_blink) count += try writer.write("\x1b[5m"); if (rapid_changed and to_rapid_b) count += try writer.write("\x1b[6m"); if (reverse_changed and to_reverse) count += try writer.write("\x1b[7m"); if (conceal_changed and to_conceal) count += try writer.write("\x1b[8m"); if (strike_changed and to_strike) count += try writer.write("\x1b[9m");
if (underline_changed and to_underline and t_.underline != .single) count += try writeUnderlineCode(writer, t_.underline);
return count;}
fn writeUnderlineCode(writer: *Writer, u: Underline) Writer.Error!usize { return switch (u) { .none => 0, .single => writer.write("\x1b[4m"), .double => writer.write("\x1b[4:2m"), .curly => writer.write("\x1b[4:3m"), .dotted => writer.write("\x1b[4:4m"), .dashed => writer.write("\x1b[4:5m"), };}
/// Reads a Select Graphic Rendition (SGR) escape sequence into `pen`,/// mirroring uv `ReadStyle` (styled.go).pub fn read(pen: *Style, params: []const u32) void { if (params.len == 0) { pen.* = .{}; return; } var i: usize = 0; while (i < params.len) : (i += 1) { const pv = styled.paramValue(params[i], 0); switch (pv) { 0 => pen.* = .{}, // Reset 1 => pen.attrs.bold = true, 2 => pen.attrs.faint = true, 3 => pen.attrs.italic = true, 4 => { const next = styled.paramAt(params, i + 1, 0); if (styled.hasMore(params[i]) and next.ok) { // Colon sub-parameters: 4:0..4:5 switch (next.value) { 0 => pen.underline = .none, 1 => pen.underline = .single, 2 => pen.underline = .double, 3 => pen.underline = .curly, 4 => pen.underline = .dotted, 5 => pen.underline = .dashed, else => {}, } i += 1; } else { // Single underline pen.underline = .single; } }, 5 => pen.attrs.blink = true, 6 => pen.attrs.rapid_blink = true, 7 => pen.attrs.reverse = true, 8 => pen.attrs.conceal = true, 9 => pen.attrs.strikethrough = true, 22 => pen.attrs.bold = false, 23 => pen.attrs.italic = false, 24 => pen.underline = .none, 25 => pen.attrs.blink = false, 27 => pen.attrs.reverse = false, 28 => pen.attrs.conceal = false, 29 => pen.attrs.strikethrough = false, 30...37 => pen.fg = .{ .ansi16 = @intCast(pv - 30) }, 38 => { const n = styled.readStyleColor(params[i..], &pen.fg); if (n > 0) i += n - 1; }, 39 => pen.fg = null, 40...47 => pen.bg = .{ .ansi16 = @intCast(pv - 40) }, 48 => { const n = styled.readStyleColor(params[i..], &pen.bg); if (n > 0) i += n - 1; }, 49 => pen.bg = null, 58 => { const n = styled.readStyleColor(params[i..], &pen.underline_color); if (n > 0) i += n - 1; }, 59 => pen.underline_color = null, 90...97 => pen.fg = .{ .ansi16 = @intCast(pv - 90 + 8) }, 100...107 => pen.bg = .{ .ansi16 = @intCast(pv - 100 + 8) }, else => {}, } }}
/// Mirrors uv `ConvertStyle` (cell.go): down-samples a style's colors to the/// profile before the pen diff. The original style is kept elsewhere so a/// later profile change can re-render. TrueColor passes through unchanged,/// ASCII drops the colors, and NoTTY zeroes the whole style.pub fn convert(style: Style, p: Profile) Style { var s = style; switch (p) { .true_color => return s, .ansi, .ansi256 => {}, .ascii => { s.fg = null; s.bg = null; s.underline_color = null; }, .no_tty => return .{}, else => {}, // unknown: the conversion loop below drops colors } if (s.fg) |c| s.fg = c.convert(p); if (s.bg) |c| s.bg = c.convert(p); if (s.underline_color) |c| s.underline_color = c.convert(p); return s;}
/// Underline style. Portable across SGR and the colon-form `4:x` variants./// Public because other modules take it as a parameter type (e.g./// `Context.setUnderlineStyle`).pub const Underline = enum { none, single, double, curly, dotted, dashed,};
/// Text attributes, stored as a bitmask mirroring `uv.Attr*`.const Attrs = packed struct { bold: bool = false, faint: bool = false, italic: bool = false, blink: bool = false, rapid_blink: bool = false, reverse: bool = false, conceal: bool = false, strikethrough: bool = false,
pub fn any(a: Attrs) bool { return @as(u8, @bitCast(a)) != 0; }};
test "style diff underline styles" { var buf: [256]u8 = undefined; var w: Writer = .fixed(&buf); const from: Style = .{}; const to: Style = .{ .underline = .double }; const n = try from.writeDiff(to, &w); try t.expectEqualStrings("\x1b[4:2m", buf[0..n]); // double -> none: reset then off w = .fixed(&buf); const n2 = try to.writeDiff(.{}, &w); try t.expectEqualStrings("\x1b[m", buf[0..n2]);}
test "style diff colors" { var buf: [256]u8 = undefined; var w: Writer = .fixed(&buf); const from: Style = .{ .fg = .fromAnsi(1) }; const to: Style = .{ .fg = .fromAnsi(2) }; const n = try from.writeDiff(to, &w); try t.expectEqualStrings("\x1b[32m", buf[0..n]); // to nil fg -> zero style collapses to reset w = .fixed(&buf); const n2 = try from.writeDiff(.{}, &w); try t.expectEqualStrings("\x1b[m", buf[0..n2]);}
test "style diff basic transitions" { var buf: [256]u8 = undefined; var w: Writer = .fixed(&buf);
// bold off -> on: just set const from: Style = .{}; const to: Style = .{ .attrs = .{ .bold = true } }; const n = try from.writeDiff(to, &w); try t.expectEqualStrings("\x1b[1m", buf[0..n]);
// same style: nothing w = .fixed(&buf); const n0 = try to.writeDiff(to, &w); try t.expect(n0 == 0); // try testing.expectEqual(@as(usize, 0), n0);
// bold/italic on -> to zero style: collapses to a single reset const from2: Style = .{ .attrs = .{ .bold = true, .italic = true } }; const to2: Style = .{}; w = .fixed(&buf); const n2 = try from2.writeDiff(to2, &w); try t.expectEqualStrings("\x1b[m", buf[0..n2]);
// to zero style collapses to reset (even from distinct) const from3: Style = .{ .fg = .fromRgb(1, 2, 3) }; w = .fixed(&buf); const n3 = try from3.writeDiff(.{}, &w); try t.expectEqualStrings("\x1b[m", buf[0..n3]);}
test "style equality and zero" { const style: Style = .{}; try t.expect(style.isZero());
var w: [256]u8 = undefined; var s1: Style = .{ .attrs = .{ .bold = true } }; try t.expect(!s1.isZero()); var fw: Writer = .fixed(&w); const n = try s1.writeSeq(&fw); try t.expectEqualStrings("\x1b[1m", w[0..n]); try t.expect(s1.equal(.{ .attrs = .{ .bold = true } }));
s1 = .{ .fg = .{ .ansi16 = 1 } }; fw = .fixed(&w); const n2 = try s1.writeSeq(&fw); try t.expectEqualStrings("\x1b[31m", w[0..n2]);}
test "fuzz style read" { try t.fuzz({}, fuzzStyleRead, .{ .corpus = &.{ "", "1;31", "4:3", "38;2;1;2;3", "38;5;196", "48;2;255;255;255;58;5;1", "0;0;0", "22;27;30;90;107", }, });}
fn fuzzStyleRead(_: void, smith: *t.Smith) anyerror!void { var params: [32]u32 = .{0} ** 32; const n = smith.index(params.len + 1); for (params[0..n]) |*p| { // SGR parameters are 0..255 in practice; the high bit is the colon // sub-parameter (has-more) flag consumed by `readStyleColor`. p.* = smith.valueRangeLessThan(u32, 0, 256) | (@as(u32, if (smith.value(bool)) 0x8000_0000 else 0)); }
var pen: Style = .{}; pen.read(params[0..n]); var pen2: Style = .{}; pen2.read(params[0..n]);
// Round-trip the parsed pen back through the writers. var buf: [256]u8 = undefined; var w: Writer = .fixed(&buf); _ = pen.writeSeq(&w) catch {}; w = .fixed(&buf); _ = pen.writeDiff(pen2, &w) catch {};
_ = Style.convert(pen, .no_tty); _ = Style.convert(pen, .ascii); _ = Style.convert(pen, .ansi); _ = Style.convert(pen, .ansi256); _ = Style.convert(pen, .true_color); _ = Style.convert(pen, .unknown); _ = pen.equal(pen2); _ = pen.isZero();}