Skip to content

bugfix: restore ITU T.416 colon-format colors with colon separators - #239

Open
dylanpulver wants to merge 1 commit into
jquast:masterfrom
dylanpulver:fix-sgr-colon-color-restore
Open

bugfix: restore ITU T.416 colon-format colors with colon separators#239
dylanpulver wants to merge 1 commit into
jquast:masterfrom
dylanpulver:fix-sgr-colon-color-restore

Conversation

@dylanpulver

@dylanpulver dylanpulver commented Sep 1, 2026

Copy link
Copy Markdown

_sgr_state_to_sequence() joins the parsed color parameters with ;. A colon-format color carries the ITU-T T.416 (ISO/IEC 8613-6) §13.1.8 colour space id element — 38:2:<cs>:R:G:B — which the legacy 38;2;R;G;B form has no slot for, so the restored sequence shifts R, G and B one position and leaves a trailing parameter.

before this change:

>>> wcwidth.wrap('\x1b[38:2::255:0:0mred text', width=4)
['\x1b[38:2::255:0:0mred\x1b[0m', '\x1b[38;2;0;255;0;0mtext\x1b[0m']
>>> # feeding that restored prefix back to the library's own parser:
>>> _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;0;255;0;0m').foreground
None

after:

>>> wcwidth.wrap('\x1b[38:2::255:0:0mred text', width=4)
['\x1b[38:2::255:0:0mred\x1b[0m', '\x1b[38:2:0:255:0:0mtext\x1b[0m']

The None is the second half of it: under ECMA-48 §8.3.117 the parameters are applied in order, so the extra 0 at the end is a RESET — the restoration sequence also clears any bold/underline it was carrying for that line. clip() shares the same call site.

38:5:N is unaffected (; and : are interchangeable there), so the change is scoped to the RGB tuple that actually has the extra element.

_parse_sgr_params() knows about the colon form and documents the 6-tuple it returns; _sgr_state_to_sequence() does not. The existing coverage matches that split: test_sgr_state_parse_colors_colon_format asserts the parsed tuple and never re-serializes it, and test_clip_sgr_result_is_self_contained — exactly the invariant that would catch this — is parametrized only over basic 3039 colors. Not from a real workload; found reading escape-sequence handling for values parsed by one path and re-emitted by another.

Alternative I rejected: normalizing the colour space element away at parse time (so 38:2::R:G:B and 38;2;R;G;B produce the same 5-tuple) also fixes it and needs no serializer change — but it changes the documented return of _parse_sgr_params() and fails test_sgr_state_parse_colors_colon_format. Happy to switch if you'd rather have the canonical form.

Verified: pytest tests/ --ignore=tests/test_benchmarks.py — 1262 passed / 4 skipped before, 1263 passed / 4 skipped after (the one new test). Reverting only wcwidth/sgr_state.py and keeping the new test: 1 failed, 24 passed in test_sgr_state.py, foreground: None != (38, 2, 0, 255, 0, 0). Full tox -e lint chain run locally: flake8, pylint 10.00/10, mypy --strict, pydocstyle, codespell, isort all clean.

Not tested: no real terminal was driven. The claim above is about what the emitted sequence denotes under T.416/ECMA-48 and under this library's own parser; how a given emulator resolves a 6-parameter 38;2;… is not something I measured.

AI assistance: this change was written with Claude Code (model Claude Opus 5). I reviewed the diff and ran everything reported above.

_sgr_state_to_sequence() joined the parsed color parameters with ';'.
A colon-format color carries the T.416 colour space element
(38:2:<cs>:R:G:B) that the legacy 38;2;R;G;B form has no slot for, so
the restored sequence shifted R, G and B one position and left a
trailing parameter, which reads as SGR 0.

    >>> wcwidth.wrap('\x1b[38:2::255:0:0mred text', width=4)[1]
    '\x1b[38;2;0;255;0;0mtext\x1b[0m'

Feeding that prefix back to _sgr_state_update() yields foreground=None:
the color is lost, and the trailing 0 also clears any bold/underline in
the same restoration sequence.

Co-authored-by: Claude <[email protected]>
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.

1 participant