Skip to content

Catppuccin base fidelity, full bg contract, and review-driven fixes - #3

Merged
hyperb1iss merged 12 commits into
mainfrom
nova/issue-2-bg-tokens
Sep 2, 2026
Merged

Catppuccin base fidelity, full bg contract, and review-driven fixes#3
hyperb1iss merged 12 commits into
mainfrom
nova/issue-2-bg-tokens

Conversation

@hyperb1iss

@hyperb1iss hyperb1iss commented Sep 2, 2026

Copy link
Copy Markdown
Owner

🎨 Catppuccin base fidelity, the full bg contract, and review-driven fixes

Closes #2. Branched from main at v0.4.1. Twelve commits, each one revertable on its own; the Catppuccin remap and the contract change are the two that consumers will notice.

💡 What this is

Issue #2 asked two things: why the dark Catppuccin flavors map bg.base to crust when Catppuccin itself calls base the background pane, and why bg.elevated has no constant when every builtin defines it. Both were defects, not design. The naive fix is to swap two lines in three TOML files. What the repo actually needed was to say what the bg.* family means, because the token guide called bg.base the "main background" while three of the four Catppuccin flavors contradicted the fourth, and the contract had silently drifted two tokens and one style behind what every theme ships.

The rest of the branch is what a full-project review turned up alongside: a loader panic on malformed hex, a red clippy gate on current stable, test targets that only compiled with all features on, four confirmed widget bugs, one terminal-adapter mis-map, and a docs site with a broken CI badge, a missing iced page, and several snippets that do not compile.

🤔 Why we need it

Every one of the 39 builtin themes maps bg.base to the deepest neutral in its palette and steps the other bg.* tokens toward the text color. Catppuccin's design goes the other way for secondary panes: base is the editor background, and mantle and crust are the recessed sidebars and status areas. Mocha, Macchiato, and Frappé painted the app canvas with crust, so an Opaline app on a Catppuccin terminal showed #11111b where the user's terminal is #1e1e2e. Latte already had it right (bg.base = base, bg.panel = mantle), which made the dark flavors inconsistent with their own sibling as well as with upstream.

The contract gap was the same shape: bg.elevated, bg.active, and cursor_line exist in all 39 themes, but nothing in opaline::names, the contract test, or the docs mentioned them, so consumers reached for raw strings and could not rely on the tokens existing.

What is deliberately not done here: the other dark families (Tokyo Night, Dracula, Nord, Everforest Dark, Gruvbox) still map bg.base to the upstream sidebar color rather than the iconic editor background. That is the same class of question as #2 and it deserves its own decision rather than a drive-by inside this branch. See follow-ups.

🎯 The invariant

bg.base is the canvas an app paints first. bg.panel is one step away from it in whichever direction the source palette prefers, and bg.highlight, bg.elevated, and bg.active step toward the text color. The contract test now builds its required lists from opaline::names, so a constant cannot exist without being enforced and a token cannot be enforced without having a constant.

🛠️ How it works

1. Catppuccin remap (src/builtins/catppuccin_{mocha,macchiato,frappe}.toml)

Flavor bg.base bg.panel bg.code bg.highlight
Mocha #1e1e2e #181825 #11111b #313244
Macchiato #24273a #1e2030 #181926 #363a4f
Frappé #303446 #292c3c #232634 #414559
Latte #eff1f5 #e6e9ef #dce0e8 #ccd0da

Only the three bg.base/bg.panel/bg.code lines changed per dark flavor; the surface ladder (highlight, elevated, active, selection) is untouched. The fidelity test catppuccin_flavors_map_base_to_main_background in tests/builtins_tests.rs pins all four flavors by hex.

2. The contract (src/names.rs, tests/builtins_tests.rs)

tokens::BG_ELEVATED, tokens::BG_ACTIVE, and styles::CURSOR_LINE are new. REQUIRED_TOKENS, REQUIRED_STYLES, and REQUIRED_GRADIENTS are now arrays of names:: constants instead of string literals, and contract_sizes_match_documentation asserts 28 / 14 / 5. Every count mention in README, AGENTS.md, CONTRIBUTING.md, and the docs site moved from 26/13 to 28/14, and docs/guide/tokens.md gained the ladder description above plus rows for the two tokens.

3. Loader panic (src/color.rs)

