Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion lib/src/core/escape/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ class EscapeParser {

var param = 0;
var hasParam = false;
// ':' separates sub-parameters (ITU T.416), e.g.
// `4:0` (underline off) or `38:2::r:g:b`. The ':' used to be dropped, which
// merged the digits (`4:0` became `40`). Collect each ';' group here.
var group = <int>[];
var hasColon = false;
final groups = <List<int>>[];
while (true) {
// The sequence isn't completed, just ignore it.
if (_queue.isEmpty) {
Expand All @@ -236,7 +242,23 @@ class EscapeParser {

final char = _queue.consume();

if (char == Ascii.colon) {
hasColon = true;
group.add(param);
param = 0;
hasParam = true;
continue;
}

if (char == Ascii.semicolon) {
if (hasColon) {
group.add(param);
groups.add(group);
group = <int>[];
param = 0;
hasParam = false;
continue;
}
if (hasParam) {
_csi.params.add(param);
}
Expand All @@ -257,7 +279,16 @@ class EscapeParser {
}

if (char >= Ascii.atSign && char <= Ascii.tilde) {
if (hasParam) {
if (hasColon) {
// Earlier ';' groups were kept apart; flatten them again.
if (hasParam) {
group.add(param);
}
groups.add(group);
for (final g in groups) {
_csi.params.addAll(_flattenSubParams(g, char));
}
} else if (hasParam) {
_csi.params.add(param);
}

Expand All @@ -267,6 +298,33 @@ class EscapeParser {
}
}

/// Turns one `;` group of colon-separated sub-parameters into the
/// equivalent semicolon-style parameters the handlers understand.
List<int> _flattenSubParams(List<int> g, int finalByte) {
if (g.length == 1) return g;
if (finalByte != 0x6d /* m */) return [g.first];

switch (g.first) {
case 4: // 4:0 none, 4:1..5 underline styles
return [g.length > 1 && g[1] == 0 ? 24 : 4];
case 38:
case 48:
if (g.length > 2 && g[1] == 2) {
// 38:2:r:g:b or 38:2:<colorspace>:r:g:b
final rgb = g.sublist(g.length - 3);
return [g.first, 2, ...rgb];
}
if (g.length > 2 && g[1] == 5) {
return [g.first, 5, g[2]];
}
return const [];
case 58: // underline colour: unsupported, swallow it
return const [];
default:
return [g.first];
}
}

late final _csiHandlers = FastLookupTable<_CsiHandler>({
// 'a'.codeUnitAt(0): _csiHandleCursorHorizontalRelative,
'b'.codeUnitAt(0): _csiHandleRepeatPreviousCharacter,
Expand Down
31 changes: 31 additions & 0 deletions test/src/core/escape/parser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,36 @@ void main() {
parser.write('\x1b[8;24;80t');
verify(parser.handler.resize(80, 24));
});

test('parses SGR colon sub-parameters', () {
final parser = EscapeParser(MockEscapeHandler());
parser.write('\x1b[4m\x1b[4:0m');
verifyInOrder([
parser.handler.setCursorUnderline(),
parser.handler.unsetCursorUnderline(),
]);
verifyNever(parser.handler.setBackgroundColor16(NamedColor.black));
});

test('parses colon-separated underline styles as underline', () {
final parser = EscapeParser(MockEscapeHandler());
parser.write('\x1b[4:3m');
verify(parser.handler.setCursorUnderline()).called(1);
});

test('parses colon-separated RGB colours', () {
final parser = EscapeParser(MockEscapeHandler());
parser.write('\x1b[38:2::1:2:3m\x1b[48:2:4:5:6m');
verify(parser.handler.setForegroundColorRgb(1, 2, 3));
verify(parser.handler.setBackgroundColorRgb(4, 5, 6));
});

test('parses colon-separated 256 colours among other parameters', () {
final parser = EscapeParser(MockEscapeHandler());
parser.write('\x1b[1;38:5:9;3m');
verify(parser.handler.setCursorBold());
verify(parser.handler.setForegroundColor256(9));
verify(parser.handler.setCursorItalic());
});
});
}