Skip to content

fix(tui): decode ANSI alpha-channel encoding in syntax themes#13382

Merged
joshka-oai merged 4 commits intoopenai:mainfrom
fcoury:fix/12890-ansi-themes
Mar 4, 2026
Merged

fix(tui): decode ANSI alpha-channel encoding in syntax themes#13382
joshka-oai merged 4 commits intoopenai:mainfrom
fcoury:fix/12890-ansi-themes

Conversation

@fcoury
Copy link
Contributor

@fcoury fcoury commented Mar 3, 2026

Problem

The ansi, base16, and base16-256 syntax themes are designed to emit ANSI palette colors so that highlighted code respects the user's terminal color scheme. Syntect encodes this intent in the alpha channel of its Color struct — a convention shared with bat — but convert_style was ignoring it entirely, treating every foreground color as raw RGB. This caused ANSI-family themes to produce hard-coded RGB values (e.g. Rgb(0x02, 0, 0) instead of Green), defeating their purpose and rendering them as near-invisible dark colors on most terminals.

Reported in #12890.

Mental model

Syntect themes use a compact encoding in their Color struct:

alpha Meaning of r Mapped to
0x00 ANSI palette index (0–255) RtColor::BlackGray for 0–7, Indexed(n) for 8–255
0x01 Unused (sentinel) None — inherit terminal default fg/bg
0xFF True RGB red channel RtColor::Rgb(r, g, b)
other Unexpected RtColor::Rgb(r, g, b) (silent fallback)

This encoding is a bat convention that three bundled themes rely on. The new convert_syntect_color function decodes it; ansi_palette_color maps indices 0–7 to ratatui's named ANSI variants.

macOS - Dark macOS - Light Windows - ansi Windows - base16
macos-dark macos-light windows-ansi windows-base16

Non-goals

  • Background color decoding — we intentionally skip backgrounds to preserve the terminal's own background. The decoder supports it, but convert_style does not apply it.
  • Italic/underline changes — those remain suppressed as before.
  • Custom .tmTheme support for ANSI encoding — only the bundled themes use this convention.

Tradeoffs

  • The alpha-channel encoding is an undocumented bat/syntect convention, not a formal spec. We match bat's behavior exactly, trading formality for ecosystem compatibility.
  • Indices 0–7 are mapped to ratatui's named variants (Black, Red, …, Gray) rather than Indexed(0)Indexed(7). This lets terminals apply bold/bright semantics to named colors, which is the expected behavior for ANSI themes, but means the two representations are not perfectly round-trippable.

Architecture

All changes are in codex-rs/tui/src/render/highlight.rs, within the style-conversion layer between syntect and ratatui:

syntect::highlighting::Color
  └─ convert_syntect_color(color)  [NEW — alpha-dispatch]
       ├─ a=0x00 → ansi_palette_color()  [NEW — index→named/indexed]
       ├─ a=0x01 → None (terminal default)
       ├─ a=0xFF → Rgb(r,g,b) (standard opaque path)
       └─ other  → Rgb(r,g,b) (silent fallback)

convert_style delegates foreground mapping to convert_syntect_color instead of inlining the Rgb(r,g,b) conversion. The core highlighter is refactored into highlight_to_line_spans_with_theme (accepts an explicit theme reference) so tests can highlight against specific themes without mutating process-global state.

ANSI-family theme contract

The ANSI-family themes (ansi, base16, base16-256) rely on upstream alpha-channel encoding from two_face/syntect. We intentionally do not validate this contract at runtime — if the upstream format changes, the ansi_themes_use_only_ansi_palette_colors test catches it at build time, long before it reaches users. A runtime warning would be unactionable noise.

Warning copy cleanup

User-facing warning messages were rewritten for clarity:

  • Removed internal jargon ("alpha-encoded ANSI color markers", "RGB fallback semantics", "persisted override config")
  • Dropped "syntax" prefix from "syntax theme" — users just think "theme"
  • Downgraded developer-only diagnostics (duplicate override, resolve fallback) from warn to debug

Observability

  • The ansi_themes_use_only_ansi_palette_colors test enforces the ANSI-family contract at build time.
  • The snapshot test provides a regression tripwire for palette color output.
  • User-facing warnings are limited to actionable issues: unknown theme names and invalid custom .tmTheme files.

