Follow the OS native theme on macOS, Windows and Linux

This commit is contained in:
2026-10-21 13:57:01 +07:00
parent f7865780d9
commit 6122474f9e
8 changed files with 797 additions and 513 deletions
+2
View File
@@ -6,6 +6,7 @@
### Added ### Added
- Follow the operating system's native theme on macOS, Windows and Linux (KDE, GNOME) via `native-theme`, applying the OS colors, fonts, radii and accessibility preferences; the bundled themes remain as the fallback when the OS theme cannot be read
- Add an identity import dialog, importing an existing identity by pasting its `nsec` secret key (encrypted with a new passphrase), NIP-49 `ncryptsec` (unlocked with its passphrase), or `bunker://` NIP-46 URI - Add an identity import dialog, importing an existing identity by pasting its `nsec` secret key (encrypted with a new passphrase), NIP-49 `ncryptsec` (unlocked with its passphrase), or `bunker://` NIP-46 URI
- Add a dropdown menu on the signed-in user button in the sidebar - Add a dropdown menu on the signed-in user button in the sidebar
- Add sign-out feature, confirmed by an alert dialog before deleting the stored session credential - Add sign-out feature, confirmed by an alert dialog before deleting the stored session credential
@@ -41,6 +42,7 @@
### Removed ### Removed
- Remove the light/dark theme pickers and the font size, corner radius, focus ring and shadow settings from the settings dialog; colors and geometry now come from the native theme
- Remove dead code: the unused `login`/`logout` family, the unwired `SyncProgress` pipeline, `merge_pull_request`, inbox mark-read/archive APIs and the wasm32-only code paths - Remove dead code: the unused `login`/`logout` family, the unwired `SyncProgress` pipeline, `merge_pull_request`, inbox mark-read/archive APIs and the wasm32-only code paths
### Deprecated ### Deprecated
Generated
+714 -104
View File
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -9,13 +9,16 @@ edition = "2024"
publish = false publish = false
[workspace.dependencies] [workspace.dependencies]
gpui = { package = "gpui-pre", version = "0.3.9" } gpui = { package = "gpui-pre", version = "0.3.8" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.9", features = ["font-kit", "x11", "wayland"] } 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.9" } reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
# GPUI Kit # GPUI Kit; exact pins keep the graph on the gpui-pre 0.3.8 closure that
gpui-component = { version = "0.8", features = ["tree-sitter-languages"] } # native-theme-gpui 0.6.1 is verified against (0.7.3 and 0.8 pin 0.3.9).
gpui-base = "0.8" gpui-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
gpui-base = "=0.7.1"
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
dock = { path = "crates/dock" } dock = { path = "crates/dock" }
settings = { path = "crates/settings" } settings = { path = "crates/settings" }
+2 -1
View File
@@ -1,4 +1,5 @@
use std::cell::Cell; use std::cell::Cell;
use std::ops::Deref as _;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
@@ -172,7 +173,7 @@ impl SignedDockSkin {
.on_drag(ResizePanel, move |info, _, _, cx| { .on_drag(ResizePanel, move |info, _, _, cx| {
cx.stop_propagation(); cx.stop_propagation();
shared.resizing_dock().set(Some(placement)); shared.resizing_dock().set(Some(placement));
cx.new(|_| info.clone()) cx.new(|_| info.deref().clone())
}) })
} }
} }
@@ -1,22 +1,15 @@
use std::cell::Cell;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{ use gpui::{App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px};
AnyWindowHandle, App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px,
};
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::{ use gpui_component::input::{Input, InputState};
Input, InputEvent, InputState, NumberInput, NumberInputEvent, StepAction,
};
use gpui_component::select::{Select, SelectEvent, SelectState}; use gpui_component::select::{Select, SelectEvent, SelectState};
use gpui_component::separator::Separator; use gpui_component::separator::Separator;
use gpui_component::setting::NumberFieldOptions;
use gpui_component::switch::Switch; use gpui_component::switch::Switch;
use gpui_component::{ use gpui_component::{
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, ThemeRegistry, WindowExt, h_flex, ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, WindowExt, h_flex, v_flex,
v_flex,
}; };
use settings::{AppearanceMode, EventFetchingStrategy, Settings, SettingsStore}; use settings::{AppearanceMode, EventFetchingStrategy, Settings, SettingsStore};
use signed_ui::{SelectOption, setting_block, setting_row}; use signed_ui::{SelectOption, setting_block, setting_row};
@@ -31,34 +24,10 @@ fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
.map(|row| IndexPath::default().row(row)) .map(|row| IndexPath::default().row(row))
} }
/// Registered themes split into light and dark options, light first.
fn theme_options(cx: &App) -> (Vec<SelectOption>, Vec<SelectOption>) {
let registry = ThemeRegistry::global(cx);
let mut light = Vec::new();
let mut dark = Vec::new();
for config in registry.themes().values() {
let name = config.name.clone();
if config.mode.is_dark() {
dark.push(SelectOption::new(name.clone(), name));
} else {
light.push(SelectOption::new(name.clone(), name));
}
}
light.sort_by(|a, b| a.label().as_ref().cmp(b.label().as_ref()));
dark.sort_by(|a, b| a.label().as_ref().cmp(b.label().as_ref()));
(light, dark)
}
/// Created once when the dialog opens, so control state survives re-renders. /// Created once when the dialog opens, so control state survives re-renders.
struct SettingsControls { struct SettingsControls {
appearance: Entity<SelectState<Vec<SelectOption>>>, appearance: Entity<SelectState<Vec<SelectOption>>>,
event_fetching: Entity<SelectState<Vec<SelectOption>>>, event_fetching: Entity<SelectState<Vec<SelectOption>>>,
light_theme: Entity<SelectState<Vec<SelectOption>>>,
dark_theme: Entity<SelectState<Vec<SelectOption>>>,
font_size: Entity<InputState>,
mono_font_size: Entity<InputState>,
radius: Entity<InputState>,
radius_lg: Entity<InputState>,
grasp_server_input: Entity<InputState>, grasp_server_input: Entity<InputState>,
/// The effective create-repository folder, shown in a disabled input. /// The effective create-repository folder, shown in a disabled input.
default_folder: Entity<InputState>, default_folder: Entity<InputState>,
@@ -107,38 +76,9 @@ impl SettingsControls {
) )
}); });
let (light_options, dark_options) = theme_options(cx);
let light_theme = cx.new(|cx| {
SelectState::new(
light_options.clone(),
selected_index(&light_options, &settings.theme.light_theme),
window,
cx,
)
});
let dark_theme = cx.new(|cx| {
SelectState::new(
dark_options.clone(),
selected_index(&dark_options, &settings.theme.dark_theme),
window,
cx,
)
});
let font_size = cx.new(|cx| {
InputState::new(window, cx).default_value(settings.theme.font_size.to_string())
});
let mono_font_size = cx.new(|cx| {
InputState::new(window, cx).default_value(settings.theme.mono_font_size.to_string())
});
let radius = cx
.new(|cx| InputState::new(window, cx).default_value(settings.theme.radius.to_string()));
let radius_lg = cx.new(|cx| {
InputState::new(window, cx).default_value(settings.theme.radius_lg.to_string())
});
let grasp_server_input = let grasp_server_input =
cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com")); cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
let default_folder = cx.new(|cx| { let default_folder = cx.new(|cx| {
let folder = settings let folder = settings
.create_repository .create_repository
@@ -178,118 +118,9 @@ impl SettingsControls {
} }
})); }));
subscriptions.push(cx.subscribe(&light_theme, |_, event, cx| {
if let SelectEvent::Confirm(Some(value)) = event {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(
|settings| settings.theme.light_theme = value.to_string(),
cx,
);
});
apply_theme(cx);
}
}));
subscriptions.push(cx.subscribe(&dark_theme, |_, event, cx| {
if let SelectEvent::Confirm(Some(value)) = event {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.dark_theme = value.to_string(), cx);
});
apply_theme(cx);
}
}));
wire_number_input(
&font_size,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 10.,
max: 32.,
..Default::default()
},
settings.theme.font_size as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.font_size = value as f32, cx);
});
apply_theme(cx);
},
);
wire_number_input(
&mono_font_size,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 8.,
max: 32.,
..Default::default()
},
settings.theme.mono_font_size as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.mono_font_size = value as f32, cx);
});
apply_theme(cx);
},
);
wire_number_input(
&radius,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 0.,
max: 24.,
..Default::default()
},
settings.theme.radius as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.radius = value as f32, cx);
});
apply_theme(cx);
},
);
wire_number_input(
&radius_lg,
&mut subscriptions,
window.window_handle(),
cx,
NumberFieldOptions {
min: 0.,
max: 24.,
..Default::default()
},
settings.theme.radius_lg as f64,
|value, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.radius_lg = value as f32, cx);
});
apply_theme(cx);
},
);
Self { Self {
appearance, appearance,
event_fetching, event_fetching,
light_theme,
dark_theme,
font_size,
mono_font_size,
radius,
radius_lg,
grasp_server_input, grasp_server_input,
default_folder, default_folder,
_subscriptions: subscriptions, _subscriptions: subscriptions,
@@ -320,8 +151,6 @@ fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement
.w_full() .w_full()
.child(appearance_section(controls, cx)) .child(appearance_section(controls, cx))
.child(Separator::horizontal()) .child(Separator::horizontal())
.child(theme_section(&settings, controls, cx))
.child(Separator::horizontal())
.child(tab_bar_section(&settings, cx)) .child(tab_bar_section(&settings, cx))
.child(Separator::horizontal()) .child(Separator::horizontal())
.child(grasp_servers_section(&settings, controls, cx)) .child(grasp_servers_section(&settings, controls, cx))
@@ -350,76 +179,6 @@ fn event_fetching_section(controls: &SettingsControls, cx: &App) -> impl IntoEle
)) ))
} }
fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> impl IntoElement {
v_flex()
.gap_3()
.w_full()
.child(setting_row(
cx,
"Light Theme",
"The theme to use when the appearance is light.",
Select::new(&controls.light_theme).w_full(),
))
.child(setting_row(
cx,
"Dark Theme",
"The theme to use when the appearance is dark.",
Select::new(&controls.dark_theme).w_full(),
))
.child(setting_row(
cx,
"UI Font Size",
"Font size for the UI.",
NumberInput::new(&controls.font_size).w_full(),
))
.child(setting_row(
cx,
"Editor Font Size",
"Font size for editor text.",
NumberInput::new(&controls.mono_font_size).w_full(),
))
.child(setting_row(
cx,
"Corner Radius",
"The corner radius for UI elements.",
NumberInput::new(&controls.radius).w_full(),
))
.child(setting_row(
cx,
"Large Corner Radius",
"The corner radius for large UI elements (dialogs, notifications).",
NumberInput::new(&controls.radius_lg).w_full(),
))
.child(setting_row(
cx,
"Focus Ring",
"Draw a ring around focused controls.",
Switch::new("focus-ring")
.checked(settings.theme.focus_ring)
.on_click(move |checked: &bool, _window, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.focus_ring = *checked, cx);
});
apply_theme(cx);
}),
))
.child(setting_row(
cx,
"Shadows",
"The shadow effect for UI elements.",
Switch::new("shadows")
.checked(settings.theme.shadow)
.on_click(move |checked: &bool, _window, cx| {
let store = SettingsStore::global(cx);
store.update(cx, |store, cx| {
store.edit(|settings| settings.theme.shadow = *checked, cx);
});
apply_theme(cx);
}),
))
}
/// Tab bar chrome, such as its next/previous tab buttons. /// Tab bar chrome, such as its next/previous tab buttons.
fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement { fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement {
v_flex().w_full().gap_3().child(setting_row( v_flex().w_full().gap_3().child(setting_row(
@@ -724,79 +483,6 @@ fn choose_default_folder(default_folder: &Entity<InputState>, window: &mut Windo
.detach(); .detach();
} }
/// Step actions clamp and persist the value; typed changes parse, clamp and persist.
fn wire_number_input(
state: &Entity<InputState>,
subscriptions: &mut Vec<Subscription>,
window_handle: AnyWindowHandle,
cx: &mut App,
options: NumberFieldOptions,
initial: f64,
on_change: impl Fn(f64, &mut App) + 'static,
) {
let on_change = Rc::new(on_change);
let initial_value = Rc::new(Cell::new(initial));
let (min, max, step) = (options.min, options.max, options.step);
subscriptions.push(cx.subscribe(&state.clone(), {
let state = state.clone();
let initial_value = initial_value.clone();
let on_change = on_change.clone();
move |_, event: &NumberInputEvent, cx| {
let NumberInputEvent::Step(action) = event;
let value = state.read(cx).value();
if let Ok(value) = value.parse::<f64>() {
let new_value = match action {
StepAction::Increment => value + step,
StepAction::Decrement => value - step,
};
let clamped = new_value.clamp(min, max);
window_handle
.update(cx, |_, window, cx| {
state.update(cx, |input, cx| {
input.set_value(SharedString::from(clamped.to_string()), window, cx);
});
})
.ok();
initial_value.set(clamped);
on_change(clamped, cx);
}
}
}));
subscriptions.push(cx.subscribe(&state.clone(), {
let state = state.clone();
let initial_value = initial_value.clone();
let on_change = on_change.clone();
move |_, event: &InputEvent, cx| {
if matches!(event, InputEvent::Change) {
let value = state.read(cx).value();
if value == initial_value.get().to_string() {
return;
}
if let Ok(parsed) = value.parse::<f64>() {
let clamped = parsed.clamp(min, max);
initial_value.set(clamped);
on_change(clamped, cx);
if clamped != parsed {
window_handle
.update(cx, |_, window, cx| {
state.update(cx, |input, cx| {
input.set_value(
SharedString::from(clamped.to_string()),
window,
cx,
);
});
})
.ok();
}
}
}
}
}));
}
fn apply_appearance(appearance: AppearanceMode, cx: &mut App) { fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
match appearance { match appearance {
AppearanceMode::System => Theme::sync_system_appearance(None, cx), AppearanceMode::System => Theme::sync_system_appearance(None, cx),
@@ -804,33 +490,3 @@ fn apply_appearance(appearance: AppearanceMode, cx: &mut App) {
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx), AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
} }
} }
fn apply_theme(cx: &mut App) {
let store = SettingsStore::global(cx);
let settings = store.read(cx).settings().theme.clone();
let registry = ThemeRegistry::global(cx);
let light_config = registry
.themes()
.get(settings.light_theme.as_str())
.cloned();
let dark_config = registry.themes().get(settings.dark_theme.as_str()).cloned();
let mode = Theme::global(cx).mode;
let theme = Theme::global_mut(cx);
theme.radius = px(settings.radius);
theme.radius_lg = px(settings.radius_lg);
theme.focus_ring = settings.focus_ring;
theme.shadow = settings.shadow;
theme.font_size = px(settings.font_size);
theme.mono_font_size = px(settings.mono_font_size);
if let Some(config) = light_config {
theme.light_theme = config;
}
if let Some(config) = dark_config {
theme.dark_theme = config;
}
// Re-apply the active mode so the updated configs take effect.
Theme::change(mode, None, cx);
}
+1
View File
@@ -38,6 +38,7 @@ gpui.workspace = true
gpui_platform.workspace = true gpui_platform.workspace = true
dock = { workspace = true } dock = { workspace = true }
gpui-component.workspace = true gpui-component.workspace = true
native-theme-gpui.workspace = true
reqwest_client.workspace = true reqwest_client.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
+15 -9
View File
@@ -5,6 +5,7 @@ use dock::TAB_BAR_HEIGHT;
use gpui::*; use gpui::*;
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme}; use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
use gpui_platform::application; use gpui_platform::application;
use native_theme_gpui::{SystemTheme, apply_system_theme};
use settings::{AppearanceMode, SettingsStore}; use settings::{AppearanceMode, SettingsStore};
actions!(signed, [Quit]); actions!(signed, [Quit]);
@@ -25,18 +26,26 @@ fn main() {
// The persisted settings must load before the theme is applied. // The persisted settings must load before the theme is applied.
let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), cx)); let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), cx));
SettingsStore::set_global(store.clone(), cx); SettingsStore::set_global(store.clone(), cx);
let settings = store.read(cx).settings().clone();
let settings = store.read(cx).settings().clone();
let registry = ThemeRegistry::global_mut(cx); let registry = ThemeRegistry::global_mut(cx);
for (name, content) in Assets.themes() { for (name, content) in Assets.themes() {
if let Err(err) = registry.load_themes_from_str(&content) { if let Err(err) = registry.load_themes_from_str(&content) {
tracing::error!("Failed to load theme {name}: {err}"); tracing::error!("Failed to load theme {name}: {err}");
} }
} }
match SystemTheme::from_system() {
Ok(system) => apply_system_theme(&system, cx),
Err(error) => {
tracing::warn!("native theme unavailable ({error})");
let light_theme = registry let light_theme = registry
.themes() .themes()
.get(settings.theme.light_theme.as_str()) .get(settings.theme.light_theme.as_str())
.cloned(); .cloned();
let dark_theme = registry let dark_theme = registry
.themes() .themes()
.get(settings.theme.dark_theme.as_str()) .get(settings.theme.dark_theme.as_str())
@@ -53,19 +62,15 @@ fn main() {
if let Some(light) = light_theme { if let Some(light) = light_theme {
theme.light_theme = light; theme.light_theme = light;
} else { } else {
tracing::warn!( tracing::warn!("{} theme is missing", settings.theme.light_theme);
"{} theme is missing from the registry",
settings.theme.light_theme
);
} }
if let Some(dark) = dark_theme { if let Some(dark) = dark_theme {
theme.dark_theme = dark; theme.dark_theme = dark;
} else { } else {
tracing::warn!( tracing::warn!("{} theme is missing", settings.theme.dark_theme);
"{} theme is missing from the registry", }
settings.theme.dark_theme }
);
} }
match settings.appearance { match settings.appearance {
@@ -76,6 +81,7 @@ fn main() {
std::fs::create_dir_all(paths::nostr_dir()).ok(); std::fs::create_dir_all(paths::nostr_dir()).ok();
std::fs::create_dir_all(paths::repos_dir()).ok(); std::fs::create_dir_all(paths::repos_dir()).ok();
signed_state::init( signed_state::init(
paths::nostr_dir(), paths::nostr_dir(),
paths::repos_dir().clone(), paths::repos_dir().clone(),
+28 -23
View File
@@ -58,7 +58,8 @@ Verified against crates.io and the upstream repo on 2026-10-21:
| --- | --- | | --- | --- |
| Latest connector | `native-theme-gpui` 0.6.1 (2026-10-09); upstream `main` is the same | | 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 | | 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`/`gpui-base` 0.7.1 pin | `gpui-pre =0.3.8` |
| `gpui-component`/`gpui-base` 0.7.3 pin | `gpui-pre =0.3.9` |
| `gpui-component` 0.8.0 pins | `gpui-pre =0.3.9` (0.8.0 published 2026-10-19) | | `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") | | This branch | moved 0.7.1 / 0.3.8 → 0.8 / 0.3.9 in `eb4fbb0` ("chore: update deps") |
@@ -79,23 +80,25 @@ gpui = { package = "gpui-pre", version = "0.3.8" }
gpui_platform = { package = "gpui-pre-platform", version = "0.3.8", features = ["font-kit", "x11", "wayland"] } 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" } reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
gpui-component = { version = "0.7.1", features = ["tree-sitter-languages"] } gpui-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
gpui-base = "0.7.1" gpui-base = "=0.7.1"
native-theme = "0.6.1" native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
native-theme-gpui = "0.6.1"
``` ```
- This is exactly the state of `master` before `eb4fbb0`, which is also the - This is the state of `master` before `eb4fbb0`, the version set the
version set the connector was verified against (2026-10-08 stamp) and that connector was verified against (2026-10-08 stamp). The exact `=0.7.1`
its nightly dependency canary exercises. `"0.7.1"` is a caret requirement; pins are required, not a style choice: a caret `"0.7.1"` resolves 0.7.3,
it resolves the newest 0.7.x (currently 0.7.3). Pin `=0.7.1` instead if whose `gpui-pre =0.3.9` pin drags the graph onto 0.3.9, outside the
strict reproducibility is preferred. closure the connector is verified against. The three `gpui-pre-*` family
- The three `gpui-pre-*` family crates are published in lockstep; all three crates are published in lockstep; all three move back to 0.3.8 together.
move back to 0.3.8 together. - The connector re-exports the `native-theme` types that appear in its
public signatures (`SystemTheme`, `AccessibilityPreferences`, …), so the
app does **not** need `native-theme` as a direct dependency.
- The cost of the revert is the two one-line tweaks `eb4fbb0` made for 0.8 - 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 (the `Deref` removal in `crates/dock/src/dock_area.rs` is restored; the
either way). The nostr changes in that commit are unrelated and stay. 0.7.1 `ResizeHandle::on_drag` hands the closure an `Rc<T>` and requires
`E: Render`). The nostr changes in that commit are unrelated and stay.
- When `native-theme-gpui` releases gpui-component 0.8 support (upstream has - 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 bumped within days of each gpui-component release so far), bump both sides
together in one commit. together in one commit.
@@ -128,8 +131,8 @@ Order is load-bearing:
4. Apply the native theme: 4. Apply the native theme:
```rust ```rust
let prefs = native_theme_gpui::AccessibilityPreferences::from_system(); // native_theme_gpui re-exports SystemTheme from native-theme.
match native_theme::SystemTheme::from_system() { match native_theme_gpui::SystemTheme::from_system() {
Ok(system) => native_theme_gpui::apply_system_theme(&system, cx), Ok(system) => native_theme_gpui::apply_system_theme(&system, cx),
Err(error) => { Err(error) => {
tracing::warn!("native theme unavailable ({error}); using bundled themes"); tracing::warn!("native theme unavailable ({error}); using bundled themes");
@@ -182,9 +185,11 @@ Remove (the native theme now owns all of these):
size. Keeping manual overrides would require re-applying them after every size. Keeping manual overrides would require re-applying them after every
upstream rebuild, a permanent fight with the connector. upstream rebuild, a permanent fight with the connector.
`apply_theme()` shrinks to the fallback-only body (registry lookups + global `apply_theme()` and `theme_options()` are deleted outright: with the pickers
field setup), called only when native detection failed at startup and by the and number fields gone they have no remaining callers. The fallback logic
Appearance handler's fallback branch. lives only in `main.rs`'s `Err` branch; the Appearance handler keeps working
through `apply_appearance()` alone (`Theme::change` picks up whatever configs
are installed, bundled or native).
`ThemeSettings` keeps its fields and serde shape — the fallback branch still `ThemeSettings` keeps its fields and serde shape — the fallback branch still
reads them, existing settings files keep parsing, and the tests in reads them, existing settings files keep parsing, and the tests in
@@ -224,8 +229,8 @@ Adopting any of them is a separate, deliberate follow-up.
| File | Change | | 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 | | `Cargo.toml` | Revert gpui-side pins to 0.3.8 / `=0.7.1` (Gate 0); add the `native-theme-gpui` workspace dep |
| `desktop/Cargo.toml` | Add `native-theme`, `native-theme-gpui` | | `desktop/Cargo.toml` | Add `native-theme-gpui` |
| `desktop/src/main.rs` | Native apply with bundled fallback (§4.2); move the settings-field setup into the fallback branch | | `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) | | `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 | | `CHANGELOG.md` | Added: native OS theme via `native-theme`; Removed: manual theme/font/radius settings |
@@ -240,8 +245,8 @@ Unchanged:
## 6. Validation ## 6. Validation
1. `cargo check`, `cargo test`, `cargo clippy` across the workspace (toolchain 1. `cargo check`, `cargo test --workspace`, `cargo clippy` across the workspace
1.96 satisfies the connector's 1.95 MSRV). (toolchain 1.96 satisfies the connector's 1.95 MSRV).
2. Manual matrix, per platform available (macOS first; KDE and GNOME Linux; 2. Manual matrix, per platform available (macOS first; KDE and GNOME Linux;
Windows): Windows):
- cold start matches the OS theme and accent; radius, fonts and controls - cold start matches the OS theme and accent; radius, fonts and controls