perf: small sgr parser improvements

pull/9645/head
Qwerasd 2025-11-17 13:17:31 -07:00
parent 6d5b4a3426
commit 9ab9bc8e19
2 changed files with 143 additions and 89 deletions

View File

@ -1772,10 +1772,6 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void {
self.cursor.style.flags.underline = v; self.cursor.style.flags.underline = v;
}, },
.reset_underline => {
self.cursor.style.flags.underline = .none;
},
.underline_color => |rgb| { .underline_color => |rgb| {
self.cursor.style.underline_color = .{ .rgb = .{ self.cursor.style.underline_color = .{ .rgb = .{
.r = rgb.r, .r = rgb.r,

View File

@ -32,7 +32,6 @@ pub const Attribute = union(Tag) {
/// Underline the text /// Underline the text
underline: Underline, underline: Underline,
reset_underline,
underline_color: color.RGB, underline_color: color.RGB,
@"256_underline_color": u8, @"256_underline_color": u8,
reset_underline_color, reset_underline_color,
@ -92,7 +91,6 @@ pub const Attribute = union(Tag) {
"reset_italic", "reset_italic",
"faint", "faint",
"underline", "underline",
"reset_underline",
"underline_color", "underline_color",
"256_underline_color", "256_underline_color",
"reset_underline_color", "reset_underline_color",
@ -186,15 +184,16 @@ pub const Parser = struct {
/// Next returns the next attribute or null if there are no more attributes. /// Next returns the next attribute or null if there are no more attributes.
pub fn next(self: *Parser) ?Attribute { pub fn next(self: *Parser) ?Attribute {
if (self.idx >= self.params.len) { if (self.idx >= self.params.len) {
// If we're at index zero it means we must have an empty // We're more likely to not be done than to be done.
// list and an empty list implicitly means unset. @branchHint(.unlikely);
if (self.idx == 0) {
// Add one to ensure we don't loop on unset
self.idx += 1;
return .unset;
}
return null; // 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]; const slice = self.params[self.idx..self.params.len];
@ -206,20 +205,30 @@ pub const Parser = struct {
// If we have a colon separator then we need to ensure we're // If we have a colon separator then we need to ensure we're
// parsing a value that allows it. // parsing a value that allows it.
if (colon) switch (slice[0]) { if (colon) {
4, 38, 48, 58 => {}, // Colons are fairly rare in the wild.
@branchHint(.unlikely);
else => { switch (slice[0]) {
// Consume all the colon separated values. 4, 38, 48, 58 => {},
const start = self.idx;
while (self.params_sep.isSet(self.idx)) self.idx += 1; else => {
self.idx += 1; // In real world use it's very rare
return .{ .unknown = .{ // that we receive an invalid sequence.
.full = self.params, @branchHint(.cold);
.partial = slice[0..@min(self.idx - start + 1, slice.len)],
} }; // 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]) { switch (slice[0]) {
0 => return .unset, 0 => return .unset,
@ -232,25 +241,37 @@ pub const Parser = struct {
4 => underline: { 4 => underline: {
if (colon) { if (colon) {
// Colons are fairly rare in the wild.
@branchHint(.unlikely);
assert(slice.len >= 2); assert(slice.len >= 2);
if (self.isColon()) { if (self.isColon()) {
// Invalid/unknown SGRs are just not very likely.
@branchHint(.cold);
self.consumeUnknownColon(); self.consumeUnknownColon();
break :underline; break :underline;
} }
self.idx += 1; self.idx += 1;
switch (slice[1]) { return .{
0 => return .reset_underline, .underline = switch (slice[1]) {
1 => return .{ .underline = .single }, 0 => .none,
2 => return .{ .underline = .double }, 1 => .single,
3 => return .{ .underline = .curly }, 2 => .double,
4 => return .{ .underline = .dotted }, 3 => .curly,
5 => return .{ .underline = .dashed }, 4 => .dotted,
5 => .dashed,
// For unknown underline styles, just render // For unknown underline styles,
// a single underline. // just render a single underline.
else => return .{ .underline = .single }, else => single: {
} // This is quite a rare condition.
@branchHint(.cold);
break :single .single;
},
},
};
} }
return .{ .underline = .single }; return .{ .underline = .single };
@ -272,7 +293,7 @@ pub const Parser = struct {
23 => return .reset_italic, 23 => return .reset_italic,
24 => return .reset_underline, 24 => return .{ .underline = .none },
25 => return .reset_blink, 25 => return .reset_blink,
@ -286,23 +307,32 @@ pub const Parser = struct {
.@"8_fg" = @enumFromInt(slice[0] - 30), .@"8_fg" = @enumFromInt(slice[0] - 30),
}, },
38 => if (slice.len >= 2) switch (slice[1]) { 38 => if (slice.len >= 2) {
// `2` indicates direct-color (r, g, b). // We are very likely to have enough parameters.
// We need at least 3 more params for this to make sense. @branchHint(.likely);
2 => if (self.parseDirectColor(
.direct_color_fg,
slice,
colon,
)) |v| return v,
// `5` indicates indexed color. switch (slice[1]) {
5 => if (slice.len >= 3) { // `2` indicates direct-color (r, g, b).
self.idx += 2; // We need at least 3 more params for this to make sense.
return .{ 2 => if (self.parseDirectColor(
.@"256_fg" = @truncate(slice[2]), .direct_color_fg,
}; slice,
}, colon,
else => {}, )) |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, 39 => return .reset_fg,
@ -311,23 +341,32 @@ pub const Parser = struct {
.@"8_bg" = @enumFromInt(slice[0] - 40), .@"8_bg" = @enumFromInt(slice[0] - 40),
}, },
48 => if (slice.len >= 2) switch (slice[1]) { 48 => if (slice.len >= 2) {
// `2` indicates direct-color (r, g, b). // We are very likely to have enough parameters.
// We need at least 3 more params for this to make sense. @branchHint(.likely);
2 => if (self.parseDirectColor(
.direct_color_bg,
slice,
colon,
)) |v| return v,
// `5` indicates indexed color. switch (slice[1]) {
5 => if (slice.len >= 3) { // `2` indicates direct-color (r, g, b).
self.idx += 2; // We need at least 3 more params for this to make sense.
return .{ 2 => if (self.parseDirectColor(
.@"256_bg" = @truncate(slice[2]), .direct_color_bg,
}; slice,
}, colon,
else => {}, )) |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, 49 => return .reset_bg,
@ -335,23 +374,31 @@ pub const Parser = struct {
53 => return .overline, 53 => return .overline,
55 => return .reset_overline, 55 => return .reset_overline,
58 => if (slice.len >= 2) switch (slice[1]) { 58 => if (slice.len >= 2) {
// `2` indicates direct-color (r, g, b). // We are very likely to have enough parameters.
// We need at least 3 more params for this to make sense. @branchHint(.likely);
2 => if (self.parseDirectColor(
.underline_color,
slice,
colon,
)) |v| return v,
// `5` indicates indexed color. switch (slice[1]) {
5 => if (slice.len >= 3) { // `2` indicates direct-color (r, g, b).
self.idx += 2; // We need at least 3 more params for this to make sense.
return .{ 2 => if (self.parseDirectColor(
.@"256_underline_color" = @truncate(slice[2]), .underline_color,
}; slice,
}, colon,
else => {}, )) |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, 59 => return .reset_underline_color,
@ -389,6 +436,9 @@ pub const Parser = struct {
// If we don't have a colon, then we expect exactly 3 semicolon // If we don't have a colon, then we expect exactly 3 semicolon
// separated values. // separated values.
if (!colon) { if (!colon) {
// Semicolons are much more common than colons.
@branchHint(.likely);
self.idx += 4; self.idx += 4;
return @unionInit(Attribute, @tagName(tag), .{ return @unionInit(Attribute, @tagName(tag), .{
.r = @truncate(slice[2]), .r = @truncate(slice[2]),
@ -402,6 +452,9 @@ pub const Parser = struct {
const count = self.countColon(); const count = self.countColon();
switch (count) { switch (count) {
3 => { 3 => {
// This is the much more common case in the wild.
@branchHint(.likely);
self.idx += 4; self.idx += 4;
return @unionInit(Attribute, @tagName(tag), .{ return @unionInit(Attribute, @tagName(tag), .{
.r = @truncate(slice[2]), .r = @truncate(slice[2]),
@ -420,6 +473,9 @@ pub const Parser = struct {
}, },
else => { else => {
// Invalid/unknown SGRs just don't happen very often at all.
@branchHint(.cold);
self.consumeUnknownColon(); self.consumeUnknownColon();
return null; return null;
}, },
@ -560,7 +616,8 @@ test "sgr: underline" {
{ {
const v = testParse(&[_]u16{24}); const v = testParse(&[_]u16{24});
try testing.expect(v == .reset_underline); try testing.expect(v == .underline);
try testing.expect(v.underline == .none);
} }
} }
@ -573,7 +630,8 @@ test "sgr: underline styles" {
{ {
const v = testParseColon(&[_]u16{ 4, 0 }); const v = testParseColon(&[_]u16{ 4, 0 });
try testing.expect(v == .reset_underline); try testing.expect(v == .underline);
try testing.expect(v.underline == .none);
} }
{ {