terminal: formatter that can emit VT sequences (#9374)
This adds a new formatter that can be used with standard Zig `{f}`
formatting that emits any portion of the terminal screen as VT
sequences. In addition to simply styling, this can emit the entire
terminal/screen state such as cursor positions, active style, terminal
modes, etc.
To do this, I've extracted all formatting to a dedicated `formatter`
package within `terminal`. This handles all formatting types (currently
plaintext and VT formatting, but can imagine things like HTML in the
future). Presently, we have "formatting" split out across a variety of
places in Terminal, Screen, PageList, and Page. I didn't remove this
code yet but I intend to unify it all on formatter in the future.
This also doesn't expose this functionality in any user-facing way yet.
This PR just adds it to the ghostty-vt Zig module and unit tests it.
Ghostty app changes will come later.
**This also improves the readonly stream** to handle OSC color
operations for _setting_ but it doesn't emit any responses of course,
since its readonly.
pull/9391/head
parent
d40321a8d8
commit
17f2dc59fa
File diff suppressed because it is too large
Load Diff
|
|
@ -13,6 +13,7 @@ pub const osc = @import("osc.zig");
|
|||
pub const point = @import("point.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const device_status = @import("device_status.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const kitty = @import("kitty.zig");
|
||||
pub const modes = @import("modes.zig");
|
||||
pub const page = @import("page.zig");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ const color = @import("color.zig");
|
|||
const hyperlink = @import("hyperlink.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
const style = @import("style.zig");
|
||||
const stylepkg = @import("style.zig");
|
||||
const Style = stylepkg.Style;
|
||||
const StyleId = stylepkg.Id;
|
||||
const StyleSet = stylepkg.Set;
|
||||
const size = @import("size.zig");
|
||||
const getOffset = size.getOffset;
|
||||
const Offset = size.Offset;
|
||||
|
|
@ -86,7 +89,7 @@ pub const Page = struct {
|
|||
assert(std.heap.page_size_min % @max(
|
||||
@alignOf(Row),
|
||||
@alignOf(Cell),
|
||||
style.Set.base_align.toByteUnits(),
|
||||
StyleSet.base_align.toByteUnits(),
|
||||
) == 0);
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +127,7 @@ pub const Page = struct {
|
|||
grapheme_map: GraphemeMap,
|
||||
|
||||
/// The available set of styles in use on this page.
|
||||
styles: style.Set,
|
||||
styles: StyleSet,
|
||||
|
||||
/// The structures used for tracking hyperlinks within the page.
|
||||
/// The map maps cell offsets to hyperlink IDs and the IDs are in
|
||||
|
|
@ -236,7 +239,7 @@ pub const Page = struct {
|
|||
.rows = rows,
|
||||
.cells = cells,
|
||||
.dirty = buf.member(usize, l.dirty_start),
|
||||
.styles = style.Set.init(
|
||||
.styles = StyleSet.init(
|
||||
buf.add(l.styles_start),
|
||||
l.styles_layout,
|
||||
.{},
|
||||
|
|
@ -372,7 +375,7 @@ pub const Page = struct {
|
|||
const alloc = arena.allocator();
|
||||
|
||||
var graphemes_seen: usize = 0;
|
||||
var styles_seen = std.AutoHashMap(style.Id, usize).init(alloc);
|
||||
var styles_seen = std.AutoHashMap(StyleId, usize).init(alloc);
|
||||
defer styles_seen.deinit();
|
||||
var hyperlinks_seen = std.AutoHashMap(hyperlink.Id, usize).init(alloc);
|
||||
defer hyperlinks_seen.deinit();
|
||||
|
|
@ -409,7 +412,7 @@ pub const Page = struct {
|
|||
}
|
||||
}
|
||||
|
||||
if (cell.style_id != style.default_id) {
|
||||
if (cell.style_id != stylepkg.default_id) {
|
||||
// If a cell has a style, it must be present in the styles
|
||||
// set. Accessing it with `get` asserts that.
|
||||
_ = self.styles.get(
|
||||
|
|
@ -767,7 +770,7 @@ pub const Page = struct {
|
|||
for (other_cells) |cell| {
|
||||
assert(!cell.hasGrapheme());
|
||||
assert(!cell.hyperlink);
|
||||
assert(cell.style_id == style.default_id);
|
||||
assert(cell.style_id == stylepkg.default_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -782,7 +785,7 @@ pub const Page = struct {
|
|||
// hit an integrity check if we have to return an error because
|
||||
// the page can't fit the new memory.
|
||||
dst_cell.hyperlink = false;
|
||||
dst_cell.style_id = style.default_id;
|
||||
dst_cell.style_id = stylepkg.default_id;
|
||||
if (dst_cell.content_tag == .codepoint_grapheme) {
|
||||
dst_cell.content_tag = .codepoint;
|
||||
}
|
||||
|
|
@ -791,7 +794,7 @@ pub const Page = struct {
|
|||
// To prevent integrity checks flipping. This will
|
||||
// get fixed up when we check the style id below.
|
||||
if (build_options.slow_runtime_safety) {
|
||||
dst_cell.style_id = style.default_id;
|
||||
dst_cell.style_id = stylepkg.default_id;
|
||||
}
|
||||
|
||||
// Copy the grapheme codepoints
|
||||
|
|
@ -867,7 +870,7 @@ pub const Page = struct {
|
|||
|
||||
try self.setHyperlink(dst_row, dst_cell, dst_id);
|
||||
}
|
||||
if (src_cell.style_id != style.default_id) style: {
|
||||
if (src_cell.style_id != stylepkg.default_id) style: {
|
||||
dst_row.styled = true;
|
||||
|
||||
if (other == self) {
|
||||
|
|
@ -995,7 +998,7 @@ pub const Page = struct {
|
|||
|
||||
// The destination row has styles if any of the cells are styled
|
||||
if (!dst_row.styled) dst_row.styled = styled: for (dst_cells) |c| {
|
||||
if (c.style_id != style.default_id) break :styled true;
|
||||
if (c.style_id != stylepkg.default_id) break :styled true;
|
||||
} else false;
|
||||
|
||||
// Clear our source row now that the copy is complete. We can NOT
|
||||
|
|
@ -1101,7 +1104,7 @@ pub const Page = struct {
|
|||
|
||||
if (row.styled) {
|
||||
for (cells) |*cell| {
|
||||
if (cell.style_id == style.default_id) continue;
|
||||
if (cell.style_id == stylepkg.default_id) continue;
|
||||
|
||||
self.styles.release(self.memory, cell.style_id);
|
||||
}
|
||||
|
|
@ -1720,7 +1723,7 @@ pub const Page = struct {
|
|||
dirty_start: usize,
|
||||
dirty_size: usize,
|
||||
styles_start: usize,
|
||||
styles_layout: style.Set.Layout,
|
||||
styles_layout: StyleSet.Layout,
|
||||
grapheme_alloc_start: usize,
|
||||
grapheme_alloc_layout: GraphemeAlloc.Layout,
|
||||
grapheme_map_start: usize,
|
||||
|
|
@ -1756,8 +1759,8 @@ pub const Page = struct {
|
|||
const dirty_start = alignForward(usize, cells_end, @alignOf(usize));
|
||||
const dirty_end: usize = dirty_start + (dirty_usize_length * @sizeOf(usize));
|
||||
|
||||
const styles_layout: style.Set.Layout = .init(cap.styles);
|
||||
const styles_start = alignForward(usize, dirty_end, style.Set.base_align.toByteUnits());
|
||||
const styles_layout: StyleSet.Layout = .init(cap.styles);
|
||||
const styles_start = alignForward(usize, dirty_end, StyleSet.base_align.toByteUnits());
|
||||
const styles_end = styles_start + styles_layout.total_size;
|
||||
|
||||
const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes);
|
||||
|
|
@ -1886,7 +1889,7 @@ pub const Capacity = struct {
|
|||
const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits());
|
||||
const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits());
|
||||
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits());
|
||||
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align.toByteUnits());
|
||||
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, StyleSet.base_align.toByteUnits());
|
||||
|
||||
// The size per row is:
|
||||
// - The row metadata itself
|
||||
|
|
@ -2014,7 +2017,7 @@ pub const Cell = packed struct(u64) {
|
|||
|
||||
/// The style ID to use for this cell within the style map. Zero
|
||||
/// is always the default style so no lookup is required.
|
||||
style_id: style.Id = 0,
|
||||
style_id: StyleId = 0,
|
||||
|
||||
/// The wide property of this cell, for wide characters. Characters in
|
||||
/// a terminal grid can only be 1 or 2 cells wide. A wide character
|
||||
|
|
@ -2123,7 +2126,7 @@ pub const Cell = packed struct(u64) {
|
|||
}
|
||||
|
||||
pub fn hasStyling(self: Cell) bool {
|
||||
return self.style_id != style.default_id;
|
||||
return self.style_id != stylepkg.default_id;
|
||||
}
|
||||
|
||||
/// Returns true if the cell has no text or styling.
|
||||
|
|
|
|||
|
|
@ -1992,8 +1992,6 @@ pub fn Stream(comptime Handler: type) type {
|
|||
log.warn("invalid OSC, should never happen", .{});
|
||||
},
|
||||
}
|
||||
|
||||
log.warn("unimplemented OSC command: {s}", .{@tagName(cmd)});
|
||||
}
|
||||
|
||||
inline fn configureCharset(
|
||||
|
|
@ -2255,8 +2253,8 @@ test "stream: print" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.print => self.c = value.cp,
|
||||
|
|
@ -2276,8 +2274,8 @@ test "simd: print invalid utf-8" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.print => self.c = value.cp,
|
||||
|
|
@ -2297,8 +2295,8 @@ test "simd: complete incomplete utf-8" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.print => self.c = value.cp,
|
||||
|
|
@ -2322,8 +2320,8 @@ test "stream: cursor right (CUF)" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.cursor_right => self.amount = value.value,
|
||||
|
|
@ -2354,8 +2352,8 @@ test "stream: dec set mode (SM) and reset mode (RM)" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.set_mode => self.mode = value.mode,
|
||||
|
|
@ -2383,8 +2381,8 @@ test "stream: ansi set mode (SM) and reset mode (RM)" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.set_mode => self.mode = value.mode,
|
||||
|
|
@ -2417,11 +2415,10 @@ test "stream: ansi set mode (SM) and reset mode (RM) with unknown value" {
|
|||
|
||||
pub fn vt(
|
||||
self: *@This(),
|
||||
comptime action: anytype,
|
||||
value: anytype,
|
||||
comptime action: Action.Tag,
|
||||
value: Action.Value(action),
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = action;
|
||||
_ = value;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ pub const Handler = struct {
|
|||
.end_of_input => self.terminal.markSemanticPrompt(.command),
|
||||
.end_of_command => self.terminal.screen.cursor.page_row.semantic_prompt = .input,
|
||||
.mouse_shape => self.terminal.mouse_shape = value,
|
||||
.color_operation => try self.colorOperation(value.op, &value.requests),
|
||||
|
||||
// No supported DCS commands have any terminal-modifying effects,
|
||||
// but they may in the future. For now we just ignore it.
|
||||
|
|
@ -186,7 +187,6 @@ pub const Handler = struct {
|
|||
.device_status,
|
||||
.kitty_keyboard_query,
|
||||
.kitty_color_report,
|
||||
.color_operation,
|
||||
.window_title,
|
||||
.report_pwd,
|
||||
.show_desktop_notification,
|
||||
|
|
@ -291,6 +291,56 @@ pub const Handler = struct {
|
|||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn colorOperation(
|
||||
self: *Handler,
|
||||
op: @import("osc/color.zig").Operation,
|
||||
requests: *const @import("osc/color.zig").List,
|
||||
) !void {
|
||||
_ = op;
|
||||
if (requests.count() == 0) return;
|
||||
|
||||
var it = requests.constIterator(0);
|
||||
while (it.next()) |req| {
|
||||
switch (req.*) {
|
||||
.set => |set| {
|
||||
switch (set.target) {
|
||||
.palette => |i| {
|
||||
self.terminal.color_palette.colors[i] = set.color;
|
||||
self.terminal.color_palette.mask.set(i);
|
||||
},
|
||||
.dynamic,
|
||||
.special,
|
||||
=> {},
|
||||
}
|
||||
},
|
||||
|
||||
.reset => |target| switch (target) {
|
||||
.palette => |i| {
|
||||
const mask = &self.terminal.color_palette.mask;
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
mask.unset(i);
|
||||
},
|
||||
.dynamic,
|
||||
.special,
|
||||
=> {},
|
||||
},
|
||||
|
||||
.reset_palette => {
|
||||
const mask = &self.terminal.color_palette.mask;
|
||||
var mask_iterator = mask.iterator(.{});
|
||||
while (mask_iterator.next()) |i| {
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
}
|
||||
mask.* = .initEmpty();
|
||||
},
|
||||
|
||||
.query,
|
||||
.reset_special,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "basic print" {
|
||||
|
|
@ -540,3 +590,51 @@ test "ignores query actions" {
|
|||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("Test", str);
|
||||
}
|
||||
|
||||
test "OSC 4 set and reset palette" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Save default color
|
||||
const default_color_0 = t.default_palette[0];
|
||||
|
||||
// Set color 0 to red
|
||||
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
|
||||
try testing.expectEqual(@as(u8, 0xff), t.color_palette.colors[0].r);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.color_palette.colors[0].g);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.color_palette.colors[0].b);
|
||||
try testing.expect(t.color_palette.mask.isSet(0));
|
||||
|
||||
// Reset color 0
|
||||
try s.nextSlice("\x1b]104;0\x1b\\");
|
||||
try testing.expectEqual(default_color_0, t.color_palette.colors[0]);
|
||||
try testing.expect(!t.color_palette.mask.isSet(0));
|
||||
}
|
||||
|
||||
test "OSC 104 reset all palette colors" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set multiple colors
|
||||
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
|
||||
try s.nextSlice("\x1b]4;1;rgb:00/ff/00\x1b\\");
|
||||
try s.nextSlice("\x1b]4;2;rgb:00/00/ff\x1b\\");
|
||||
try testing.expect(t.color_palette.mask.isSet(0));
|
||||
try testing.expect(t.color_palette.mask.isSet(1));
|
||||
try testing.expect(t.color_palette.mask.isSet(2));
|
||||
|
||||
// Reset all palette colors
|
||||
try s.nextSlice("\x1b]104\x1b\\");
|
||||
try testing.expectEqual(t.default_palette[0], t.color_palette.colors[0]);
|
||||
try testing.expectEqual(t.default_palette[1], t.color_palette.colors[1]);
|
||||
try testing.expectEqual(t.default_palette[2], t.color_palette.colors[2]);
|
||||
try testing.expect(!t.color_palette.mask.isSet(0));
|
||||
try testing.expect(!t.color_palette.mask.isSet(1));
|
||||
try testing.expect(!t.color_palette.mask.isSet(2));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,74 @@ pub const Style = struct {
|
|||
_ = 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 };
|
||||
}
|
||||
|
||||
const VTFormatter = struct {
|
||||
style: *const Style,
|
||||
|
||||
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 formatColor(writer, 38, self.style.fg_color);
|
||||
try formatColor(writer, 48, self.style.bg_color);
|
||||
try formatColor(writer, 58, self.style.underline_color);
|
||||
}
|
||||
|
||||
fn formatColor(
|
||||
writer: *std.Io.Writer,
|
||||
prefix: u8,
|
||||
value: Color,
|
||||
) !void {
|
||||
switch (value) {
|
||||
.none => {},
|
||||
.palette => |idx| try writer.print(
|
||||
"\x1b[{d};5;{}m",
|
||||
.{ prefix, idx },
|
||||
),
|
||||
.rgb => |rgb| try writer.print(
|
||||
"\x1b[{d};2;{};{};{}m",
|
||||
.{ prefix, 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.
|
||||
|
|
@ -394,6 +462,316 @@ pub const Set = RefCountedSet(
|
|||
},
|
||||
);
|
||||
|
||||
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 "Set basic usage" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
|
|||
Loading…
Reference in New Issue