diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/style.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/style.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/style.zig | 1078 |
1 files changed, 1078 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/style.zig b/vendor/libghostty-vt/src/terminal/style.zig new file mode 100644 index 00000000..ae9488af --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/style.zig @@ -0,0 +1,1078 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const color = @import("color.zig"); +const sgr = @import("sgr.zig"); +const page = @import("page.zig"); +const size = @import("size.zig"); +const Offset = size.Offset; +const OffsetBuf = size.OffsetBuf; +const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet; + +/// The unique identifier for a style. This is at most the number of cells +/// that can fit into a terminal page. +pub const Id = size.StyleCountInt; + +/// The Id to use for default styling. +pub const default_id: Id = 0; + +/// The style attributes for a cell. +pub const Style = struct { + /// Various colors, all self-explanatory. + fg_color: Color = .none, + bg_color: Color = .none, + underline_color: Color = .none, + + /// On/off attributes that don't require much bit width so we use + /// a packed struct to make this take up significantly less space. + flags: Flags = .{}, + + const Flags = packed struct(u16) { + bold: bool = false, + italic: bool = false, + faint: bool = false, + blink: bool = false, + inverse: bool = false, + invisible: bool = false, + strikethrough: bool = false, + overline: bool = false, + underline: sgr.Attribute.Underline = .none, + _padding: u5 = 0, + }; + + /// The color for an SGR attribute. A color can come from multiple + /// sources so we use this to track the source plus color value so that + /// we can properly react to things like palette changes. + pub const Color = union(Tag) { + none: void, + palette: u8, + rgb: color.RGB, + + const Tag = enum(u8) { + none, + palette, + rgb, + }; + + /// True if the color is equal to another color. + pub fn eql(self: Color, other: Color) bool { + return @as(Tag, self) == @as(Tag, other) and switch (self) { + .none => true, + .palette => self.palette == other.palette, + .rgb => self.rgb == other.rgb, + }; + } + + /// Formatting to make debug logs easier to read + /// by only including non-default attributes. + pub fn format( + self: Color, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: *std.Io.Writer, + ) !void { + _ = fmt; + _ = options; + switch (self) { + .none => { + _ = try writer.write("Color.none"); + }, + .palette => |p| { + _ = try writer.print("Color.palette{{ {} }}", .{p}); + }, + .rgb => |rgb| { + _ = try writer.print("Color.rgb{{ {}, {}, {} }}", .{ rgb.r, rgb.g, rgb.b }); + }, + } + } + }; + + /// True if the style is the default style. + pub inline fn default(self: Style) bool { + return self.eql(.{}); + } + + /// True if the style is equal to another style. + pub fn eql(self: Style, other: Style) bool { + return self.flags == other.flags and + self.fg_color.eql(other.fg_color) and + self.bg_color.eql(other.bg_color) and + self.underline_color.eql(other.underline_color); + } + + /// Returns the bg color for a cell with this style given the cell + /// that has this style and the palette to use. + /// + /// Note that generally if a cell is a color-only cell, it SHOULD + /// only have the default style, but this is meant to work with the + /// default style as well. + pub fn bg( + self: Style, + cell: *const page.Cell, + palette: *const color.Palette, + ) ?color.RGB { + return switch (cell.content_tag) { + .bg_color_palette => palette[cell.content.color_palette], + .bg_color_rgb => rgb: { + const rgb = cell.content.color_rgb; + break :rgb .{ .r = rgb.r, .g = rgb.g, .b = rgb.b }; + }, + + else => switch (self.bg_color) { + .none => null, + .palette => |idx| palette[idx], + .rgb => |rgb| rgb, + }, + }; + } + + /// The color to use for bold text. This avoids a dependency on the + /// config module by using terminal-native color types. + pub const BoldColor = union(enum) { + color: color.RGB, + bright, + }; + + pub const Fg = struct { + /// The default color to use if the style doesn't specify a + /// foreground color and no configuration options override + /// it. + default: color.RGB, + + /// The current color palette. Required to map palette indices to + /// real color values. + palette: *const color.Palette, + + /// If specified, the color to use for bold text. + bold: ?BoldColor = null, + }; + + /// Returns the fg color for a cell with this style given the palette + /// and various configuration options. + pub fn fg( + self: Style, + opts: Fg, + ) color.RGB { + // Note we don't pull the bold check to the top-level here because + // we don't want to duplicate the conditional multiple times since + // certain colors require more checks (e.g. `bold_is_bright`). + + return switch (self.fg_color) { + .none => default: { + if (self.flags.bold) { + if (opts.bold) |bold| switch (bold) { + .bright => {}, + .color => |v| break :default v, + }; + } + + break :default opts.default; + }, + + .palette => |idx| palette: { + if (self.flags.bold) { + if (opts.bold) |_| { + const bright_offset = @intFromEnum(color.Name.bright_black); + if (idx < bright_offset) { + break :palette opts.palette[idx + bright_offset]; + } + } + } + + break :palette opts.palette[idx]; + }, + + .rgb => |rgb| rgb: { + if (self.flags.bold and rgb.eql(opts.default)) { + if (opts.bold) |bold| switch (bold) { + .color => |v| break :rgb v, + .bright => {}, + }; + } + + break :rgb rgb; + }, + }; + } + + /// Returns the underline color for this style. + pub fn underlineColor( + self: Style, + palette: *const color.Palette, + ) ?color.RGB { + return switch (self.underline_color) { + .none => null, + .palette => |idx| palette[idx], + .rgb => |rgb| rgb, + }; + } + + /// Returns a bg-color only cell from this style, if it exists. + pub fn bgCell(self: Style) ?page.Cell { + return switch (self.bg_color) { + .none => null, + .palette => |idx| .{ + .content_tag = .bg_color_palette, + .content = .{ .color_palette = idx }, + }, + .rgb => |rgb| .{ + .content_tag = .bg_color_rgb, + .content = .{ .color_rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + } }, + }, + }; + } + + /// Formatting to make debug logs easier to read + /// by only including non-default attributes. + pub fn format( + self: Style, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: *std.Io.Writer, + ) !void { + _ = fmt; + _ = options; + + const dflt: Style = .{}; + + _ = try writer.write("Style{ "); + + var started = false; + + inline for (std.meta.fields(Style)) |f| { + if (std.mem.eql(u8, f.name, "flags")) { + if (started) { + _ = try writer.write(", "); + } + + _ = try writer.write("flags={ "); + + started = false; + + inline for (std.meta.fields(@TypeOf(self.flags))) |ff| { + const v = @as(ff.type, @field(self.flags, ff.name)); + const d = @as(ff.type, @field(dflt.flags, ff.name)); + if (ff.type == bool) { + if (v) { + if (started) { + _ = try writer.write(", "); + } + _ = try writer.print("{s}", .{ff.name}); + started = true; + } + } else if (!std.meta.eql(v, d)) { + if (started) { + _ = try writer.write(", "); + } + _ = try writer.print( + "{s}={any}", + .{ ff.name, v }, + ); + started = true; + } + } + _ = try writer.write(" }"); + + started = true; + comptime continue; + } + const value = @as(f.type, @field(self, f.name)); + const d_val = @as(f.type, @field(dflt, f.name)); + if (!std.meta.eql(value, d_val)) { + if (started) { + _ = try writer.write(", "); + } + _ = try writer.print( + "{s}={any}", + .{ f.name, value }, + ); + started = true; + } + } + + _ = try writer.write(" }"); + } + + /// Returns a formatter that renders this style as VT sequences, + /// to be used with `{f}`. This always resets the style first `\x1b[0m` + /// since a style is meant to be fully self-contained. + /// + /// For individual styles, this always emits multiple SGR sequences + /// (i.e. an individual `\x1b[<stuff>m` for each attribute) rather than + /// trying to combine them into a single sequence. We do this because + /// terminals have varying levels of support for combined sequences + /// especially with mixed separators (e.g. `:` vs `;`). + pub fn formatterVt(self: *const Style) VTFormatter { + return .{ .style = self }; + } + + /// Returns a formatter that renders this style as inline CSS properties, + /// to be used with `{f}`. The output is a valid CSS style string suitable + /// for use in a `style` attribute (e.g., "color: rgb(255, 0, 0); font-weight: bold;"). + /// + /// Palette colors are emitted as CSS variables like `var(--vt-palette-N)`. + pub fn formatterHtml(self: *const Style) HtmlFormatter { + return .{ .style = self }; + } + + const VTFormatter = struct { + style: *const Style, + + /// If set, palette colors will be emitted as RGB values instead of + /// palette indices. This is useful when you want to capture the + /// exact colors at formatting time rather than relying on the + /// terminal's palette. + palette: ?*const color.Palette = null, + + pub fn format( + self: VTFormatter, + writer: *std.Io.Writer, + ) !void { + // Always reset the style. Styles are fully self-contained. + // Even if this style is empty, then that means we want to go + // back to the default. + try writer.writeAll("\x1b[0m"); + + // Our flags + if (self.style.flags.bold) try writer.writeAll("\x1b[1m"); + if (self.style.flags.faint) try writer.writeAll("\x1b[2m"); + if (self.style.flags.italic) try writer.writeAll("\x1b[3m"); + if (self.style.flags.blink) try writer.writeAll("\x1b[5m"); + if (self.style.flags.inverse) try writer.writeAll("\x1b[7m"); + if (self.style.flags.invisible) try writer.writeAll("\x1b[8m"); + if (self.style.flags.strikethrough) try writer.writeAll("\x1b[9m"); + if (self.style.flags.overline) try writer.writeAll("\x1b[53m"); + switch (self.style.flags.underline) { + .none => {}, + .single => try writer.writeAll("\x1b[4m"), + .double => try writer.writeAll("\x1b[4:2m"), + .curly => try writer.writeAll("\x1b[4:3m"), + .dotted => try writer.writeAll("\x1b[4:4m"), + .dashed => try writer.writeAll("\x1b[4:5m"), + } + + // Various RGB colors. + try self.formatColor(writer, 38, self.style.fg_color); + try self.formatColor(writer, 48, self.style.bg_color); + try self.formatColor(writer, 58, self.style.underline_color); + } + + fn formatColor( + self: VTFormatter, + writer: *std.Io.Writer, + prefix: u8, + value: Color, + ) !void { + switch (value) { + .none => {}, + .palette => |idx| { + if (self.palette) |p| { + const rgb = p[idx]; + try writer.print( + "\x1b[{d};2;{d};{d};{d}m", + .{ prefix, rgb.r, rgb.g, rgb.b }, + ); + } else { + try writer.print( + "\x1b[{d};5;{d}m", + .{ prefix, idx }, + ); + } + }, + .rgb => |rgb| try writer.print( + "\x1b[{d};2;{d};{d};{d}m", + .{ prefix, rgb.r, rgb.g, rgb.b }, + ), + } + } + }; + + const HtmlFormatter = struct { + style: *const Style, + + /// If set, palette colors will be emitted as RGB values instead of + /// CSS variables. This is useful when you want to capture the exact + /// colors at formatting time rather than relying on CSS variables. + palette: ?*const color.Palette = null, + + pub fn format( + self: HtmlFormatter, + writer: *std.Io.Writer, + ) !void { + // Colors + try self.formatColor(writer, "color", self.style.fg_color); + try self.formatColor(writer, "background-color", self.style.bg_color); + try self.formatColor(writer, "text-decoration-color", self.style.underline_color); + + // Text decoration line + const has_line = self.style.flags.underline != .none or + self.style.flags.strikethrough or + self.style.flags.overline or + self.style.flags.blink; + if (has_line) { + try writer.writeAll("text-decoration-line:"); + if (self.style.flags.underline != .none) try writer.writeAll(" underline"); + if (self.style.flags.strikethrough) try writer.writeAll(" line-through"); + if (self.style.flags.overline) try writer.writeAll(" overline"); + if (self.style.flags.blink) try writer.writeAll(" blink"); + try writer.writeAll(";"); + } + + // Text decoration style + switch (self.style.flags.underline) { + .none => {}, + .single => try writer.writeAll("text-decoration-style: solid;"), + .double => try writer.writeAll("text-decoration-style: double;"), + .curly => try writer.writeAll("text-decoration-style: wavy;"), + .dotted => try writer.writeAll("text-decoration-style: dotted;"), + .dashed => try writer.writeAll("text-decoration-style: dashed;"), + } + + if (self.style.flags.bold) try writer.writeAll("font-weight: bold;"); + if (self.style.flags.italic) try writer.writeAll("font-style: italic;"); + if (self.style.flags.faint) try writer.writeAll("opacity: 0.5;"); + if (self.style.flags.invisible) try writer.writeAll("visibility: hidden;"); + if (self.style.flags.inverse) try writer.writeAll("filter: invert(100%);"); + } + + fn formatColor( + self: HtmlFormatter, + writer: *std.Io.Writer, + property: []const u8, + c: Color, + ) !void { + switch (c) { + .none => {}, + .palette => |idx| { + if (self.palette) |p| { + const rgb = p[idx]; + try writer.print("{s}: rgb({d}, {d}, {d});", .{ property, rgb.r, rgb.g, rgb.b }); + } else { + try writer.print("{s}: var(--vt-palette-{d});", .{ property, idx }); + } + }, + .rgb => |rgb| try writer.print("{s}: rgb({d}, {d}, {d});", .{ property, rgb.r, rgb.g, rgb.b }), + } + } + }; + + /// `PackedStyle` represents the same data as `Style` but without padding, + /// which is necessary for hashing via re-interpretation of the underlying + /// bytes. + /// + /// `Style` is still preferred for everything else as it has type-safety + /// when using the `Color` tagged union. + /// + /// Empirical testing shows that storing all of the tags first and then the + /// data provides a better layout for serializing into and is faster on + /// benchmarks. + const PackedStyle = packed struct(u128) { + tags: packed struct { + fg: Color.Tag, + bg: Color.Tag, + underline: Color.Tag, + }, + data: packed struct { + fg: Data, + bg: Data, + underline: Data, + }, + flags: Flags, + _padding: u16 = 0, + + /// After https://github.com/ziglang/zig/issues/19754 is implemented, + /// it will be an compiler-error to have packed union fields of + /// differing size. + /// + /// For now we just need to be careful not to accidentally introduce + /// padding. + const Data = packed union { + none: u24, + palette: packed struct(u24) { + idx: u8, + _padding: u16 = 0, + }, + rgb: color.RGB, + + fn fromColor(c: Color) Data { + return switch (c) { + inline else => |v, t| @unionInit( + Data, + @tagName(t), + switch (t) { + .none => 0, + .palette => .{ .idx = v }, + .rgb => v, + }, + ), + }; + } + }; + + inline fn fromStyle(style: Style) PackedStyle { + return .{ + .tags = .{ + .fg = @as(Color.Tag, style.fg_color), + .bg = @as(Color.Tag, style.bg_color), + .underline = @as(Color.Tag, style.underline_color), + }, + .data = .{ + .fg = .fromColor(style.fg_color), + .bg = .fromColor(style.bg_color), + .underline = .fromColor(style.underline_color), + }, + .flags = style.flags, + }; + } + }; + + pub fn hash(self: *const Style) u64 { + // We pack the style in to 128 bits, fold it to 64 bits, + // then use std.hash.int to make it sufficiently uniform. + const packed_style: PackedStyle = .fromStyle(self.*); + const wide: [2]u64 = @bitCast(packed_style); + return @call(.always_inline, std.hash.int, .{wide[0] ^ wide[1]}); + } + + comptime { + assert(@sizeOf(PackedStyle) == 16); + assert(std.meta.hasUniqueRepresentation(PackedStyle)); + for (@typeInfo(PackedStyle.Data).@"union".fields) |field| { + assert(@bitSizeOf(field.type) == @bitSizeOf(PackedStyle.Data)); + } + } +}; + +pub const Set = RefCountedSet( + Style, + Id, + size.CellCountInt, + struct { + pub fn hash(self: *const @This(), style: Style) u64 { + _ = self; + return style.hash(); + } + + pub fn eql(self: *const @This(), a: Style, b: Style) bool { + _ = self; + return a.eql(b); + } + }, +); + +test "Style VT formatting empty" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{}; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m", builder.writer.buffered()); +} + +test "Style VT formatting bold" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .bold = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[1m", builder.writer.buffered()); +} + +test "Style VT formatting faint" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .faint = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[2m", builder.writer.buffered()); +} + +test "Style VT formatting italic" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .italic = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[3m", builder.writer.buffered()); +} + +test "Style VT formatting blink" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .blink = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[5m", builder.writer.buffered()); +} + +test "Style VT formatting inverse" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .inverse = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[7m", builder.writer.buffered()); +} + +test "Style VT formatting invisible" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .invisible = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[8m", builder.writer.buffered()); +} + +test "Style VT formatting strikethrough" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .strikethrough = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[9m", builder.writer.buffered()); +} + +test "Style VT formatting overline" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .overline = true } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[53m", builder.writer.buffered()); +} + +test "Style VT formatting underline single" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .single } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[4m", builder.writer.buffered()); +} + +test "Style VT formatting underline double" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .double } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[4:2m", builder.writer.buffered()); +} + +test "Style VT formatting underline curly" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .curly } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[4:3m", builder.writer.buffered()); +} + +test "Style VT formatting underline dotted" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .dotted } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[4:4m", builder.writer.buffered()); +} + +test "Style VT formatting underline dashed" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .dashed } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[4:5m", builder.writer.buffered()); +} + +test "Style VT formatting fg palette" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .fg_color = .{ .palette = 42 } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[38;5;42m", builder.writer.buffered()); +} + +test "Style VT formatting fg rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .fg_color = .{ .rgb = .{ .r = 255, .g = 128, .b = 64 } } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[38;2;255;128;64m", builder.writer.buffered()); +} + +test "Style VT formatting bg palette" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .bg_color = .{ .palette = 7 } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[48;5;7m", builder.writer.buffered()); +} + +test "Style VT formatting bg rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .bg_color = .{ .rgb = .{ .r = 32, .g = 64, .b = 96 } } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[48;2;32;64;96m", builder.writer.buffered()); +} + +test "Style VT formatting underline_color palette" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .underline_color = .{ .palette = 15 } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[58;5;15m", builder.writer.buffered()); +} + +test "Style VT formatting underline_color rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .underline_color = .{ .rgb = .{ .r = 200, .g = 100, .b = 50 } } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[58;2;200;100;50m", builder.writer.buffered()); +} + +test "Style VT formatting multiple flags" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .bold = true, .italic = true, .underline = .single } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings("\x1b[0m\x1b[1m\x1b[3m\x1b[4m", builder.writer.buffered()); +} + +test "Style VT formatting all flags" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ + .bold = true, + .faint = true, + .italic = true, + .blink = true, + .inverse = true, + .invisible = true, + .strikethrough = true, + .overline = true, + .underline = .curly, + } }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings( + "\x1b[0m\x1b[1m\x1b[2m\x1b[3m\x1b[5m\x1b[7m\x1b[8m\x1b[9m\x1b[53m\x1b[4:3m", + builder.writer.buffered(), + ); +} + +test "Style VT formatting combined colors and flags" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ + .fg_color = .{ .rgb = .{ .r = 255, .g = 0, .b = 0 } }, + .bg_color = .{ .palette = 8 }, + .underline_color = .{ .rgb = .{ .r = 0, .g = 255, .b = 0 } }, + .flags = .{ .bold = true, .italic = true, .underline = .double }, + }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings( + "\x1b[0m\x1b[1m\x1b[3m\x1b[4:2m\x1b[38;2;255;0;0m\x1b[48;5;8m\x1b[58;2;0;255;0m", + builder.writer.buffered(), + ); +} + +test "Style VT formatting all colors rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ + .fg_color = .{ .rgb = .{ .r = 10, .g = 20, .b = 30 } }, + .bg_color = .{ .rgb = .{ .r = 40, .g = 50, .b = 60 } }, + .underline_color = .{ .rgb = .{ .r = 70, .g = 80, .b = 90 } }, + }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings( + "\x1b[0m\x1b[38;2;10;20;30m\x1b[48;2;40;50;60m\x1b[58;2;70;80;90m", + builder.writer.buffered(), + ); +} + +test "Style VT formatting all colors palette" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ + .fg_color = .{ .palette = 1 }, + .bg_color = .{ .palette = 2 }, + .underline_color = .{ .palette = 3 }, + }; + try builder.writer.print("{f}", .{style.formatterVt()}); + try testing.expectEqualStrings( + "\x1b[0m\x1b[38;5;1m\x1b[48;5;2m\x1b[58;5;3m", + builder.writer.buffered(), + ); +} + +test "Style VT formatting palette with palette set emits rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .fg_color = .{ .palette = 1 } }; + var formatter = style.formatterVt(); + formatter.palette = &color.default; + try builder.writer.print("{f}", .{formatter}); + try testing.expectEqualStrings("\x1b[0m\x1b[38;2;204;102;102m", builder.writer.buffered()); +} + +test "Style VT formatting all palette colors with palette set" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ + .fg_color = .{ .palette = 1 }, + .bg_color = .{ .palette = 2 }, + .underline_color = .{ .palette = 3 }, + }; + var formatter = style.formatterVt(); + formatter.palette = &color.default; + try builder.writer.print("{f}", .{formatter}); + try testing.expectEqualStrings( + "\x1b[0m\x1b[38;2;204;102;102m\x1b[48;2;181;189;104m\x1b[58;2;240;198;116m", + builder.writer.buffered(), + ); +} + +test "Set basic usage" { + const testing = std.testing; + const alloc = testing.allocator; + const layout: Set.Layout = .init(16); + const buf = try alloc.alignedAlloc(u8, Set.base_align, layout.total_size); + defer alloc.free(buf); + + const style: Style = .{ .flags = .{ .bold = true } }; + const style2: Style = .{ .flags = .{ .italic = true } }; + + var set = Set.init(.init(buf), layout, .{}); + + // Add style + const id = try set.add(buf, style); + try testing.expect(id > 0); + + // Second add should return the same metadata. + { + const id2 = try set.add(buf, style); + try testing.expectEqual(id, id2); + } + + // Look it up + { + const v = set.get(buf, id); + try testing.expect(v.flags.bold); + + const v2 = set.get(buf, id); + try testing.expectEqual(v, v2); + } + + // Add a second style + const id2 = try set.add(buf, style2); + + // Look it up + { + const v = set.get(buf, id2); + try testing.expect(v.flags.italic); + } + + // Ref count + try testing.expect(set.refCount(buf, id) == 2); + try testing.expect(set.refCount(buf, id2) == 1); + + // Release + set.release(buf, id); + try testing.expect(set.refCount(buf, id) == 1); + set.release(buf, id2); + try testing.expect(set.refCount(buf, id2) == 0); + + // We added the first one twice, so + set.release(buf, id); + try testing.expect(set.refCount(buf, id) == 0); +} + +test "Set capacities" { + // We want to support at least this many styles without overflowing. + _ = Set.Layout.init(16384); +} + +test "Style HTML formatting basic bold" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .bold = true } }; + try builder.writer.print("{f}", .{style.formatterHtml()}); + try testing.expectEqualStrings("font-weight: bold;", builder.writer.buffered()); +} + +test "Style HTML formatting fg color rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .fg_color = .{ .rgb = .{ .r = 255, .g = 128, .b = 64 } } }; + try builder.writer.print("{f}", .{style.formatterHtml()}); + try testing.expectEqualStrings("color: rgb(255, 128, 64);", builder.writer.buffered()); +} + +test "Style HTML formatting bg color palette" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .bg_color = .{ .palette = 7 } }; + try builder.writer.print("{f}", .{style.formatterHtml()}); + try testing.expectEqualStrings("background-color: var(--vt-palette-7);", builder.writer.buffered()); +} + +test "Style HTML formatting combined colors and flags" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ + .fg_color = .{ .rgb = .{ .r = 255, .g = 0, .b = 0 } }, + .bg_color = .{ .rgb = .{ .r = 0, .g = 0, .b = 255 } }, + .flags = .{ .bold = true, .italic = true }, + }; + try builder.writer.print("{f}", .{style.formatterHtml()}); + const result = builder.writer.buffered(); + try testing.expect(std.mem.indexOf(u8, result, "color: rgb(255, 0, 0);") != null); + try testing.expect(std.mem.indexOf(u8, result, "background-color: rgb(0, 0, 255);") != null); + try testing.expect(std.mem.indexOf(u8, result, "font-weight: bold;") != null); + try testing.expect(std.mem.indexOf(u8, result, "font-style: italic;") != null); +} + +test "Style HTML formatting single decoration line" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .single } }; + try builder.writer.print("{f}", .{style.formatterHtml()}); + const result = builder.writer.buffered(); + try testing.expect(std.mem.indexOf(u8, result, "text-decoration-line: underline;") != null); + try testing.expect(std.mem.indexOf(u8, result, "text-decoration-style: solid;") != null); +} + +test "Style HTML formatting multiple decoration lines" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .flags = .{ .underline = .curly, .strikethrough = true, .overline = true } }; + try builder.writer.print("{f}", .{style.formatterHtml()}); + const result = builder.writer.buffered(); + try testing.expect(std.mem.indexOf(u8, result, "text-decoration-line: underline line-through overline;") != null); + try testing.expect(std.mem.indexOf(u8, result, "text-decoration-style: wavy;") != null); +} + +test "Style HTML formatting palette with palette set emits rgb" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ .bg_color = .{ .palette = 7 } }; + var formatter = style.formatterHtml(); + formatter.palette = &color.default; + try builder.writer.print("{f}", .{formatter}); + try testing.expectEqualStrings("background-color: rgb(197, 200, 198);", builder.writer.buffered()); +} + +test "Style HTML formatting all palette colors with palette set" { + const testing = std.testing; + const alloc = testing.allocator; + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var style: Style = .{ + .fg_color = .{ .palette = 1 }, + .bg_color = .{ .palette = 2 }, + .underline_color = .{ .palette = 3 }, + }; + var formatter = style.formatterHtml(); + formatter.palette = &color.default; + try builder.writer.print("{f}", .{formatter}); + try testing.expectEqualStrings( + "color: rgb(204, 102, 102);background-color: rgb(181, 189, 104);text-decoration-color: rgb(240, 198, 116);", + builder.writer.buffered(), + ); +} |
