parseAnsi function
Splits text into runs of visible characters, each carrying the SGR state
that was in effect when it was written.
Every escape sequence that is not SGR is dropped. A child writes plenty of
them — mason_logger's own Progress brackets each frame with ESC[?7l
and ESC[2K, and revali dev clears the screen with ESC[2J ESC[0;0H —
and they are instructions to a terminal, addressed to a screen the child
believes it owns. A pane inside another program's frame is not that screen,
so printing them is why [2J[0;0H shows up as characters.
Dropping them is the answer for all but one. kClearScreen says something
a pane can act on, and by the time text reaches here it already has:
ServiceSession.ingest acts on it and hands this only what survived. So a
ESC[2J seen here is one nothing claimed, and dropping it is right.
Adjacent runs with the same state are merged, so a line the child wrapped in a redundant reset does not become two spans that render identically.
Implementation
List<AnsiSpan> parseAnsi(String text) {
final spans = <AnsiSpan>[];
final buffer = StringBuffer();
int? color;
var bold = false;
var dim = false;
void flush() {
if (buffer.isEmpty) return;
// Merged rather than appended when the state has not moved: two spans that
// paint the same are one span that paints the same, and a renderer given
// the second has to decide all over again what to do with it.
if (spans.isNotEmpty &&
spans.last.color == color &&
spans.last.bold == bold &&
spans.last.dim == dim) {
final merged = spans.removeLast();
spans.add(
AnsiSpan('${merged.text}$buffer', color: color, bold: bold, dim: dim),
);
} else {
spans.add(AnsiSpan('$buffer', color: color, bold: bold, dim: dim));
}
buffer.clear();
}
var index = 0;
while (index < text.length) {
if (text.codeUnitAt(index) != _esc) {
buffer.writeCharCode(text.codeUnitAt(index));
index++;
continue;
}
final sequence = _sequenceAt(text, index);
if (sequence == null) {
// A trailing `ESC` with nothing after it: the chunk split mid-sequence.
// Dropped rather than printed — the rest of it is in the next chunk and
// a lone escape byte is not a character anyone meant to see.
break;
}
// The state changes *between* runs, so what came before it keeps the old
// one.
if (_sgrParameters(text, sequence) case final parameters?) {
flush();
for (final parameter in parameters) {
switch (parameter) {
case 0:
color = null;
bold = false;
dim = false;
case 1:
bold = true;
case 2:
dim = true;
case 22:
bold = false;
dim = false;
case 39:
color = null;
case >= 30 && <= 37:
case >= 90 && <= 97:
color = parameter;
}
}
}
index = sequence.end;
}
flush();
return spans;
}