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/sgr.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig | 1103 |
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); +} |
