Skip to content

Themes: Accept a dark/light theme table - #985

Open
masonmcelvain wants to merge 4 commits into
modem-dev:mainfrom
masonmcelvain:feat/adaptive-theme-config
Open

Themes: Accept a dark/light theme table#985
masonmcelvain wants to merge 4 commits into
modem-dev:mainfrom
masonmcelvain:feat/adaptive-theme-config

Conversation

@masonmcelvain

Copy link
Copy Markdown
Contributor

Support a [theme] table in config.toml to set dark/light themes:

[theme]
dark = "catppuccin-mocha"
light = "catppuccin-latte"
fallback = "github-dark-default"  # optional

fallback covers sessions where terminal background is unknown, defaulting to dark.

Writing the pair back exposed that the config writer was rewriting more of config.toml than it should, so that is fixed here too.

Changes

Theme selection

  • Add src/core/theme/selection.ts: a ThemeSelection is either a theme id or a { dark, light, fallback? } pair, and the module owns reading, comparing, and resolving one against a terminal background.
  • Probe the terminal background for a pair the same way auto does.
  • --theme <id> overrides a configured pair for one run.
  • Picking a theme in the app replaces the pair with that single id. The save-on-quit prompt renders the collapse before anything is written. - Keep the committed selection separate from the id it resolves to: persistence and in-session refresh carry the pair, while extension theme_changed events keep receiving a concrete id.
  • Reject a table that sets only one background, names an unknown key, or gives a non-string id, naming the offending file and key path. An unknown id is still tolerated, since an extension may contribute it later.

Config writing

  • Rewrite an existing theme in place instead of appending a second definition, including quoted ("theme" = …) and dotted (theme.dark = …) key forms, and drop duplicate definitions rather than leaving them behind.
  • Preserve trailing comments on a rewritten line and comments inside a [theme] table; keep a comment attached to the table it documents.
  • Collapse a [theme] table to a top-level theme = "…" when a single id replaces the pair, leaving one blank line between the neighbours it separated.
  • Write a pair as an inline table when the file has no [theme] section.

QA

Themes used below are interchangeable; any two built-in ids work.

  1. A pair follows the terminal. Put a [theme] table in ~/.config/hunk/config.toml with distinguishable dark/light ids, then run hunk diff in a dark terminal and again in a light one. Each should draw its own side.
  2. Fallback. Add fallback and run through a host that does not answer the background query (a script-allocated pty, or LazyGit as a pager). The fallback theme should render; remove fallback and dark should.
  3. Flag wins. hunk diff --theme github-dark-default with the table still in config should ignore the pair.
  4. Collapse on pick. With the table configured, press t, choose a theme, then q. The prompt should show - theme = { dark = "…", light = "…" } / + theme = "…", and saving should leave a top-level theme = "…" with no [theme] table.
  5. Nothing else in the file moves. Before step 4, add comments, a [custom_theme] table, and an [extensions] table around the [theme] table. After saving, they should be untouched, with no doubled blank lines, and the file should still parse.
  6. Refresh. With a pair configured, press r, then t. One side of the pair should still be active, and quitting should not offer to save a theme change.
  7. Bad tables. A table missing light, or with a typo'd key, should exit 1 naming the file and the problem.

masonmcelvain and others added 4 commits September 4, 2026 10:45
`theme = "auto"` only ever chose between github-light-default and
github-dark-default, so a reader who wanted one-light and one-dark-pro
had no way to follow their terminal. Accept a `[theme]` table naming both
sides instead, with an optional `fallback` for terminals that never
answer the background probe, following Helix's config shape.

The committed preference now stays whatever config asked for until
someone picks a theme in the selector, so quitting without touching
themes no longer rewrites `auto` — or a pair — into the one id it
happened to resolve to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Y3vNR6iJEH4epjQo8JARe
The theme writer only recognized the `theme = ` and `[theme]` spellings,
while the reader takes whatever Bun.TOML.parse produced. A config written
as `theme.dark = "nord"` read fine, then gained a second `theme = "..."`
assignment on the next preference save, after which Hunk refused to start
with `BuildMessage: Cannot redefine key 'theme'` — an error naming neither
the file nor the key. A quoted key inside `[theme]` failed the same way.
Match every spelling TOML accepts for a key, and drop the extra lines a
dotted key spreads a value over.

Saves also deleted comments. The `[theme]` range ran to the next section
header, so collapsing the table took the blank lines and comments that
introduce whatever follows, and every rewritten key lost its trailing
comment. Since one save rewrites all nine preferences, toggling
`wrap_lines` stripped comments out of an untouched `[theme]` table.
Comments now stay attached to the key or section they introduce, which is
also why a collapsed table is written over its own header rather than
appended — but only while `[theme]` is the first table, since a top-level
key at any later position would scope into the table above it.

Theme errors named `theme` whichever section or file held the bad table.
That matters more now that a checked-in `.hunk/config.toml` can hard-fail
startup for everyone in the repo, so they name both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLvy5LioGNhwJHzDPb7xxy
Saving view preferences could leave a config file slightly messier than it
found it. Collapsing a [theme] table that sat between two other tables left
the blank line from each side, and a file with no tables at all had new keys
inserted above its trailing comment, because the backtrack that keeps a
comment attached to the table it documents could not tell a real table index
from the clamp used when no table exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJVQRku6dLXkxiACCy177U
An in-session refresh rebuilt its reload input from the theme id the
selection currently resolved to, so an adaptive pair arrived at the reload as
whichever side the terminal happened to be on. Nothing collapses today
because every refresh path keeps the mounted App, but the descriptor is the
wrong thing to freeze. Carry the committed selection instead, and leave the
resolved id to extension events, which want a concrete theme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJVQRku6dLXkxiACCy177U
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR author is not in the allowed authors list.

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

@masonmcelvain is attempting to deploy a commit to the Modem Team on Vercel.

A member of the Team first needs to authorize it.

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.

Extend config file to set dark/light themes

1 participant