Skip to content

feat: custom themes and the theme registry (#82, #138) - #148

Merged
marcoarnulfo merged 14 commits into
mainfrom
feat/custom-themes
Aug 1, 2026
Merged

marcoarnulfo merged 14 commits into
mainfrom
feat/custom-themes

Conversation

@marcoarnulfo

Copy link
Copy Markdown
Owner

Closes #138.

Advances #82 — boxes 1 and 3 (load a theme from YAML, ship two built-in themes). #82 stays open on its third box, configurable keybindings, which is a separate tranche: it touches 51 bindings and shares nothing with color but the config file.

Design: docs/superpowers/specs/2026-08-01-custom-themes-design.md · Plan: docs/superpowers/plans/2026-08-01-custom-themes.md

What you can do now

theme: dracula

themes:
  mine:
    accent: {light: "127", dark: "205"}
    muted: "240"

Three built-ins — default, dracula, nord. A theme of your own names only the tokens it changes; the rest come from default, so the common case is two lines. A value is one color for both backgrounds, or a {light:, dark:} pair.

Everything invalid stops startup

Not a fallback — an error, before the TUI opens, naming the theme, the token and the value. The reason is measured: lipgloss never complains. An invalid color renders as no color, with no error and no panic, and 999 is not rejected but emitted as \x1b[38;5;999m, which no terminal reads. Nothing downstream would ever tell the user, so this is the only place a typo can be caught.

Every theme you define is checked, not only the selected one — a typo in a theme you are not using today is still a typo, and the day you switch is the worst moment to find it.

The dark themes are shipped faithfully, and the READMEs say what that costs

dracula and nord are dark-terminal palettes and are not adapted to a light background. Measured: dracula's green is 1.37:1 against white, and nord's zebra stripe is 2.09:1 against the dark text painted on top of it — a dark stripe under dark text, well under the 3:1 floor. On a light terminal, parts of the UI genuinely cannot be read. default remains the adaptive one. Both READMEs carry the numbers rather than a vague warning.

The zebra stripe survives a custom theme

v1.9 gave subtle an explicit 16-color value because the automatic downconvert collapses it onto the background and the stripe vanishes. A user-written theme would have undone that silently. It is now derived: between ANSI 8 and 7 — the only two shades at 16 colors — whichever is perceptually closer, with no threshold constant. That reproduces the two hand-picked values exactly (236→8, 254→7), so the default theme renders byte-identically at every profile, and a test pins it.

#138: two frames, one weight

The report drew its table frame with th.Border and its empty-state box with th.Box, which had no border color — so a report with no hours showed a differently-weighted frame from one with hours. th.Box now takes the same color. Exactly two goldens moved, both rendered at truecolor; the fifteen others run under termenv.Ascii, where the change is byte-identical.

Architecture

A new leaf package, internal/themes, owns the palette, the built-ins, the YAML shape and the validation. internal/config keeps the raw YAML, internal/cli resolves before the program starts — the last point where a config error still reaches stderr instead of a running TUI — and internal/tui receives a palette it did not build. New gained an argument at 44 call sites.

Verification

Every number in the docs and comments was measured by running code, and re-measured independently by a second and often third party. That discipline earned its keep: the final review caught that a contrast figure in both READMEs was attributed to the wrong reference color, that the dark half of every color pair was unvalidated with the whole suite still green, and that a package doc comment asserted an import relationship that had since become false. Nine mutations were run across the branch; eight were caught by existing tests, and the one that was not is now covered.

Gate clean at HEAD: gofmt -l . · go vet ./... · staticcheck ./... · go build ./... · go test ./... -race. Exactly two goldens regenerated, both looked at.

🤖 Generated with Claude Code

Seven findings from the final whole-branch review:

- README.md/README.it.md: attribute each contrast figure to what it was
  actually measured against — dracula's green vs white, nord's zebra
  stripe vs the terminal's own dark text on top of it, not both vs white.
- internal/themes/resolve.go: TestResolveErrors gained a case with a
  valid Light and an invalid Dark, so the Dark half of a color pair is
  no longer unvalidated in the suite's eyes.
- internal/themes/themes.go: reworded the package doc's leaf-package
  clause as the counterfactual it always was, now that internal/config
  does import internal/themes.
- internal/tui/app_test.go: translated the repo's one Italian test
  failure message to English.
- CHANGELOG.md: split the Added entry so an unknown theme and an
  invalid color are described by the message each actually produces.
- README.it.md: fixed a formal-plural slip on the hex-quoting bullet.
- internal/tui/theme_test.go: TestBoxAndBorderShareTheirColor now
  asserts all four border sides instead of only the top one.
@marcoarnulfo
marcoarnulfo merged commit 8654d3f into main Aug 1, 2026
4 checks passed
@marcoarnulfo
marcoarnulfo deleted the feat/custom-themes branch August 1, 2026 11:58
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.

refactor(tui): the report table's numeric columns are reserved, not clamped; and two frame weights on one screen

1 participant