fix: normalize invalid RGB in keyframes - #201
Open
vitorbaptista wants to merge 1 commit into
Open
Conversation
vitorbaptista
force-pushed
the
fix/avt-keyframe-rgb
branch
from
August 17, 2026 21:12
c40043b to
e6c3c52
Compare
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
force-pushed
the
fix/avt-keyframe-rgb
branch
from
August 18, 2026 19:53
e6c3c52 to
5ba929d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Root cause
avt serializes truecolor as
38:2:R:G:B(parser.rs:961on main;color.rs:16in 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:3and38:2::1:2:3to 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;Bis non-conformant but universally supported,38:2::R:G:Bis the only strictly correct one, and38: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:
...mrun can become a semicolon.38:5:Nfolds to38;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.jsfor already-released broadcasters. Dropped after review:onmessage/the decrypt chain (~130 ms block on a 1 MB history replay); theindexOf(0x1b)fast path misses on essentially every TUI frame;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-targetscleanavt::Vtdumps: fg/bg RGB normalized,38:5:196folded, literal screen textHI 38:2:1:2:3untouched, unterminated trailing CSI normalized38:2::R:G:Bform byte-for-byte unchangedFixes #189