AnsiByteBreakdown.analyze constructor
AnsiByteBreakdown.analyze(
- String data
Categorizes a single emitted ANSI string by UTF-8 byte count.
Handles the CSI grammar AnsiRenderer emits: ESC [ optionally followed
by a ? private marker, then parameter/intermediate bytes, then a final
byte in 0x40..0x7E. A bare ESC not starting a CSI is conservatively
counted toward other.
Implementation
factory AnsiByteBreakdown.analyze(String data) {
var content = 0;
var sgr = 0;
var cursor = 0;
var sync = 0;
var session = 0;
var other = 0;
var image = 0;
final contentRun = StringBuffer();
void flushContent() {
if (contentRun.isEmpty) return;
content += utf8.encode(contentRun.toString()).length;
contentRun.clear();
}
const esc = 0x1B;
const csi = 0x5B; // '['
const privateMarker = 0x3F; // '?'
var i = 0;
final n = data.length;
while (i < n) {
final cu = data.codeUnitAt(i);
if (cu == 0x08 || cu == 0x0A || cu == 0x0D) {
// BS, LF, and CR are printable-stream C0 controls that terminals use
// as cursor movement. Fleury's sanitized cell content cannot contain
// them, and peer captures use them for the same cursor-control role.
flushContent();
cursor += 1;
i++;
continue;
}
if (cu != esc) {
contentRun.writeCharCode(cu);
i++;
continue;
}
flushContent();
final start = i;
i++; // consume ESC
if (i < n) {
final intro = data.codeUnitAt(i);
// String escapes carry inline-image protocol payloads: APC (ESC _,
// Kitty graphics), DCS (ESC P, Sixel or a tmux image-passthrough
// envelope), OSC 1337 (ESC ], iTerm2). Consume through the string
// terminator (ST `ESC \` or BEL) as one run. Non-image OSC (clipboard
// 52, hyperlink 8) stays 'other'.
if (intro == 0x5F /* _ APC */ || intro == 0x50 /* P DCS */ ) {
i = _consumeStringEscape(data, i + 1, n);
image += i - start;
continue;
}
if (intro == 0x5D /* ] OSC */ ) {
final isImage = _oscIsImage(data, i + 1, n);
i = _consumeStringEscape(data, i + 1, n);
if (isImage) {
image += i - start;
} else {
other += i - start;
}
continue;
}
}
if (i >= n || data.codeUnitAt(i) != csi) {
// Not a CSI; count the ESC alone as overhead and continue.
other += 1;
continue;
}
i++; // consume '['
var private = false;
var kittyMarker = false;
if (i < n && data.codeUnitAt(i) == privateMarker) private = true;
if (i < n) {
final marker = data.codeUnitAt(i);
// '>' (push) / '<' (pop) — the Kitty keyboard-protocol prefixes.
if (marker == 0x3E || marker == 0x3C) kittyMarker = true;
}
final paramStart = i;
var finalByte = 0;
while (i < n) {
final c = data.codeUnitAt(i);
i++;
if (c >= 0x40 && c <= 0x7E) {
finalByte = c;
break;
}
}
final len = i - start; // CSI is ASCII: byte length == code-unit length
if (private && (finalByte == 0x68 || finalByte == 0x6C)) {
// DEC private mode set/reset: synchronized output (2026) is the
// per-frame wrapper; every other private mode is session lifecycle.
if (_paramsAreSynchronizedOutput(data, paramStart + 1, i - 1)) {
sync += len;
} else {
session += len;
}
} else if (kittyMarker && finalByte == 0x75) {
session += len; // u: Kitty keyboard push/pop
} else if (_isCursorCsiFinal(finalByte)) {
cursor += len;
} else if (finalByte == 0x6D) {
sgr += len; // m
} else {
other += len;
}
}
flushContent();
return AnsiByteBreakdown(
content: content,
sgr: sgr,
cursor: cursor,
sync: sync,
session: session,
other: other,
image: image,
);
}