writeBytes method
Writes raw bytes from the underlying program to the terminal. This is
the byte-level counterpart of write: UTF-8 is decoded incrementally,
so a multibyte sequence split across two calls is reassembled instead of
being replaced by U+FFFD on both sides (which is what per-chunk
utf8.decode does). Malformed bytes decode to U+FFFD exactly like
utf8.decode(bytes, allowMalformed: true).
A chunk without multibyte content is never decoded at all: bytes below 0x80 are their own code points, so one classifying scan decides between the fast path (bytes go straight to the buffer — no String, no code point list) and the parser. Only chunks that actually contain multibyte UTF-8 pay for decoding, with the control-character scan fused into it.
Note: data may be retained by the parser (when a chunk ends mid
sequence), so do not mutate or reuse the buffer after the call — pass a
fresh buffer per chunk, like socket and file reads do.
Do not interleave writeBytes with write on the same terminal: the two entry points track split sequences separately and interleaving them can reorder output.
Implementation
void writeBytes(Uint8List data) {
if (!_utf8Decoder.hasPendingBytes) {
// Classify the chunk in a single pass. Stops early at the first
// multibyte lead byte — such chunks go through the decoder, which
// re-detects control characters while decoding.
var asciiOnly = true;
var hasControlChars = false;
for (var i = 0; i < data.length; i++) {
final byte = data[i];
if (byte >= 0x80) {
asciiOnly = false;
break;
}
if (byte < 0x20 || byte == 0x7F) {
hasControlChars = true;
}
}
if (asciiOnly) {
if (!hasControlChars && !_parser.hasPendingInput) {
// Fast path: plain text. The bytes are already code points
// (Uint8List is a List<int>), so the buffer consumes them
// zero-copy.
if (data.isNotEmpty) {
_precedingCodepoint = data.last;
}
_buffer.writeCodepoints(data);
_notifyOutput();
return;
}
// Pure ASCII with escapes or controls: the parser consumes the bytes
// as code points directly, again without a decoding pass.
_parser.writeCodepoints(data);
_notifyOutput();
return;
}
}
// Multibyte content (or a sequence still pending from the previous
// chunk): decode incrementally into the scratch buffer.
final codepoints = _decodeScratch;
_utf8Decoder.decodeInto(data, codepoints);
if (!_parser.hasPendingInput && !_utf8Decoder.lastChunkHasControlChars) {
// Fast path: decoded text with nothing the parser would dispatch on.
if (codepoints.isNotEmpty) {
_precedingCodepoint = codepoints.last;
}
_buffer.writeCodepoints(codepoints);
codepoints.clear();
_notifyOutput();
return;
}
// The parser's queue stores the list, so hand over ownership and start a
// fresh scratch buffer for the next chunk.
_decodeScratch = <int>[];
_parser.writeCodepoints(codepoints);
_notifyOutput();
}