AnsiByteBreakdown.analyze constructor

AnsiByteBreakdown.analyze(
  1. 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,
  );
}