Skip to content

fix: normalize invalid RGB in keyframes - #201

Open
vitorbaptista wants to merge 1 commit into
mainfrom
fix/avt-keyframe-rgb
Open

fix: normalize invalid RGB in keyframes#201
vitorbaptista wants to merge 1 commit into
mainfrom
fix/avt-keyframe-rgb

Conversation

@vitorbaptista

@vitorbaptista vitorbaptista commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • normalize avt's ambiguous colon RGB parameters where keyframes are generated
  • cover the spelling in the existing keyframe e2e test

Root cause

avt serializes truecolor as 38:2:R:G:B (parser.rs:961 on main; color.rs:16 in the pinned 0.16). T.416 does not allow that form: the color-space field is positional, not optional, so a reader following the spec takes G and B for R and G and defaults B to 0 - a periodic keyframe repaints an otherwise correct terminal in yellow-green.

avt's own parser accepts the form it emits (its tests parse 38:2:1:2:3 and 38:2::1:2:3 to the same color), so its dumps round-trip cleanly and the damage only appears in a real emulator. It is still present on avt main, so upgrading would not fix it - worth reporting upstream separately.

xterm.js is not the place to fix this. xtermjs/xterm.js#5792 reported exactly this and was closed as working-as-intended, citing T.416 and DEC STD 070: an omitted middle parameter must still be separated, so positions never shift with parameter count. Of the three spellings in the wild, 38;2;R;G;B is non-conformant but universally supported, 38:2::R:G:B is the only strictly correct one, and 38:2:R:G:B - avt's - is simply invalid. The emitter is what has to change.

Approach

The rewrite happens at the one place we generate those bytes. Two facts make it a three-line transform rather than a pattern match:

  • screen text can never contain an ESC (avt's parser consumes control chars instead of storing them as cells), so a CSI-bounded scan cannot touch user text;
  • the only colons avt puts inside an SGR are colors, so every colon in a ...m run can become a semicolon. 38:5:N folds to 38;5;N, which is equally valid.

Vt::dump() also appends in-flight parser state as a final-byte-less CSI, so the end of the dump terminates a sequence too - otherwise a keyframe that fires mid-SGR ships an un-normalized trailing color.

Params that already spell the empty color space (38:2::R:G:B) pass through untouched, so if avt is fixed upstream to emit the valid colon form, bumping the dependency does not silently re-break the colors by flattening :: into ;;.

Deliberately not fixed in the viewer

An earlier revision of this PR also rewrote SGR in room.js for already-released broadcasters. Dropped after review:

  • the viewer cannot distinguish an old broadcaster's keyframe from any application's live output, so it rewrote every session's bytes - contradicting "raw bytes end to end";
  • measured ~4.1 ms per 31 KB colored frame, synchronous in onmessage/the decrypt chain (~130 ms block on a 1 MB history replay); the indexOf(0x1b) fast path misses on essentially every TUI frame;
  • the bounded carry needed for split CSIs silently gave up past its cap, passing through exactly the ambiguous form it existed to fix;
  • and it could not fix the unterminated trailing CSI at all, since its regex needs the closing m.

Trade-off: released binaries in the wild keep emitting the ambiguous form, so their keyframes stay miscolored for late joiners until the user upgrades. Cosmetic, self-healing on upgrade, and not worth a permanent per-frame cost on every viewer.

Testing

  • cargo clippy --all-targets clean
  • normalizer verified against real avt::Vt dumps: fg/bg RGB normalized, 38:5:196 folded, literal screen text HI 38:2:1:2:3 untouched, unterminated trailing CSI normalized
  • normalizer also verified to leave the valid 38:2::R:G:B form byte-for-byte unchanged
  • full e2e suite: 259 passed

Fixes #189

@vitorbaptista vitorbaptista changed the title fix: normalize ambiguous RGB keyframes fix: normalize ambiguous RGB in keyframes Aug 17, 2026
avt serializes truecolor as `38:2:R:G:B`, which T.416 does not allow: the
color-space field is positional, not optional, so a reader following the
spec takes G and B for R and G and defaults B to 0 - a keyframe repaints
an otherwise correct terminal in yellow-green. avt's own parser accepts
the form it emits, so its dumps round-trip cleanly and the damage only
appears in a real emulator; it is still there on avt main, so upgrading
would not fix it.

xterm.js is not the place to fix this - xtermjs/xterm.js#5792 was closed
as working-as-intended, with the spec reading that positions are fixed
and only `38;2;R;G;B` and `38:2::R:G:B` are acceptable. So the emitter
has to change, and until avt does, we change it at the one place we
generate those bytes.

Screen text can never contain an ESC and the only colons avt puts inside
an SGR are colors, so replacing every colon in a `...m` run is enough -
no need to recognize the RGB shape. `Vt::dump` reproduces in-flight
parser state as a final-byte-less CSI, so the end of the dump terminates
a sequence too. Params already spelling the empty color space are left
alone, so a future avt emitting the valid colon form is not re-broken.

Deliberately not fixed in the viewer: it cannot tell an old broadcaster's
keyframe from any application's live output, so it would silently rewrite
every session's bytes, cost a per-frame string round trip on the render
path, and still miss the unterminated trailing CSI.

Fixes #189

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vitorbaptista vitorbaptista changed the title fix: normalize ambiguous RGB in keyframes fix: normalize invalid RGB in keyframes Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

xterm.js mis-renders colon-form truecolor without the colour-space slot (38:2:R:G:B) - upstream declined it (xtermjs/xterm.js#5792)

1 participant