aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/sgr.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/sgr.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/sgr.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/sgr.zig1103
1 files changed, 1103 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/sgr.zig b/vendor/libghostty-vt/src/terminal/sgr.zig
new file mode 100644
index 00000000..e9b9354c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/sgr.zig
@@ -0,0 +1,1103 @@
+//! SGR (Select Graphic Rendition) attrinvbute parsing and types.
+
+const std = @import("std");
+const assert = @import("../quirks.zig").inlineAssert;
+const testing = std.testing;
+const lib = @import("lib.zig");
+const color = @import("color.zig");
+const SepList = @import("Parser.zig").Action.CSI.SepList;
+
+/// Attribute type for SGR
+pub const Attribute = union(Tag) {
+ /// Unset all attributes
+ unset,
+
+ /// Unknown attribute, the raw CSI command parameters are here.
+ unknown: Unknown,
+
+ /// Bold the text.
+ bold,
+ reset_bold,
+
+ /// Italic text.
+ italic,
+ reset_italic,
+
+ /// Faint/dim text.
+ /// Note: reset faint is the same SGR code as reset bold
+ faint,
+
+ /// Underline the text
+ underline: Underline,
+ underline_color: color.RGB,
+ @"256_underline_color": u8,
+ reset_underline_color,
+
+ // Overline the text
+ overline,
+ reset_overline,
+
+ /// Blink the text
+ blink,
+ reset_blink,
+
+ /// Invert fg/bg colors.
+ inverse,
+ reset_inverse,
+
+ /// Invisible
+ invisible,
+ reset_invisible,
+
+ /// Strikethrough the text.
+ strikethrough,
+ reset_strikethrough,
+
+ /// Set foreground color as RGB values.
+ direct_color_fg: color.RGB,
+
+ /// Set background color as RGB values.
+ direct_color_bg: color.RGB,
+
+ /// Set the background/foreground as a named color attribute.
+ @"8_bg": color.Name,
+ @"8_fg": color.Name,
+
+ /// Reset the fg/bg to their default values.
+ reset_fg,
+ reset_bg,
+
+ /// Set the background/foreground as a named bright color attribute.
+ @"8_bright_bg": color.Name,
+ @"8_bright_fg": color.Name,
+
+ /// Set background color as 256-color palette.
+ @"256_bg": u8,
+
+ /// Set foreground color as 256-color palette.
+ @"256_fg": u8,
+
+ pub const Tag = lib.Enum(
+ lib.target,
+ &.{
+ "unset",
+ "unknown",
+ "bold",
+ "reset_bold",
+ "italic",
+ "reset_italic",
+ "faint",
+ "underline",
+ "underline_color",
+ "256_underline_color",
+ "reset_underline_color",
+ "overline",
+ "reset_overline",
+ "blink",
+ "reset_blink",
+ "inverse",
+ "reset_inverse",
+ "invisible",
+ "reset_invisible",
+ "strikethrough",
+ "reset_strikethrough",
+ "direct_color_fg",
+ "direct_color_bg",
+ "8_bg",
+ "8_fg",
+ "reset_fg",
+ "reset_bg",
+ "8_bright_bg",
+ "8_bright_fg",
+ "256_bg",
+ "256_fg",
+ },
+ );
+
+ pub const Unknown = struct {
+ /// Full is the full SGR input.
+ full: []const u16,
+
+ /// Partial is the remaining, where we got hung up.
+ partial: []const u16,
+
+ pub const C = extern struct {
+ full_ptr: [*]const u16,
+ full_len: usize,
+ partial_ptr: [*]const u16,
+ partial_len: usize,
+ };
+
+ pub fn cval(self: Unknown) Unknown.C {
+ return .{
+ .full_ptr = self.full.ptr,
+ .full_len = self.full.len,
+ .partial_ptr = self.partial.ptr,
+ .partial_len = self.partial.len,
+ };
+ }
+ };
+
+ pub const Underline = enum(u3) {
+ none = 0,
+ single = 1,
+ double = 2,
+ curly = 3,
+ dotted = 4,
+ dashed = 5,
+
+ pub const C = c_int;
+
+ pub fn cval(self: Underline) Underline.C {
+ return @intFromEnum(self);
+ }
+ };
+
+ /// C ABI functions.
+ const c_union = lib.TaggedUnion(
+ lib.target,
+ @This(),
+ // Padding size for C ABI compatibility.
+ // Largest variant is Unknown.C: 2 pointers + 2 usize = 32 bytes on 64-bit.
+ // We use [8]u64 (64 bytes) to allow room for future expansion while
+ // maintaining ABI compatibility.
+ [8]u64,
+ );
+ pub const Value = c_union.Value;
+ pub const C = c_union.C;
+ pub const CValue = c_union.CValue;
+ pub const cval = c_union.cval;
+};
+
+/// Parser parses the attributes from a list of SGR parameters.
+pub const Parser = struct {
+ params: []const u16 = &.{},
+ params_sep: SepList = .initEmpty(),
+ idx: usize = 0,
+
+ /// Empty state parser.
+ pub const empty: Parser = .{};
+
+ /// Next returns the next attribute or null if there are no more attributes.
+ pub fn next(self: *Parser) ?Attribute {
+ if (self.idx >= self.params.len) {
+ // We're more likely to not be done than to be done.
+ @branchHint(.unlikely);
+
+ // Add one to ensure we don't loop on unset
+ defer self.idx += 1;
+
+ // If we're at index zero it means we must have an empty list
+ // and an empty list implicitly means unset, otherwise we're
+ // done and return null.
+ return if (self.idx == 0) .unset else null;
+ }
+
+ const slice = self.params[self.idx..self.params.len];
+ // Call inlined for performance reasons.
+ const colon = @call(
+ .always_inline,
+ SepList.isSet,
+ .{ self.params_sep, self.idx },
+ );
+ self.idx += 1;
+
+ // Our last one will have an idx be the last value.
+ if (slice.len == 0) return null;
+
+ // If we have a colon separator then we need to ensure we're
+ // parsing a value that allows it.
+ if (colon) {
+ // Colons are fairly rare in the wild.
+ @branchHint(.unlikely);
+
+ switch (slice[0]) {
+ 4, 38, 48, 58 => {},
+
+ else => {
+ // In real world use it's very rare
+ // that we receive an invalid sequence.
+ @branchHint(.cold);
+
+ // Consume all the colon separated
+ // values and return them as unknown.
+ const start = self.idx;
+ while (self.params_sep.isSet(self.idx)) self.idx += 1;
+ self.idx += 1;
+ return .{ .unknown = .{
+ .full = self.params,
+ .partial = slice[0..@min(self.idx - start + 1, slice.len)],
+ } };
+ },
+ }
+ }
+
+ switch (slice[0]) {
+ 0 => return .unset,
+
+ 1 => return .bold,
+
+ 2 => return .faint,
+
+ 3 => return .italic,
+
+ 4 => underline: {
+ if (colon) {
+ // Colons are fairly rare in the wild.
+ @branchHint(.unlikely);
+
+ // A trailing colon with no following sub-param
+ // (e.g. "ESC[58:4:m") leaves the colon separator
+ // bit set on the last param without adding another
+ // entry, so we can see param 4 with a colon but
+ // nothing after it.
+ if (slice.len < 2) {
+ @branchHint(.cold);
+ break :underline;
+ }
+
+ if (self.isColon()) {
+ // Invalid/unknown SGRs are just not very likely.
+ @branchHint(.cold);
+
+ self.consumeUnknownColon();
+ break :underline;
+ }
+
+ self.idx += 1;
+ return .{
+ .underline = switch (slice[1]) {
+ 0 => .none,
+ 1 => .single,
+ 2 => .double,
+ 3 => .curly,
+ 4 => .dotted,
+ 5 => .dashed,
+
+ // For unknown underline styles,
+ // just render a single underline.
+ else => single: {
+ // This is quite a rare condition.
+ @branchHint(.cold);
+ break :single .single;
+ },
+ },
+ };
+ }
+
+ return .{ .underline = .single };
+ },
+
+ 5 => return .blink,
+
+ 6 => return .blink,
+
+ 7 => return .inverse,
+
+ 8 => return .invisible,
+
+ 9 => return .strikethrough,
+
+ 21 => return .{ .underline = .double },
+
+ 22 => return .reset_bold,
+
+ 23 => return .reset_italic,
+
+ 24 => return .{ .underline = .none },
+
+ 25 => return .reset_blink,
+
+ 27 => return .reset_inverse,
+
+ 28 => return .reset_invisible,
+
+ 29 => return .reset_strikethrough,
+
+ 30...37 => return .{
+ .@"8_fg" = @enumFromInt(slice[0] - 30),
+ },
+
+ 38 => if (slice.len >= 2) {
+ // We are very likely to have enough parameters.
+ @branchHint(.likely);
+
+ switch (slice[1]) {
+ // `2` indicates direct-color (r, g, b).
+ // We need at least 3 more params for this to make sense.
+ 2 => if (self.parseDirectColor(
+ .direct_color_fg,
+ slice,
+ colon,
+ )) |v| return v,
+
+ // `5` indicates indexed color.
+ 5 => if (slice.len >= 3) {
+ // We are very likely to have enough parameters.
+ @branchHint(.likely);
+
+ self.idx += 2;
+ return .{
+ .@"256_fg" = @truncate(slice[2]),
+ };
+ },
+
+ else => {},
+ }
+ },
+
+ 39 => return .reset_fg,
+
+ 40...47 => return .{
+ .@"8_bg" = @enumFromInt(slice[0] - 40),
+ },
+
+ 48 => if (slice.len >= 2) {
+ // We are very likely to have enough parameters.
+ @branchHint(.likely);
+
+ switch (slice[1]) {
+ // `2` indicates direct-color (r, g, b).
+ // We need at least 3 more params for this to make sense.
+ 2 => if (self.parseDirectColor(
+ .direct_color_bg,
+ slice,
+ colon,
+ )) |v| return v,
+
+ // `5` indicates indexed color.
+ 5 => if (slice.len >= 3) {
+ // We are very likely to have enough parameters.
+ @branchHint(.likely);
+
+ self.idx += 2;
+ return .{
+ .@"256_bg" = @truncate(slice[2]),
+ };
+ },
+
+ else => {},
+ }
+ },
+
+ 49 => return .reset_bg,
+
+ 53 => return .overline,
+ 55 => return .reset_overline,
+
+ 58 => if (slice.len >= 2) {
+ // We are very likely to have enough parameters.
+ @branchHint(.likely);
+
+ switch (slice[1]) {
+ // `2` indicates direct-color (r, g, b).
+ // We need at least 3 more params for this to make sense.
+ 2 => if (self.parseDirectColor(
+ .underline_color,
+ slice,
+ colon,
+ )) |v| return v,
+
+ // `5` indicates indexed color.
+ 5 => if (slice.len >= 3) {
+ // We are very likely to have enough parameters.
+ @branchHint(.likely);
+
+ self.idx += 2;
+ return .{
+ .@"256_underline_color" = @truncate(slice[2]),
+ };
+ },
+ else => {},
+ }
+ },
+
+ 59 => return .reset_underline_color,
+
+ 90...97 => return .{
+ // 82 instead of 90 to offset to "bright" colors
+ .@"8_bright_fg" = @enumFromInt(slice[0] - 82),
+ },
+
+ 100...107 => return .{
+ .@"8_bright_bg" = @enumFromInt(slice[0] - 92),
+ },
+
+ else => {},
+ }
+
+ return .{ .unknown = .{ .full = self.params, .partial = slice } };
+ }
+
+ fn parseDirectColor(
+ self: *Parser,
+ comptime tag: Attribute.Tag,
+ slice: []const u16,
+ colon: bool,
+ ) ?Attribute {
+ // Any direct color style must have at least 5 values.
+ if (slice.len < 5) return null;
+
+ // Only used for direct color sets (38, 48, 58) and subparam 2.
+ assert(slice[1] == 2);
+
+ // Note: We use @truncate because the value should be 0 to 255. If
+ // it isn't, the behavior is undefined so we just... truncate it.
+
+ // If we don't have a colon, then we expect exactly 3 semicolon
+ // separated values.
+ if (!colon) {
+ // Semicolons are much more common than colons.
+ @branchHint(.likely);
+
+ self.idx += 4;
+ return @unionInit(Attribute, @tagName(tag), .{
+ .r = @truncate(slice[2]),
+ .g = @truncate(slice[3]),
+ .b = @truncate(slice[4]),
+ });
+ }
+
+ // We have a colon, we might have either 5 or 6 values depending
+ // on if the colorspace is present.
+ const count = self.countColon();
+ switch (count) {
+ 3 => {
+ // This is the much more common case in the wild.
+ @branchHint(.likely);
+
+ self.idx += 4;
+ return @unionInit(Attribute, @tagName(tag), .{
+ .r = @truncate(slice[2]),
+ .g = @truncate(slice[3]),
+ .b = @truncate(slice[4]),
+ });
+ },
+
+ 4 => {
+ self.idx += 5;
+ return @unionInit(Attribute, @tagName(tag), .{
+ .r = @truncate(slice[3]),
+ .g = @truncate(slice[4]),
+ .b = @truncate(slice[5]),
+ });
+ },
+
+ else => {
+ // Invalid/unknown SGRs just don't happen very often at all.
+ @branchHint(.cold);
+
+ self.consumeUnknownColon();
+ return null;
+ },
+ }
+ }
+
+ /// Returns true if the present position has a colon separator.
+ /// This always returns false for the last value since it has no
+ /// separator.
+ inline fn isColon(self: *Parser) bool {
+ // Call inlined for performance reasons.
+ return @call(
+ .always_inline,
+ SepList.isSet,
+ .{ self.params_sep, self.idx },
+ );
+ }
+
+ fn countColon(self: *Parser) usize {
+ var count: usize = 0;
+ var idx = self.idx;
+ while (idx < self.params.len - 1 and self.params_sep.isSet(idx)) : (idx += 1) {
+ count += 1;
+ }
+ return count;
+ }
+
+ /// Consumes all the remaining parameters separated by a colon and
+ /// returns an unknown attribute.
+ fn consumeUnknownColon(self: *Parser) void {
+ const count = self.countColon();
+ self.idx += count + 1;
+ }
+};
+
+fn testParse(params: []const u16) Attribute {
+ var p: Parser = .{ .params = params };
+ return p.next().?;
+}
+
+fn testParseColon(params: []const u16) Attribute {
+ var p: Parser = .{ .params = params };
+ // Mark all parameters except the last as having a colon after.
+ for (0..params.len - 1) |i| p.params_sep.set(i);
+ return p.next().?;
+}
+
+test "sgr: Attribute C compat" {
+ _ = Attribute.C;
+}
+
+test "sgr: Parser" {
+ try testing.expect(testParse(&[_]u16{}) == .unset);
+ try testing.expect(testParse(&[_]u16{0}) == .unset);
+
+ {
+ const v = testParse(&[_]u16{ 38, 2, 40, 44, 52 });
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 40), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 44), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 52), v.direct_color_fg.b);
+ }
+
+ try testing.expect(testParse(&[_]u16{ 38, 2, 44, 52 }) == .unknown);
+
+ {
+ const v = testParse(&[_]u16{ 48, 2, 40, 44, 52 });
+ try testing.expect(v == .direct_color_bg);
+ try testing.expectEqual(@as(u8, 40), v.direct_color_bg.r);
+ try testing.expectEqual(@as(u8, 44), v.direct_color_bg.g);
+ try testing.expectEqual(@as(u8, 52), v.direct_color_bg.b);
+ }
+
+ try testing.expect(testParse(&[_]u16{ 48, 2, 44, 52 }) == .unknown);
+}
+
+test "sgr: Parser multiple" {
+ var p: Parser = .{ .params = &[_]u16{ 0, 38, 2, 40, 44, 52 } };
+ try testing.expect(p.next().? == .unset);
+ try testing.expect(p.next().? == .direct_color_fg);
+ try testing.expect(p.next() == null);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: unsupported with colon" {
+ var p: Parser = .{
+ .params = &[_]u16{ 0, 4, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ break :sep list;
+ },
+ };
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: unsupported with multiple colon" {
+ var p: Parser = .{
+ .params = &[_]u16{ 0, 4, 2, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ list.set(1);
+ break :sep list;
+ },
+ };
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: bold" {
+ {
+ const v = testParse(&[_]u16{1});
+ try testing.expect(v == .bold);
+ }
+
+ {
+ const v = testParse(&[_]u16{22});
+ try testing.expect(v == .reset_bold);
+ }
+}
+
+test "sgr: italic" {
+ {
+ const v = testParse(&[_]u16{3});
+ try testing.expect(v == .italic);
+ }
+
+ {
+ const v = testParse(&[_]u16{23});
+ try testing.expect(v == .reset_italic);
+ }
+}
+
+test "sgr: underline" {
+ {
+ const v = testParse(&[_]u16{4});
+ try testing.expect(v == .underline);
+ }
+
+ {
+ const v = testParse(&[_]u16{24});
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .none);
+ }
+}
+
+test "sgr: underline styles" {
+ {
+ const v = testParseColon(&[_]u16{ 4, 2 });
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .double);
+ }
+
+ {
+ const v = testParseColon(&[_]u16{ 4, 0 });
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .none);
+ }
+
+ {
+ const v = testParseColon(&[_]u16{ 4, 1 });
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .single);
+ }
+
+ {
+ const v = testParseColon(&[_]u16{ 4, 3 });
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .curly);
+ }
+
+ {
+ const v = testParseColon(&[_]u16{ 4, 4 });
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .dotted);
+ }
+
+ {
+ const v = testParseColon(&[_]u16{ 4, 5 });
+ try testing.expect(v == .underline);
+ try testing.expect(v.underline == .dashed);
+ }
+}
+
+test "sgr: underline style with more" {
+ var p: Parser = .{
+ .params = &[_]u16{ 4, 2, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ break :sep list;
+ },
+ };
+
+ try testing.expect(p.next().? == .underline);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: underline style with too many colons" {
+ var p: Parser = .{
+ .params = &[_]u16{ 4, 2, 3, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ list.set(1);
+ break :sep list;
+ },
+ };
+
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: blink" {
+ {
+ const v = testParse(&[_]u16{5});
+ try testing.expect(v == .blink);
+ }
+
+ {
+ const v = testParse(&[_]u16{6});
+ try testing.expect(v == .blink);
+ }
+
+ {
+ const v = testParse(&[_]u16{25});
+ try testing.expect(v == .reset_blink);
+ }
+}
+
+test "sgr: inverse" {
+ {
+ const v = testParse(&[_]u16{7});
+ try testing.expect(v == .inverse);
+ }
+
+ {
+ const v = testParse(&[_]u16{27});
+ try testing.expect(v == .reset_inverse);
+ }
+}
+
+test "sgr: strikethrough" {
+ {
+ const v = testParse(&[_]u16{9});
+ try testing.expect(v == .strikethrough);
+ }
+
+ {
+ const v = testParse(&[_]u16{29});
+ try testing.expect(v == .reset_strikethrough);
+ }
+}
+
+test "sgr: 8 color" {
+ var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .@"8_fg");
+ try testing.expect(v.@"8_fg" == .red);
+ }
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .@"8_bg");
+ try testing.expect(v.@"8_bg" == .yellow);
+ }
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .@"8_bright_fg");
+ try testing.expect(v.@"8_bright_fg" == .bright_black);
+ }
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .@"8_bright_bg");
+ try testing.expect(v.@"8_bright_bg" == .bright_yellow);
+ }
+}
+
+test "sgr: 256 color" {
+ var p: Parser = .{ .params = &[_]u16{ 38, 5, 161, 48, 5, 236 } };
+ try testing.expect(p.next().? == .@"256_fg");
+ try testing.expect(p.next().? == .@"256_bg");
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: 256 color underline" {
+ var p: Parser = .{ .params = &[_]u16{ 58, 5, 9 } };
+ try testing.expect(p.next().? == .@"256_underline_color");
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: 24-bit bg color" {
+ {
+ const v = testParseColon(&[_]u16{ 48, 2, 1, 2, 3 });
+ try testing.expect(v == .direct_color_bg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b);
+ }
+}
+
+test "sgr: underline color" {
+ {
+ const v = testParseColon(&[_]u16{ 58, 2, 1, 2, 3 });
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 1), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 2), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 3), v.underline_color.b);
+ }
+
+ {
+ const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 1), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 2), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 3), v.underline_color.b);
+ }
+}
+
+test "sgr: reset underline color" {
+ var p: Parser = .{ .params = &[_]u16{59} };
+ try testing.expect(p.next().? == .reset_underline_color);
+}
+
+test "sgr: invisible" {
+ var p: Parser = .{ .params = &[_]u16{ 8, 28 } };
+ try testing.expect(p.next().? == .invisible);
+ try testing.expect(p.next().? == .reset_invisible);
+}
+
+test "sgr: underline, bg, and fg" {
+ var p: Parser = .{
+ .params = &[_]u16{ 4, 38, 2, 255, 247, 219, 48, 2, 242, 93, 147, 4 },
+ };
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline);
+ try testing.expectEqual(Attribute.Underline.single, v.underline);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 255), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 247), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 219), v.direct_color_fg.b);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_bg);
+ try testing.expectEqual(@as(u8, 242), v.direct_color_bg.r);
+ try testing.expectEqual(@as(u8, 93), v.direct_color_bg.g);
+ try testing.expectEqual(@as(u8, 147), v.direct_color_bg.b);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline);
+ try testing.expectEqual(Attribute.Underline.single, v.underline);
+ }
+}
+
+test "sgr: direct color fg missing color" {
+ // This used to crash
+ var p: Parser = .{ .params = &[_]u16{ 38, 5 } };
+ while (p.next()) |_| {}
+}
+
+test "sgr: direct color bg missing color" {
+ // This used to crash
+ var p: Parser = .{ .params = &[_]u16{ 48, 5 } };
+ while (p.next()) |_| {}
+}
+
+test "sgr: direct fg/bg/underline ignore optional color space" {
+ // These behaviors have been verified against xterm.
+
+ // Colon version should skip the optional color space identifier
+ {
+ // 3 8 : 2 : Pi : Pr : Pg : Pb
+ const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
+ }
+ {
+ // 4 8 : 2 : Pi : Pr : Pg : Pb
+ const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .direct_color_bg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b);
+ }
+ {
+ // 5 8 : 2 : Pi : Pr : Pg : Pb
+ const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 1), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 2), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 3), v.underline_color.b);
+ }
+
+ // Semicolon version should not parse optional color space identifier
+ {
+ // 3 8 ; 2 ; Pr ; Pg ; Pb
+ const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 0), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_fg.b);
+ }
+ {
+ // 4 8 ; 2 ; Pr ; Pg ; Pb
+ const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .direct_color_bg);
+ try testing.expectEqual(@as(u8, 0), v.direct_color_bg.r);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_bg.g);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_bg.b);
+ }
+ {
+ // 5 8 ; 2 ; Pr ; Pg ; Pb
+ const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3 });
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 0), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 1), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 2), v.underline_color.b);
+ }
+}
+
+test "sgr: direct fg colon with too many colons" {
+ var p: Parser = .{
+ .params = &[_]u16{ 38, 2, 0, 1, 2, 3, 4, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ for (0..6) |idx| list.set(idx);
+ break :sep list;
+ },
+ };
+
+ try testing.expect(p.next().? == .unknown);
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: direct fg colon with colorspace and extra param" {
+ var p: Parser = .{
+ .params = &[_]u16{ 38, 2, 0, 1, 2, 3, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ for (0..5) |idx| list.set(idx);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
+ }
+
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+test "sgr: direct fg colon no colorspace and extra param" {
+ var p: Parser = .{
+ .params = &[_]u16{ 38, 2, 1, 2, 3, 1 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ for (0..4) |idx| list.set(idx);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
+ }
+
+ try testing.expect(p.next().? == .bold);
+ try testing.expect(p.next() == null);
+}
+
+// Kakoune sent this complex SGR sequence that caused invalid behavior.
+test "sgr: kakoune input" {
+ // This used to crash
+ var p: Parser = .{
+ .params = &[_]u16{ 0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(1);
+ list.set(8);
+ list.set(9);
+ list.set(10);
+ list.set(11);
+ list.set(12);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .unset);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline);
+ try testing.expectEqual(Attribute.Underline.curly, v.underline);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 175), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 175), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 215), v.direct_color_fg.b);
+ }
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 190), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 80), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 70), v.underline_color.b);
+ }
+
+ //try testing.expect(p.next() == null);
+}
+
+// Discussion #5930, another input sent by kakoune
+test "sgr: kakoune input issue underline, fg, and bg" {
+ // echo -e "\033[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136mset everything in one sequence, broken\033[m"
+
+ // This used to crash
+ var p: Parser = .{
+ .params = &[_]u16{ 4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ break :sep list;
+ },
+ };
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline);
+ try testing.expectEqual(Attribute.Underline.curly, v.underline);
+ }
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_fg);
+ try testing.expectEqual(@as(u8, 51), v.direct_color_fg.r);
+ try testing.expectEqual(@as(u8, 51), v.direct_color_fg.g);
+ try testing.expectEqual(@as(u8, 51), v.direct_color_fg.b);
+ }
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .direct_color_bg);
+ try testing.expectEqual(@as(u8, 170), v.direct_color_bg.r);
+ try testing.expectEqual(@as(u8, 170), v.direct_color_bg.g);
+ try testing.expectEqual(@as(u8, 170), v.direct_color_bg.b);
+ }
+
+ {
+ const v = p.next().?;
+ try testing.expect(v == .underline_color);
+ try testing.expectEqual(@as(u8, 255), v.underline_color.r);
+ try testing.expectEqual(@as(u8, 97), v.underline_color.g);
+ try testing.expectEqual(@as(u8, 136), v.underline_color.b);
+ }
+
+ try testing.expect(p.next() == null);
+}
+
+// Fuzz crash: afl-out/stream/default/crashes/id:000021
+// Input "ESC [ 5 8 : 4 : m" produces params [58, 4] with colon
+// separator bits set at indices 0 and 1. The trailing colon causes
+// the second iteration to see param 4 (underline) with a colon,
+// triggering assert(slice.len >= 2) with slice.len == 1.
+test "sgr: underline colon with trailing separator and short slice" {
+ var p: Parser = .{
+ .params = &[_]u16{ 58, 4 },
+ .params_sep = sep: {
+ var list = SepList.initEmpty();
+ list.set(0);
+ list.set(1);
+ break :sep list;
+ },
+ };
+
+ // 58:4 is not a valid underline color (sub-param 4 is not 2 or 5),
+ // so it falls through as unknown.
+ try testing.expect(p.next().? == .unknown);
+
+ // Param 4 with a trailing colon but no sub-param is malformed,
+ // so it also falls through as unknown rather than panicking.
+ try testing.expect(p.next().? == .unknown);
+
+ try testing.expect(p.next() == null);
+}