From f7865780d9b8c6819b34eb0ff265e9c0642adf6e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 21 Oct 2026 10:56:14 +0700 Subject: [PATCH] add plan --- docs/NATIVE-THEME-MIGRATION.md | 269 +++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 docs/NATIVE-THEME-MIGRATION.md diff --git a/docs/NATIVE-THEME-MIGRATION.md b/docs/NATIVE-THEME-MIGRATION.md new file mode 100644 index 0000000..4136611 --- /dev/null +++ b/docs/NATIVE-THEME-MIGRATION.md @@ -0,0 +1,269 @@ +# Native theme migration plan + +Integrate [`native-theme`](https://github.com/tiborgats/native-theme) (via the +`native-theme-gpui` connector) so the app follows the OS native theme on +macOS, Windows and Linux (KDE/GNOME), instead of only the bundled +`signed.json` themes. + +Principles for this migration: plug the connector in, migrate what it +replaces, do not wrap it, and do not adopt its optional subsystems (icons, +theme-drawn widgets, presets UI) in this pass. + +--- + +## 1. Current state + +| Piece | Location | Notes | +| --- | --- | --- | +| Startup theme setup | `desktop/src/main.rs` | `gpui_component::init` + `theme::init`, loads `signed.json` into `ThemeRegistry`, sets `Theme::global_mut` fields (`radius`, `radius_lg`, `focus_ring`, `shadow`, `font_size`, `mono_font_size`, `light_theme`, `dark_theme`), then `Theme::change` / `Theme::sync_system_appearance` | +| Runtime theme application | `crates/workspace/src/views/sidebar/settings_dialog.rs` | `apply_theme()` re-reads settings into the global `Theme`; `apply_appearance()` maps `AppearanceMode` to `Theme::change` / `sync_system_appearance` | +| System appearance follow | `crates/workspace/src/workspace.rs` | `cx.observe_window_appearance` → `Theme::sync_system_appearance` when appearance is `System` | +| Theme settings | `crates/settings/src/settings.rs` (`ThemeSettings`) | `light_theme`, `dark_theme`, `font_size`, `mono_font_size`, `radius`, `radius_lg`, `focus_ring`, `shadow` | +| Bundled themes | `crates/assets/assets/themes/signed.json` | `Signed Light` / `Signed Dark` colors + a `highlight` section (editor/syntax colors) | +| Theme consumers | `dock`, `signed_ui`, `workspace` | ~50 `cx.theme()` call sites via `gpui_component::ActiveTheme` | + +Dependencies today (branch `feat/native-theme`, commit `eb4fbb0`): +`gpui-pre` 0.3.9, `gpui-pre-platform` 0.3.9, `gpui-component` 0.8, +`gpui-base` 0.8, toolchain 1.96. + +## 2. What the connector does + +`native-theme-gpui` turns a `native_theme::ResolvedTheme` (read from the OS, +or from a bundled preset) into a fully configured `gpui_component::Theme` and +installs it: + +- all 139 `ThemeColor` fields, fonts, `radius`, `shadow`, `focus_ring`, + scrollbar and base-layer values; +- a `ThemeConfig` per mode, so the app's existing + `Theme::change(mode, None, cx)` / `Theme::sync_system_appearance(..)` + switch between the native light and dark palettes unchanged; +- an internal observer that restores the native values gpui-component + overwrites on each rebuild; +- accessibility: the OS text-scaling factor scales fonts (through the window + rem), reduce-motion and reduce-transparency are forwarded. + +Because the connector produces a regular gpui-component `Theme`, every +existing `cx.theme()` call site keeps working without modification. That is +the core reason this integration is small. + +Key APIs: `SystemTheme::from_system()`, `apply_system_theme(&system, cx)`, +`apply(theme, &resolved, &prefs, cx)`, `from_preset(name, is_dark, &prefs)`, +`AccessibilityPreferences::from_system()`. + +## 3. Gate 0 — version compatibility (blocking) + +Verified against crates.io and the upstream repo on 2026-10-21: + +| Fact | Value | +| --- | --- | +| Latest connector | `native-theme-gpui` 0.6.1 (2026-10-09); upstream `main` is the same | +| Connector requires | `gpui-component ^0.7.1`, `gpui-base ^0.7.1`, `gpui-pre ^0.3.8`, Rust ≥ 1.95 | +| `gpui-component` 0.7.x pins | `gpui-pre =0.3.8` | +| `gpui-component` 0.8.0 pins | `gpui-pre =0.3.9` (0.8.0 published 2026-10-19) | +| This branch | moved 0.7.1 / 0.3.8 → 0.8 / 0.3.9 in `eb4fbb0` ("chore: update deps") | + +The pins are mutually exclusive: with the app on `gpui-component` 0.8, cargo +cannot unify the graph — the connector would pull a second `gpui-pre` 0.3.8 +and a second `gpui-component` 0.7.x, and `apply(.., cx)` would not even +type-check against the app's `App`. **The connector cannot be added on top of +gpui-component 0.8 today.** + +### Decision + +Revert the GPUI-side dependency bump on this branch and align with the +connector's supported closure (recommended): + +```toml +# [workspace.dependencies] +gpui = { package = "gpui-pre", version = "0.3.8" } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] } +reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" } + +gpui-component = { version = "0.7.1", features = ["tree-sitter-languages"] } +gpui-base = "0.7.1" + +native-theme = "0.6.1" +native-theme-gpui = "0.6.1" +``` + +- This is exactly the state of `master` before `eb4fbb0`, which is also the + version set the connector was verified against (2026-10-08 stamp) and that + its nightly dependency canary exercises. `"0.7.1"` is a caret requirement; + it resolves the newest 0.7.x (currently 0.7.3). Pin `=0.7.1` instead if + strict reproducibility is preferred. +- The three `gpui-pre-*` family crates are published in lockstep; all three + move back to 0.3.8 together. +- The cost of the revert is the two one-line tweaks `eb4fbb0` made for 0.8 + (the `Deref` removal in `crates/dock/src/dock_area.rs`; verify it compiles + either way). The nostr changes in that commit are unrelated and stay. +- When `native-theme-gpui` releases gpui-component 0.8 support (upstream has + bumped within days of each gpui-component release so far), bump both sides + together in one commit. + +Alternative, rejected for now: staying on gpui-component 0.8 and waiting for +upstream 0.8 support. Nothing in this migration depends on 0.8-only APIs, and +0.8.0 shipped two days before this plan, so the revert is the cheaper path. + +Note: the connector's README quick-start shows the `gpui-kit` facade crate; +this project depends on `gpui-component` + `gpui-base` directly, which is +equivalent — no `gpui-kit` dependency is needed. + +## 4. Target design + +### 4.1 Theme source + +The native OS theme becomes the primary source. The bundled `signed.json` +registry stays as the fallback when the OS theme cannot be read (unsupported +Linux desktop, headless, etc.). This keeps the existing startup code as the +fallback branch instead of deleting a working safety net. + +### 4.2 Startup flow (`desktop/src/main.rs`) + +Order is load-bearing: + +1. `gpui_component::init(cx)` and `theme::init(cx)` — must run before + `apply` (connector requirement). +2. `SettingsStore` load — already before any theme work. +3. `ThemeRegistry` load from `Assets.themes()` — kept, feeds the fallback. +4. Apply the native theme: + +```rust +let prefs = native_theme_gpui::AccessibilityPreferences::from_system(); +match native_theme::SystemTheme::from_system() { + Ok(system) => native_theme_gpui::apply_system_theme(&system, cx), + Err(error) => { + tracing::warn!("native theme unavailable ({error}); using bundled themes"); + // existing path: registry lookups + Theme::global_mut field setup + } +} +// shared: the existing `match settings.appearance { .. }` block stays, +// now driving native palettes per mode on the native path. +``` + +`apply_system_theme` installs both the light and dark variants plus their +configs, so the subsequent `Theme::change` / `sync_system_appearance` for the +user's `AppearanceMode` picks the right native palette. + +On the native path, do **not** set `radius` / `font_size` / `light_theme` / +`dark_theme` etc. on `Theme::global_mut` — the connector owns those fields, +and overwriting them fights its observer. + +### 4.3 Appearance switching (no changes) + +- `AppearanceMode::System | Light | Dark` and `apply_appearance()` in + `settings_dialog.rs` work as-is. +- `workspace.rs`'s `observe_window_appearance` → `sync_system_appearance` + keeps following the OS while in `System` mode. +- `Theme::change(mode, None, cx)` reproduces the native palette for the mode + because the connector installed a `ThemeConfig` per variant. + +Known gap (not addressed here): gpui-component's system-appearance follow is +driven by window-activation events. A live OS theme change while the window +is focused may not propagate on Linux until the window is re-focused; +native-theme's `watch` feature (a theme-change watcher) is a possible +follow-up, out of scope. + +### 4.4 Settings dialog (`settings_dialog.rs`) + +Keep: + +- the **Appearance** row (`System` / `Light` / `Dark`) and its handler. + +Remove (the native theme now owns all of these): + +- **Light Theme** / **Dark Theme** pickers and `theme_options()` — colors are + chosen by the OS. This is also the only way to avoid the connector's + registry trap: selecting a registry theme would overwrite the connector's + per-mode configs on the next `Theme::change` and silently drop native + colors until restart. +- **Font size**, **Mono font size**, **Radius**, **Large radius**, + **Focus ring**, **Shadows** rows and their handlers — the resolved theme + provides geometry, and the connector already scales fonts by the OS text + size. Keeping manual overrides would require re-applying them after every + upstream rebuild, a permanent fight with the connector. + +`apply_theme()` shrinks to the fallback-only body (registry lookups + global +field setup), called only when native detection failed at startup and by the +Appearance handler's fallback branch. + +`ThemeSettings` keeps its fields and serde shape — the fallback branch still +reads them, existing settings files keep parsing, and the tests in +`settings.rs` stay valid. They become fallback-only tuning (editable via +`settings.json`). Deleting the fields is a possible follow-up cleanup if the +fallback ever moves to native-theme presets. + +### 4.5 Syntax highlighting (known trade-off) + +`signed.json` carries a `highlight` section (editor and syntax colors) used by +code and markdown rendering (`TextView` in the files view and elsewhere). The +connector's installed per-mode configs carry upstream's default highlighter +styles, so on the native path the active syntax palette is upstream's, not +Signed's. + +Decision for this pass: accept upstream's defaults — zero code. If the Signed +syntax palette must be kept later, investigate whether gpui-component 0.7 +exposes a syntax-theme setter that survives `Theme::change`; do not build an +override mechanism now. + +### 4.6 Features + +Use the connector with narrowed features; the app uses its own SVG icons and +gpui-component widgets: + +```toml +native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] } +``` + +`system-fonts` keeps a theme font family the system lacks drawing in the +platform's substitute. `widgets` (theme-drawn checkbox/switch/etc.), +`material-icons`, `lucide-icons`, `system-icons`, `svg-rasterize` are all +unused here and each carries build weight (the icon bundles are ~2 MB). +Adopting any of them is a separate, deliberate follow-up. + +## 5. Change list + +| File | Change | +| --- | --- | +| `Cargo.toml` | Revert gpui-side pins to 0.3.8 / 0.7.1 (Gate 0); add `native-theme` and `native-theme-gpui` workspace deps | +| `desktop/Cargo.toml` | Add `native-theme`, `native-theme-gpui` | +| `desktop/src/main.rs` | Native apply with bundled fallback (§4.2); move the settings-field setup into the fallback branch | +| `crates/workspace/src/views/sidebar/settings_dialog.rs` | Remove theme picker + font/radius/focus/shadow rows; shrink `apply_theme` to fallback-only (§4.4) | +| `CHANGELOG.md` | Added: native OS theme via `native-theme`; Removed: manual theme/font/radius settings | + +Unchanged: + +- every `cx.theme()` consumer in `dock`, `signed_ui`, `workspace` — including + the dock/tab skins and window controls, which keep reading tokens; +- `crates/workspace/src/workspace.rs` appearance observer; +- `crates/settings` (fields stay), `crates/assets` (`signed.json` stays as + the fallback), `crates/dock`. + +## 6. Validation + +1. `cargo check`, `cargo test`, `cargo clippy` across the workspace (toolchain + 1.96 satisfies the connector's 1.95 MSRV). +2. Manual matrix, per platform available (macOS first; KDE and GNOME Linux; + Windows): + - cold start matches the OS theme and accent; radius, fonts and controls + read as native; + - switching Appearance `System ↔ Light ↔ Dark` in settings switches native + palettes; + - with `System`, changing the OS theme and re-focusing the window updates + the app; + - settings dialog, dock tabs, diffs, files view, markdown rendering look + correct; the syntax palette difference from §4.5 is reviewed and + accepted; + - `cargo run` once with native detection forced to fail (e.g. temporarily + return early in the match) to prove the bundled fallback still works. +3. `cargo tree -i gpui-pre` and `cargo tree -i gpui-component` must each show + exactly one version — the whole point of Gate 0. + +## 7. Explicitly out of scope + +- native-theme icon sets / `CustomIconName` mapping — the app keeps its own + SVG assets. +- theme-drawn widgets (`widgets` feature). +- a preset-picker UI (`from_preset`) in settings. +- runtime OS-theme watching (`watch` feature). +- per-field user overrides layered on the native theme. +- migrating `signed.json` into a native-theme `Theme` definition.