feat(theme): add palette module with 4 built-in themes#3
Merged
Conversation
Adds the theme infrastructure for v0.2.0: - ThemePalette struct with 8 named color slots covering every hardcoded Color::* site in the UI. - Built-in palettes: dark (default, current look), light, monokai, dracula. Adding a new theme is a one-line append to builtin::ALL. - Color parser supporting #RRGGBB hex and the 16 ANSI names (case-insensitive, _/- separators tolerated). - by_name() lookup and names() listing for CLI integration. No UI code consumes the palette yet — that lands in the next commit. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Adds the configuration plumbing for v0.2.0: - Config struct (theme, defaults, colors sections) with serde deserialization. Unknown fields and unknown color slots both error at parse time so typos surface immediately. - load_or_default() in load.rs: reads ~/.typerush/config.toml, never errors — missing file gives defaults, malformed file gives defaults plus a warning surfaced via the error modal. - ResolvedConfig: the flat, fully-resolved shape the rest of the app consumes. Bakes in CLI > config > default precedence for theme. - DefaultMode + CodeLangKind: config-side mirrors of the runtime Mode enum, kept here to avoid the config module depending on app/words. - 17 tests covering valid configs, invalid configs, color overrides, unknown themes, unknown modes, CLI override precedence. Nothing consumes load_or_default() yet — wiring lands in the next commit alongside the UI rewire. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Every hardcoded Color::* in the UI now reads from app.theme. Switching themes only requires changing one ThemePalette value at startup. - App grows a 'theme: ThemePalette' field and a constructor that takes the resolved palette + default mode. The constructor also pre-selects the matching menu row (exact match preferred, falls back to first row of the same family). - main.rs loads ~/.typerush/config.toml via config::load::load_or_default, surfacing the first warning (if any) through the existing error modal. - New CLI flags: --theme <name> for one-shot overrides, --list-themes for shell-completion-friendly enumeration. - typing/menu/results/stats/help screens fully rethemed. Zen mode now desaturates to theme.pending + theme.neutral instead of hardcoded grays, so light backgrounds remain readable. - Added a 'neutral' palette slot for emphasized neutral text (chars totals, session count) — maps to white on dark themes, near-black on light themes. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Finalizes v0.2.0: - Cargo.toml: version 0.1.0 → 0.2.0 - CHANGELOG.md: full Added / Changed / Fixed / Compatibility section for 0.2.0; clears [Unreleased]. Notes that the stats.json format and every v0.1 CLI flag are unchanged. - docs/ROADMAP.md: moves all four v0.2 line items into the shipped section. - docs/USAGE.md: new 'Configuration' section documenting theme picking, per-slot overrides, defaults, and CLI > config > built-in precedence. - README.md: surfaces themes in the features list, adds a short 'Make it yours' section linking to USAGE.md. - config.example.toml: documented starter config at repo root. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Addresses the three actionable items from the Phase 3 review: - config: include_str!-based round-trip test for config.example.toml. Catches schema drift the moment someone renames a slot or adds a field under deny_unknown_fields without updating the example. - config: multi-warning accumulation test for apply_color_overrides. - app: 5 tests for best_menu_match — exact time/words hits, family fallback (time_seconds = 45 lands on the first time row, not on something unrelated), and the zero-tier modes (Zen). - menu: comment explaining why highlight foreground stays Color::Black rather than going through the palette. All four built-in themes pair accent with a bright color, so black-on-accent is the high-contrast choice; users who override accent to something dark can also override. Tests: 29 → 36 passed. Clippy and fmt unchanged. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Mode::Custom was missing from the completion check in submit_word(), so after typing every word the session waited for Esc — contradicting the 'ends when you complete the last word' line in docs/USAGE.md. One-line addition to the matches!() guard, plus a regression test that drives a 2-word custom session to completion and asserts the app lands on Screen::Results. 37 tests passing. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Adds three concrete items to v0.4 — More content & menu UX: - snippet library at ~/.typerush/snippets/ (was in 💡 ideas) - 'Custom' menu row + last-picked-file memory - visual section gaps in the menu Renames the v0.4 section heading to '— More content & menu UX' to reflect the broadened scope. Removes the snippet-library line from the 💡 ideas section since it's now committed. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Three new bullets under 'v0.5 — Quality of life': - typerush config get/set/show/reset/path — scriptable CLI for editing ~/.typerush/config.toml without opening it. - In-app Settings screen — theme picker, default mode picker, reset-to-defaults. Navigable from the main menu. - typerush --init-config — writes a starter config.toml so new users don't have to copy from config.example.toml manually. Existing v0.5 items (pause/resume, replay, CSV export, daily challenge) unchanged. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Splits the practical 'how do I run / test / iterate locally' content out of CONTRIBUTING.md (which keeps coding conventions, commit style, PR flow) into a dedicated dev guide. DEVELOPMENT.md covers what wasn't previously written down in any one place: - Debug vs release profile, when to use which - Passing CLI flags through cargo run -- (the -- separator) - Running ./target/debug or ./target/release directly - HOME=/tmp/sandbox redirect for safely testing config.toml without touching the real ~/.typerush/ - Testing malformed configs to verify graceful fallback - Test filtering, --nocapture, --test-threads - Quality gates that mirror CI - cargo-watch with the TUI-specific caveat (re-launch fights the terminal) and the recommended two-terminal loop - A few task-shaped loops (tweaking a palette, debugging matcher behavior, testing config + theme together) CONTRIBUTING.md's old 'Running during development' subsection now points at DEVELOPMENT.md with a 3-line quick-reference instead of duplicating the content. README's doc-link list includes the new file. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Earlier in v0.2.0 the light theme only swapped foreground colors — on
a dark terminal it still looked dark, just with different text colors.
Same problem for monokai and dracula: they were branded by name but
adopted the user's terminal background, so they only looked correct
on terminals that happened to match.
Fix:
- ThemePalette gains a 'background: Color' slot.
- Built-in palettes set authentic backgrounds:
dark → Color::Reset (terminal default — preserves v0.1 look)
light → #FAFAFA (off-white)
monokai → #272822 (canonical)
dracula → #282A36 (canonical)
- ui/mod.rs::render paints the whole frame with the theme background
before any screen renders. Subsequent widgets render on top — text
cells show fg, empty cells show the painted bg.
- light theme's 'pending' color shifted from a near-white shade to a
medium gray so it stays readable on white.
- Config's [colors] section accepts a 'background' field. The example
config and docs/USAGE.md document the new slot, with a callout
explaining the Color::Reset semantics on the dark theme.
Tests: 37 → 39 (added dark_background_is_reset and
non_dark_themes_paint_explicit_backgrounds). Clippy and fmt clean.
https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Three follow-up fixes after the v0.2.0 background work shipped: 1. UI dispatcher now paints the background via buffer.set_style on every cell, instead of rendering a no-border Block widget. The Block path was leaving thin unpainted stripes around the edge of the screen on some terminals; the direct buffer write is unconditional. (Note: terminal-side window padding — e.g. Windows Terminal's default 8px — is still outside ratatui's reach. Module docs now call that out so users know to adjust their terminal profile if a thin stripe remains.) 2. Menu screen: added a 2-row top spacer before the TYPERUSH banner, so it no longer hugs the terminal title bar / tab strip. 3. Light theme: darkened every foreground slot so the palette is readable on the off-white background. The previous values were carried over almost verbatim from the dark theme and washed out on white — especially the medium gray 'pending' which was nearly invisible. New values give roughly 4.5:1+ contrast against the #FAFAFA background across all slots. Tests still pass (39), clippy clean, fmt clean. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Final v0.2.0 review flagged that no test asserted the buffer-level bg paint actually lands on the buffer — only the palette constants were checked. Adds four ui::tests using ratatui's TestBackend that render the menu screen into a fake terminal and sample a far-corner cell (untouched by any widget): - dark_theme_leaves_background_as_reset v0.1 compatibility — dark must NOT force an explicit bg; the user's terminal background must shine through. - monokai_theme_paints_canonical_background (#272822) - dracula_theme_paints_canonical_background (#282A36) - light_theme_paints_off_white_background (#FAFAFA) If a future refactor breaks the dispatcher's set_style call or replaces it with something that skips cells, these tests fail before users do. Tests: 39 → 43. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Two visual bugs surfaced after the light theme started painting a real
background.
1. Gauge label inherited a default style that produced odd contrast on
the colored fill (peach-on-cyan on monokai, washed out on dracula).
Now explicitly styled as 'theme.secondary + BOLD' — yellow/amber on
every built-in theme, readable on both the filled (bg=accent) and
unfilled (bg=theme bg) portions without needing a dedicated
'on-accent' palette slot.
2. Plain text rendered without an explicit fg falls through to the
terminal's default foreground. On a dark terminal that's near-white,
which becomes invisible the moment we force the bg to white via the
light theme. Affected:
- Menu list items (unselected rows)
- Stats table 'Date' column
- Plain Line::from("…") entries in the help overlay
Fixed by setting widget-level .style(fg=theme.neutral). Spans that
are already explicitly styled keep their own colors because Span
style overrides parent Paragraph/List/Table style.
Tests: 43 passing, clippy + fmt clean.
https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
…state
Three docs were stale relative to the code state on this branch.
CHANGELOG.md — v0.2.0 entry rewritten to cover the work that landed
after the initial draft:
- Banner top padding + light theme palette retune (Changed)
- Gauge label readability (Fixed)
- Plain unstyled text now uses theme.neutral on light theme (Fixed)
- docs/DEVELOPMENT.md as a shipped artifact (Added)
- Test count updated from '25 new' → '4 to 43' for accuracy
docs/ARCHITECTURE.md — refreshed in three places:
- Module map now lists src/theme/ and src/config/, which didn't
exist when this file was last edited.
- 'Steady cursor' design note updated — both cursor positions now
use a single underline style (was REVERSED + underline before
the v0.2.0 cursor consolidation).
- New 'Theme-aware background paint' design note explaining how
ui::render fills every cell with theme.background, why widgets
with plain Line/Cell entries need a widget-level .style(), and
why the dark theme uses Color::Reset.
- New 'When you add a new theme' recipe at the end — drop a const
into src/theme/builtin.rs and append to ALL, no other wiring.
CONTRIBUTING.md — 'Adding a new theme' section pointed at a
non-existent src/config.rs with a 'once theming lands in v0.2' note.
Replaced with a one-line pointer to the ARCHITECTURE recipe.
No code changes, no test count change.
https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
Real bug since v0.1, surfaced by the user. Most terminals (Windows
Terminal, iTerm2, most Linux emulators) send Ctrl+Backspace as a
literal ^H byte. crossterm reports that as KeyCode::Char('h') with
CTRL — NOT as KeyCode::Backspace with CTRL — so the dedicated
Backspace+CTRL branch in handle_typing_key never fired. The keystroke
fell into the 'ignore control chords' branch and silently went
nowhere.
Fix:
- Add an explicit Char('h') | Char('w') + CTRL match arm in
handle_typing_key that calls app.handle_backspace(true). Ctrl+W is
the Unix 'kill word' convention and was likely also broken with the
same root cause.
- The original Backspace+CTRL branch is preserved for terminals that
DO surface Ctrl+Backspace as a Backspace keycode.
Tests (main.rs gains its first test module):
- ctrl_h_deletes_current_word — the original bug
- ctrl_w_deletes_current_word — Unix convention
- ctrl_backspace_keycode_still_deletes_word — old path still works
- plain_backspace_deletes_one_char — no regression on plain bs
- other_ctrl_chords_are_ignored — Ctrl+A/Z still no-op
Docs:
- USAGE.md keybinding table now lists Ctrl+W and Ctrl+H alongside
Ctrl+Backspace.
- CHANGELOG.md gains a Fixed bullet for the bug.
Tests: 43 → 48, clippy + fmt clean.
https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
origin/main shipped v0.1.1 (cursor-flicker fix, README rewrite, demo GIF URL fix, release.yml tweaks, assets/ folder) after this branch was cut. Merging it back in so v0.2.0 sits cleanly on top of v0.1.1 instead of forking the version history. Conflict resolutions: - Cargo.toml: keep version 0.2.0, accept the new exclude block (no conflict on the exclude itself, only on the version line). - CHANGELOG.md: kept v0.2.0 entry in full, dropped its duplicate cursor-fix bullet (since that's now properly attributed to v0.1.1 upstream), updated link refs to chain Unreleased → v0.2.0 → v0.1.1 → tag. Bumped the test-count line from 43 to 48 to reflect the post-merge Ctrl+H tests. - Cargo.lock: took main's version and rebuilt — adds toml_edit / toml_write / winnow for our toml dep. Brought in unchanged from main: README preview banner + crates.io badge, .github/ISSUE_TEMPLATE bug_report tweak, release.yml updates, and the assets/ directory (demo gif, mp4, vhs tape, screenshots). Tests: 48 passing, clippy + fmt clean. https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu
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.
Adds the theme infrastructure for v0.2.0:
hardcoded Color::* site in the UI.
dracula. Adding a new theme is a one-line append to builtin::ALL.
(case-insensitive, _/- separators tolerated).
No UI code consumes the palette yet — that lands in the next commit.
https://claude.ai/code/session_013rjUTXRnD7tFJS9qCyWeXu