capture CSI intermediate bytes; stop bare-final mis-dispatch (T-123)

_consumeCsi silently discarded intermediate bytes (0x20-0x2f), so an
intermediate-bearing sequence dispatched on its bare final byte —
`CSI 5 SP @` (VT420 scroll-left) ran as "insert 5 blank characters",
and `CSI Ps SP q` (DECSCUSR) could collide with any future bare-q
handler. The parser now records intermediates on the CSI scratch
object and routes any sequence carrying them to unknownCSI, since no
intermediate form is implemented yet.

Implementing DECSCUSR itself (cursor shape + renderer support) is
filed as T-397.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 02:55:58 +02:00
co-authored by Claude Fable 5
parent f8958fd372
commit c3dd1d3e3f
5 changed files with 114 additions and 8 deletions
+19 -8
View File
@@ -217,7 +217,11 @@ class EscapeParser extends _EscapeParserBase with _CsiHandlers, _ModeHandlers, _
final consumed = _consumeCsi();
if (!consumed) return false;
final csiHandler = _csiHandlers[_csi.finalByte];
// An intermediate byte changes the meaning of the final byte
// (`CSI 5 SP @` is scroll-left, not insert-blank). None of the
// intermediate forms are implemented, so report them as unknown
// rather than mis-dispatching on the bare final byte.
final csiHandler = _csi.intermediates.isEmpty ? _csiHandlers[_csi.finalByte] : null;
if (csiHandler == null) {
handler.unknownCSI(_csi.finalByte);
@@ -237,6 +241,7 @@ class EscapeParser extends _EscapeParserBase with _CsiHandlers, _ModeHandlers, _
_csi.params.clear();
_csi.subParam.clear();
_csi.intermediates.clear();
// test whether the csi is a `CSI ? Ps ...` or `CSI Ps ...`
final prefix = _queue.peek();
@@ -290,8 +295,13 @@ class EscapeParser extends _EscapeParserBase with _CsiHandlers, _ModeHandlers, _
continue;
}
if (char >= Ascii.space && char <= Ascii.slash) {
_csi.intermediates.add(char);
continue;
}
if (char > Ascii.NULL && char < Ascii.num0) {
// intermediates.add(char);
// Other C0 controls embedded in a CSI: ignore, as before.
continue;
}
@@ -341,11 +351,7 @@ class EscapeParser extends _EscapeParserBase with _CsiHandlers, _ModeHandlers, _
}
class _Csi {
_Csi({
required this.params,
required this.finalByte,
// required this.intermediates,
});
_Csi({required this.params, required this.finalByte});
int? prefix;
@@ -356,7 +362,12 @@ class _Csi {
final List<bool> subParam = [];
int finalByte;
// final List<int> intermediates;
/// Intermediate bytes (0x200x2f) between the parameters and the final
/// byte — `SP` in `CSI Ps SP q` (DECSCUSR), `!` in `CSI ! p` (DECSTR).
/// They change the meaning of the final byte, so dispatch must not fall
/// through to the bare-final handler when any are present.
final List<int> intermediates = [];
@override
String toString() {