Tests

  • Unit tests for each alpha branch: alpha=0x00 with low index (named color), alpha=0x00 with high index (Indexed), alpha=0x01 (terminal default), unexpected alpha (falls back to RGB), ANSI white → Gray mapping.
  • Integration test: ansi_family_themes_use_terminal_palette_colors_not_rgb — highlights a Rust snippet with each ANSI-family theme and asserts zero Rgb foreground colors appear.
  • Snapshot test: ansi_family_foreground_palette_snapshot — records the exact set of unique foreground colors each ANSI-family theme produces, guarding against regressions.
  • Warning validation tests: verify user-facing warnings for missing custom themes, invalid .tmTheme files, and bundled theme resolution.

Test plan

  • cargo test -p codex-tui passes all new and existing tests
  • Select ansi, base16, or base16-256 theme and verify code blocks render with terminal palette colors (not near-black RGB)
  • Select a standard RGB theme (e.g. dracula) and verify no regression in color output

Copilot AI review requested due to automatic review settings March 3, 2026 19:54
@fcoury
Copy link
Contributor Author

fcoury commented Mar 3, 2026

@codex review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes ANSI-family syntax themes in the TUI by decoding syntect’s alpha-channel ANSI palette encoding (as used by bat), so ansi, base16, and base16-256 themes emit terminal palette colors instead of near-black hardcoded RGB values.

Changes:

  • Added syntect color decoding that maps alpha-encoded ANSI indices to ratatui named/indexed colors, preserving “terminal default” semantics.
  • Refactored the highlighter core to accept an explicit theme reference to support theme-specific tests without mutating global state.
  • Added contract validation + warnings for ANSI-family themes and introduced snapshot/regression tests for palette outputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
codex-rs/tui/src/render/highlight.rs Implements alpha-based syntect→ratatui color decoding, contract validation/warnings, refactors highlighting for testability, and adds extensive tests.
codex-rs/tui/src/render/snapshots/codex_tui__render__highlight__tests__ansi_family_foreground_palette.snap Adds snapshot output to lock in expected ANSI-family foreground palette behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chatgpt-codex-connector
Copy link
Contributor

Codex Review: Didn't find any major issues. 🚀

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fcoury added 3 commits March 3, 2026 22:22
Replace the warn-once bitmap in `tui/src/render/highlight.rs` with a
simple warning path that logs every unexpected alpha marker.

This removes global atomic state and keeps the alpha decoding logic
straightforward while preserving RGB fallback behavior for unknown values.
Keep `convert_syntect_color` resilient by treating unexpected alpha values as
plain RGB without emitting runtime warnings.

Built-in themes can legitimately contain these values, so warning spam adds
noise without improving correctness or user actionability.
Update style-conversion tests in `tui/src/render/highlight.rs` to use
`matches!` pattern assertions instead of constructing `RtColor::Indexed`
and `RtColor::Rgb` values directly.

This keeps the assertions equivalent while satisfying the crate-level
`clippy::disallowed_methods` policy in test code.
@fcoury fcoury force-pushed the fix/12890-ansi-themes branch from 02f3d07 to a90aee6 Compare March 4, 2026 01:23
Copy link
Collaborator

@etraut-openai etraut-openai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good. I left one comment — not sure the warning is needed here. Seems to be adding a lot of code for little benefit.

Copy link
Collaborator

@joshka-oai joshka-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a module grows a large percentage of code like this, it's worth considering whether the code can be split into a new more focused module (e.g. highlight/syntect_theme.rs (or whatever makes sense - theme.rs?). This helps keep the tests, and docs about why and how this code works pretty well self-contained / local / collocated and makes the additive and changed parts of a change like this more obvious. A bunch of the mental model docs in the PR probably belong in the code too.

Stamped - mainly because this improves how this looks significantly, so code nits are more about making it read nicely.

Reword user-facing theme warnings to be clearer and less jargon-heavy.
Remove runtime ANSI-family contract diagnostic (caught by tests, not
actionable by users). Downgrade duplicate-override and resolve-fallback
messages to debug level since they are developer-only breadcrumbs.
@joshka-oai joshka-oai merged commit 98923e5 into openai:main Mar 4, 2026
65 of 75 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Mar 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants