Catppuccin base fidelity, full bg contract, and review-driven fixes - #3
Merged
Conversation
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
force-pushed
the
nova/issue-2-bg-tokens
branch
from
September 2, 2026 06:22
0beb234 to
c930409
Compare
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
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.
🎨 Catppuccin base fidelity, the full bg contract, and review-driven fixes
💡 What this is
Issue #2 asked two things: why the dark Catppuccin flavors map
bg.basetocrustwhen Catppuccin itself callsbasethe background pane, and whybg.elevatedhas 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 thebg.*family means, because the token guide calledbg.basethe "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.baseto the deepest neutral in its palette and steps the otherbg.*tokens toward the text color. Catppuccin's design goes the other way for secondary panes:baseis the editor background, andmantleandcrustare the recessed sidebars and status areas. Mocha, Macchiato, and Frappé painted the app canvas withcrust, so an Opaline app on a Catppuccin terminal showed#11111bwhere 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, andcursor_lineexist in all 39 themes, but nothing inopaline::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.baseto 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.baseis the canvas an app paints first.bg.panelis one step away from it in whichever direction the source palette prefers, andbg.highlight,bg.elevated, andbg.activestep toward the text color. The contract test now builds its required lists fromopaline::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)bg.basebg.panelbg.codebg.highlight#1e1e2e#181825#11111b#313244#24273a#1e2030#181926#363a4f#303446#292c3c#232634#414559#eff1f5#e6e9ef#dce0e8#ccd0daOnly the three
bg.base/bg.panel/bg.codelines changed per dark flavor; the surface ladder (highlight,elevated,active,selection) is untouched. The fidelity testcatppuccin_flavors_map_base_to_main_backgroundintests/builtins_tests.rspins all four flavors by hex.2. The contract (
src/names.rs,tests/builtins_tests.rs)tokens::BG_ELEVATED,tokens::BG_ACTIVE, andstyles::CURSOR_LINEare new.REQUIRED_TOKENS,REQUIRED_STYLES, andREQUIRED_GRADIENTSare now arrays ofnames::constants instead of string literals, andcontract_sizes_match_documentationasserts 28 / 14 / 5. Every count mention in README, AGENTS.md, CONTRIBUTING.md, and the docs site moved from 26/13 to 28/14, anddocs/guide/tokens.mdgained the ladder description above plus rows for the two tokens.3. Loader panic (
src/color.rs)from_hexchecked 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 asInvalidHexbefore 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:
KeyEventKind::Releasenow returnsNoop.Noop.Select(id)without applying that theme, because the preview only ran on navigation. Enter now callsapply_previewfirst.5. Everything else
src/adapters/owo_colors.rs:rapid_blinkmaps toblink_fast()(SGR 6) instead of collapsing intoblink()(SGR 5), matching crossterm and ratatui.src/adapters/egui.rs: twelveStroke::new(1.0, …)literals gained_f32. They tripped the future-incompatfloat_literal_f32_fallbacklint on current stable, which madeclippy -- -D warningsexit 101.tests/*.rs: gradient, ratatui, and builtin-dependent tests are gated on their features, socargo testcompiles 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 fromBUILTIN_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 throughegui0.33 and is a maintenance notice, so it is acknowledged next to the existingtimeentry with the exit path recorded.ci.yml(the workflow iscicd.yml); the docs.rs feature table listed 6 of 13 features; the iced adapter had no guide page; the ratatui guide importedStylizewhere the method lives onStyled; twoCircularReferencepatterns omittedtoken; the CSS guide showed Catppuccin hex values forTheme::default(); the egui table claimedtext.primarydrivesoverride_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 onmain).cargo test --all-features→ 220 passed, 0 failed (210 onmain; 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 onmain.cargo doc --all-features --no-deps→ zero warnings.cargo deny --all-features check→ advisories ok, bans ok, licenses ok, sources ok (advisories failed onmainbefore the ignore entry).src/widgets/theme_selector.rsreverted tomain, the five new selector tests fail and the six existing ones pass.from_hexinputs, 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 ofmain.from_hexpanic was reproduced onmain(load_from_strwith palette value"#aé000"→byte index 3 is not a char boundary) before the fix.#1e1e2erather than#11111b, so the image is one shade off until a rerender.🔍 What reviewers should focus on
bg.panelnow sits darker thanbg.base(mantle under base). Kanagawa already ships that way and Latte already did, but it is a visible change for anyone painting panels withbg.panelon a Catppuccin theme.render_theme_entriesinsrc/widgets/theme_selector.rs. The skip-before-header reorder is small, but scroll math depends oncount_section_headersand the rendered header count staying equal.from_hexnow returnsInvalidHexfor non-ASCII input even when the character count is wrong; previously such input either panicked or returnedInvalidLength.bg.panelis lighter thanbg.base. I found none in this repo, git-iris, or hypercolor, but say so if you have one.📌 Follow-ups (deliberate non-fixes)
bg.baseto 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.ThemeSelectorStatehas norestore_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.rose_pine.tomlin a user dir does not shadowrose-pine. Normalising the stem is a one-liner but changes ids users may already rely on.Theme::gradient_text(gradient, text)andcli_gradient(text, gradient)take the same two&strin opposite order; fixing it is a breaking change.OpalineError::MissingSectionis never constructed. Removing it and marking the enum#[non_exhaustive]belongs in the next minor.eguionce a release dropsttf-parser, then remove the RUSTSEC-2026-0192 ignore.tests/adapter_tests.rsis gated onratatuiandgradientstogether, so a ratatui-only build exercises none of theFromimpl tests. Splitting the gate per test is mechanical.🤖 Generated with Claude Code
https://claude.ai/code/session_015pQZqxAgTfPCnwGRFcVF2M