from_hex checked byte length and then sliced by byte index. A 7-byte string with a multibyte character ("#aé000") passed the length check and panicked at a char boundary. Palette values feed straight into this, so a malformed user theme file crashed any app using discovery. Non-ASCII input is now rejected as InvalidHex before any slicing. Tests cover the color path and the loader path.

4. Theme selector (src/widgets/theme_selector.rs)

Five defects, each with a regression test that fails against the previous code:

  • Key-release events (Windows, kitty protocol) fired every action twice. KeyEventKind::Release now returns Noop.
  • Ctrl, Alt, and Super chords arrived as plain characters and went into the filter, eating Ctrl+C. Those chords now return Noop.
  • Enter as the first key returned Select(id) without applying that theme, because the preview only ran on navigation. Enter now calls apply_preview first.
  • The sticky section header always rendered the list's first section, so a window scrolled into the light themes read "Dark Themes". Only visible items feed the header state now, and a buffer-render test pins it.
  • A heading whose section started exactly at the viewport edge rendered on the last row with nothing beneath it. A heading now needs room for its first item, and a height-sweep test pins that.

5. Everything else

  • src/adapters/owo_colors.rs: rapid_blink maps to blink_fast() (SGR 6) instead of collapsing into blink() (SGR 5), matching crossterm and ratatui.
  • src/adapters/egui.rs: twelve Stroke::new(1.0, …) literals gained _f32. They tripped the future-incompat float_literal_f32_fallback lint on current stable, which made clippy -- -D warnings exit 101.
  • tests/*.rs: gradient, ratatui, and builtin-dependent tests are gated on their features, so cargo test compiles under any feature selection. CI only runs --all-features, which is why this was invisible.
  • examples/theme_showcase.rs: the style samples panel rendered eight names no theme defines; it now samples the 14 contract styles and reads the theme count from BUILTIN_COUNT.
  • deny.toml: CI's cargo-deny job fails on every branch today because RUSTSEC-2026-0192 (ttf-parser unmaintained) landed in the advisory database after main's last run. It reaches opaline only through egui 0.33 and is a maintenance notice, so it is acknowledged next to the existing time entry with the exit path recorded.
  • Docs: README CI badge pointed at ci.yml (the workflow is cicd.yml); the docs.rs feature table listed 6 of 13 features; the iced adapter had no guide page; the ratatui guide imported Stylize where the method lives on Styled; two CircularReference patterns omitted token; the CSS guide showed Catppuccin hex values for Theme::default(); the egui table claimed text.primary drives override_text_color; missing-token fallbacks were described as magenta (they are neutral gray).

🧪 Validation

All receipts against b70c98f.

  • cargo fmt --all --check → clean.
  • cargo clippy --all-targets --all-features → zero warnings (was 12 on main).
  • cargo test --all-features → 220 passed, 0 failed (210 on main; new tests cover the hex panic, the Catppuccin mapping, contract sizes, SGR 5 vs 6, and the five widget fixes).
  • cargo test --no-default-features → 66 passed. --features builtin-themes → 83. --features gradients → 90. --features ratatui → 66. All of these failed to compile on main.
  • cargo doc --all-features --no-deps → zero warnings.
  • cargo deny --all-features check → advisories ok, bans ok, licenses ok, sources ok (advisories failed on main before the ignore entry).
  • Falsifier check: with src/widgets/theme_selector.rs reverted to main, the five new selector tests fail and the six existing ones pass.
  • An independent verification pass over the branch (62 adversarial from_hex inputs, 7 loader inputs, 2008 selector renders across five filters, eight heights, and full cursor sweeps) reported 0 panics, 0 cells drawn outside the area, and 0 missing cursor markers, and confirmed each new test fails against a copy of main.
  • The from_hex panic was reproduced on main (load_from_str with palette value "#aé000"byte index 3 is not a char boundary) before the fix.

⚠️ The README showcase screenshots for Catppuccin Mocha and Latte are not regenerated; Mocha's canvas is now #1e1e2e rather than #11111b, so the image is one shade off until a rerender.

🔍 What reviewers should focus on

  • The Catppuccin decision itself. For the dark flavors, bg.panel now sits darker than bg.base (mantle under base). Kanagawa already ships that way and Latte already did, but it is a visible change for anyone painting panels with bg.panel on a Catppuccin theme.
  • render_theme_entries in src/widgets/theme_selector.rs. The skip-before-header reorder is small, but scroll math depends on count_section_headers and the rendered header count staying equal.
  • from_hex now returns InvalidHex for non-ASCII input even when the character count is wrong; previously such input either panicked or returned InvalidLength.
  • The strongest objection would be a consumer that computed contrast assuming bg.panel is lighter than bg.base. I found none in this repo, git-iris, or hypercolor, but say so if you have one.

📌 Follow-ups (deliberate non-fixes)

  • Decide whether Tokyo Night, Dracula, Nord, Everforest Dark, and Gruvbox should map bg.base to their iconic editor background the way Catppuccin now does. Same class as "bg.base" and "bg.panel" swapped for catppuccin themes? #2, wider blast radius.
  • ThemeSelectorState has no restore_original() for close paths other than Esc, and no constructor that takes an app-specific theme list. Both are additive API and want a design call.
  • Discovery uses the raw file stem as the id while builtins hyphenate, so rose_pine.toml in a user dir does not shadow rose-pine. Normalising the stem is a one-liner but changes ids users may already rely on.
  • Theme::gradient_text(gradient, text) and cli_gradient(text, gradient) take the same two &str in opposite order; fixing it is a breaking change.
  • OpalineError::MissingSection is never constructed. Removing it and marking the enum #[non_exhaustive] belongs in the next minor.
  • Bump egui once a release drops ttf-parser, then remove the RUSTSEC-2026-0192 ignore.
  • tests/adapter_tests.rs is gated on ratatui and gradients together, so a ratatui-only build exercises none of the From impl tests. Splitting the gate per test is mechanical.

🤖 Generated with Claude Code

https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M

hyperb1iss and others added 11 commits September 1, 2026 23:09
from_hex sliced the input by byte index after checking only the byte
length. A 7-byte string containing a multibyte character (for example
"#aé000") passed the length check and then panicked at a char boundary
inside the slice. Because palette values flow straight into from_hex,
a malformed user theme file could crash any app using the discovery
feature instead of surfacing an OpalineError.

Reject non-ASCII input up front as InvalidHex so the byte-index slices
below are always valid. Regression tests cover from_hex directly and
the loader path that reads palette values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
The three dark Catppuccin flavors mapped bg.base to crust and bg.panel
to base, while Latte already mapped bg.base to base and bg.panel to
mantle. Upstream Catppuccin defines base as the main background pane
and mantle/crust as the secondary panes, so an app that paints its
canvas with bg.base showed crust (#11111b for Mocha) instead of the
color every Catppuccin terminal already uses (#1e1e2e).

Align Mocha, Macchiato, and Frappé with Latte and with upstream:
bg.base = base, bg.panel = mantle, bg.code = crust. The surface ladder
(highlight, elevated, active, selection) is unchanged. A fidelity test
pins the mapping for all four flavors so it cannot drift again.

Reported in GitHub issue #2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
All 39 builtin themes already define the bg.elevated and bg.active
tokens and the cursor_line style, but none of them had a constant in
opaline::names and none were in the documented or tested contract.
Consumers had to reach for raw strings and could not rely on the
tokens existing.

Add BG_ELEVATED, BG_ACTIVE, and CURSOR_LINE constants, extend the
contract to 28 tokens and 14 styles, and rewrite the contract test
lists in terms of the names constants so the two can never drift
again. A size assertion pins the numbers the docs quote.

The token guide now describes the bg.* family as a layering ladder
(bg.base is the canvas, bg.panel the secondary pane, and the rest step
toward the text color) and explains why some palettes recess panels
while others raise them. It also corrects a stale claim that missing
tokens fall back to magenta; the fallback is neutral gray.

Reported in GitHub issue #2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
Stroke::new takes impl Into<f32>, and an unsuffixed float literal now
triggers the "falling back to f32" future-incompatibility lint on
current stable (rust-lang/rust#154024). Twelve call sites produced
twelve warnings on every clippy run and will become hard errors in a
future release. Suffix the literals so inference has nothing to guess.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
cargo test only compiled with the default or full feature set because
several test files imported gradient, ratatui, and builtin APIs
unconditionally. Any narrower feature selection failed to build the
test targets even though the library itself compiles under every
combination. CI runs --all-features so the gap went unnoticed.

Gate adapter_tests, gradient_tests, and builtins_tests at the crate
level, and gate the individual gradient tests inside loader_tests,
resolver_tests, and builtins_tests. Verified with cargo test under
--no-default-features, builtin-themes only, gradients only, ratatui
only, and --all-features.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
A docs audit against the current source turned up a cluster of drift:

- The README CI badge pointed at ci.yml; the workflow is cicd.yml, so
  the badge rendered "repo or workflow not found".
- The crate-level feature table on docs.rs listed 6 of 13 features and
  the adapter list omitted iced and the colored crate.
- The iced adapter had no guide page and no sidebar entry even though
  README and the feature reference advertise it. Several adapter lists
  and the installation feature table also omitted iced.
- The ratatui guide imported Stylize where the method lives on Styled,
  and two CircularReference patterns omitted the token field, so those
  snippets did not compile.
- The CSS guide showed Catppuccin hex values and a diff-added class for
  Theme::default(), which is SilkCircuit Neon and has no such class.
  The example now shows real generator output.
- The egui guide claimed text.primary drives override_text_color; the
  adapter leaves that None and uses text.primary for the open-widget
  stroke. It also claimed every color property is overridden.
- Missing-token fallbacks were described as magenta in two places; the
  fallback is neutral gray.
- The widgets feature also enables ratatui, name-based global loading
  needs builtin-themes, the syntect settings table missed three fields,
  the error table missed ThemeNotFound, and the custom-themes guide
  implied tilde expansion that the loader does not perform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
The owo-colors adapter collapsed both blink modifiers into blink(),
which emits SGR 5 (slow blink). owo-colors exposes blink_fast() for
SGR 6, and the crossterm and ratatui adapters already keep the two
distinct, so a style with rapid_blink rendered differently depending
on which terminal adapter was in use. A test now pins both codes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
Four confirmed defects in the theme selector's input and rendering:

- handle_key matched on key code alone. Terminals that report key-up
  (Windows, the kitty protocol) fired every action twice, so one Down
  moved two rows and each typed character landed twice. Release events
  are now ignored.
- Ctrl, Alt, and Super chords arrived as plain characters and were
  appended to the filter, swallowing Ctrl+C and friends. Those chords
  now return Noop so the host app can handle them.
- Enter as the first key returned Select(id) for the highlighted theme
  without applying it, because the preview only ran on navigation. An
  app that persisted the id then showed one theme and saved another.
  Enter now applies the preview before reporting.
- The sticky section header always rendered the list's first section,
  so a window scrolled into the light themes read "Dark Themes" above
  a column of light entries. Only visible items now feed the header
  state, so the first visible row carries its own section heading.

Each fix has a regression test that fails against the previous code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
Half of the style samples used names no builtin theme defines
(file_path, commit_hash, diff_added, git_staged, author, timestamp and
friends), so those rows rendered unstyled and the demo taught names
that live in consuming apps, not in the core contract. The info sample
also hardcoded a stale theme count.

Sample the 14 contract styles instead, including the newly promoted
cursor_line, and read the theme count from BUILTIN_COUNT.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
Both tables quoted a fixed number that was already stale and drifts
with every added test. The command is the useful part; the count rots.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
When a variant boundary landed exactly on the final visible row, the
heading rendered with no entry beneath it, so the list ended on an
orphaned "Light Themes" line. A heading now requires room for at least
its first item. A test sweeps heights around the boundary and asserts
that a heading always has an entry under it; it fails on the previous
code at height 34.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
@hyperb1iss
hyperb1iss force-pushed the nova/issue-2-bg-tokens branch from 0beb234 to c930409 Compare September 2, 2026 06:22
RUSTSEC-2026-0192 marks ttf-parser as unmaintained. It reaches this
crate only through the egui feature (egui 0.33 pulls epaint, ab_glyph,
and owned_ttf_parser) and is a maintenance notice rather than a
vulnerability. The lock file is unchanged; the advisory database moved
underneath main, so cargo deny now fails on every branch. Ignore it
alongside the existing time advisory, with the exit path recorded: the
entry goes away when an egui release stops depending on ttf-parser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M
@hyperb1iss
hyperb1iss merged commit d5a6ee3 into main Sep 2, 2026
6 checks passed
@hyperb1iss
hyperb1iss deleted the nova/issue-2-bg-tokens branch September 2, 2026 08:36
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.

"bg.base" and "bg.panel" swapped for catppuccin themes?

1 participant