feat: add Light / Dark / System appearance selector - #1599
Conversation
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
|
Thanks @cjpais ! |
|
Follow-up: the appearance selector shipped here missed two spots, fixed in #1659.
|
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.
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.
Before Submitting This PR
Please confirm you have done the following:
If this is a feature or change that was previously closed/rejected:
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.
Systemstays 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 thedata-themeattribute; app follows OSprefers-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.AppSettings) and is applied synchronously before React mounts, so there's no flash of the wrong palette on launch.What changed (technical)
Themeenum (System/Light/Dark) insettings.rs, athemefield onAppSettings(defaults toSystem,serdedefault keeps existing settings files valid), and achange_theme_settingcommand registered incollect_commands!.:root[data-theme="light"]/:root[data-theme="dark"]overrides inApp.css. Their higher specificity beats the existingprefers-color-schememedia query, which is left untouched forSystem.theme.tsutil applies the mode todocument.documentElement.dataset.themeand mirrors it tolocalStoragefor flash-free boot; aThemeSelectordropdown 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).Screenshots/Videos (if applicable)
AI Assistance
If AI was used:
sound_theme/app_languagepatterns in the codebase; the description and reasoning above are my own.