Skip to content

feat: add Light / Dark / System appearance selector - #1599

Merged
cjpais merged 8 commits into
cjpais:mainfrom
kud:feat/theme-selector
Jul 10, 2026
Merged

feat: add Light / Dark / System appearance selector#1599
cjpais merged 8 commits into
cjpais:mainfrom
kud:feat/theme-selector

Conversation

@kud

@kud kud commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Before Submitting This PR

Please confirm you have done the following:

If this is a feature or change that was previously closed/rejected:

  • I have explained in the description below why this should be reconsidered
  • I have gathered community feedback (link to discussion below)

Human Written Description

I noticed Handy already ships both a light and a dark palette, but it always follows the OS — there's no way to choose. To me this isn't really a "community" ask, it feels like an obvious, expected behaviour: loads of apps let you pick. My own case is that my macOS is set to light, but I keep a lot of apps in dark because I prefer them that way. I'd like Handy to be one of them.

To be clear about the earlier rejected PRs (#1232, #551): those asked to change Handy's brand colours. This does not. It doesn't add or alter a single colour — it only lets me pick which of the two palettes you already designed is used, exactly like the macOS "Appearance" setting itself. System stays the default and keeps today's behaviour.

Related Issues/Discussions

Discussion: #1598

Community Feedback

Opened a discussion to gauge support: #1598 — happy to gather more before this is considered.

Testing

Static checks (all green): tsc --noEmit, eslint src, prettier --check, cargo fmt.

Expected behaviour of the new Appearance dropdown (Settings → About):

  • System — removes the data-theme attribute; app follows OS prefers-color-scheme (unchanged default behaviour).
  • Light — forces the light palette even while the OS is in dark mode.
  • Dark — forces the dark palette even while the OS is in light mode.
  • The choice persists (stored in AppSettings) and is applied synchronously before React mounts, so there's no flash of the wrong palette on launch.

Note: this still needs a manual click-through in a normal bun run tauri dev session; CI also compiles the backend. Marking the PR as draft until that's done — flagging honestly rather than claiming a run I couldn't complete on my Intel Mac (unrelated ort/ONNX Runtime prebuilt-binary issue for x86_64-apple-darwin).

What changed (technical)

  • Backend: new Theme enum (System/Light/Dark) in settings.rs, a theme field on AppSettings (defaults to System, serde default keeps existing settings files valid), and a change_theme_setting command registered in collect_commands!.
  • CSS: added :root[data-theme="light"] / :root[data-theme="dark"] overrides in App.css. Their higher specificity beats the existing prefers-color-scheme media query, which is left untouched for System.
  • Frontend: theme.ts util applies the mode to document.documentElement.dataset.theme and mirrors it to localStorage for flash-free boot; a ThemeSelector dropdown next to the existing language selector; wired through the settings store like every other setting. English i18n keys added (other locales fall back to English).
  • Scope: the recording overlay is intentionally untouched — it has its own deliberate styling.

Screenshots/Videos (if applicable)

AI Assistance

  • No AI was used in this PR
  • AI was used (please describe below)

If AI was used:

  • Tools used: Claude Code
  • How extensively: AI wrote the implementation code following the existing sound_theme / app_language patterns in the codebase; the description and reasoning above are my own.

Introduce a Theme enum (System/Light/Dark) in settings.rs with a default of System, wiring it through the full stack:

- Backend: Theme enum, default_theme(), AppSettings.theme field, change_theme_setting Tauri command
- Frontend: theme.ts utility (applyTheme, getStoredTheme, syncThemeFromSettings), ThemeSelector component, CSS overrides via data-theme attribute
- Bootstrapped synchronously in main.tsx to avoid palette flash on load
- i18n keys added for title, description, and all three options
@kud
kud force-pushed the feat/theme-selector branch from a79e0f5 to f22df24 Compare July 5, 2026 00:11
@kud
kud marked this pull request as ready for review July 8, 2026 21:19
@cjpais
cjpais merged commit d929a94 into cjpais:main Jul 10, 2026
@kud

kud commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @cjpais !

@kud
kud deleted the feat/theme-selector branch July 11, 2026 12:34
@kud

kud commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: the appearance selector shipped here missed two spots, fixed in #1659.

  1. Recording overlay didn't follow the setting. The overlay is a separate webview. The :root[data-theme=…] override CSS lived in App.css (only the settings window loads it), and overlay/main.tsx never set the data-theme attribute — so the overlay stayed on prefers-color-scheme and kept following the OS regardless of the choice. Fix moves the override into the shared theme.css and applies/syncs the theme in the overlay entry, plus a theme-changed event for live updates.

  2. macOS native title bar stayed light. window.set_theme() (the only thing that themes the native chrome) was gated to #[cfg(target_os = "windows")], deliberately — to avoid dragging the overlay along, since it's app-wide on macOS. But that's exactly the behaviour we now want, so the guard is extended to macOS: title bar + overlay move together.

@cjpais

cjpais commented Jul 11, 2026

Copy link
Copy Markdown
Owner

@kud did you notice #2 on macOS or is this just from a code review? on my actual machine it switches just fine

TaylorFinklea added a commit to TaylorFinklea/Handy that referenced this pull request Jul 16, 2026
Upstream shipped its own Light/Dark/System appearance selector (cjpais#1599) that
collided head-on with the theming engine here: both claim the `theme` setting
key, upstream as a `Theme` enum and this fork as a theme-registry id.

Resolved in favor of the registry, which is a strict superset — its
`handy-light`/`handy-dark` themes already carry upstream's exact palettes, and
`system` still follows the OS. Dropped upstream's `Theme` enum, its
`src/lib/utils/theme.ts`, and its `ThemeSelector`.

Kept the one piece of upstream's work CSS cannot replicate: Windows title-bar
theming. The backend cannot map a theme id to light/dark without duplicating
the registry, so the frontend now reports the resolved appearance
(`resolveAppearance`) and it is persisted as `theme_appearance`. Adding a theme
still needs no backend change.

Preserved all of upstream's non-theme work, notably the cjpais#1619 settings salvage
(one invalid field no longer resets every setting), `paste_delay_after_ms`, and
the prompt-injection-hardened post-process prompt. Their frozen v0.9.0 store
fixture now asserts the sound-split migration runs exactly once and then
converges, rather than asserting no migration at all; their salvage tests probe
`start_sound` since `sound_theme` no longer exists.

Also fixed four duplicate definitions git produced without flagging a conflict
(two `change_theme_setting` fns, a duplicate `theme` struct field, a duplicate
`theme` key in settingsStore, and a duplicate `changeThemeSetting` binding) —
each of which broke the build.

Verified: 129 cargo tests, 4 frontend unit tests, tsc, eslint, release build.
TaylorFinklea added a commit to TaylorFinklea/Handy that referenced this pull request Jul 16, 2026
Upstream's Light/Dark/System selector (cjpais#1599) was dropped in the v0.9.2 merge in
favor of this fork's theme registry, which translates under `settings.theme.*`.
Its `theme.*` keys survived the merge in all 22 locales with no code referencing
them.
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.

2 participants