Compare commits
4
Commits
4b4b8b5cf2
...
36e5a18982
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36e5a18982 | ||
|
|
6122474f9e | ||
|
|
f7865780d9 | ||
|
|
eb4fbb0a33 |
@@ -6,6 +6,7 @@
|
||||
|
||||
### 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 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
|
||||
@@ -41,6 +42,7 @@
|
||||
|
||||
### 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
|
||||
|
||||
### Deprecated
|
||||
|
||||
Generated
+430
-201
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -13,9 +13,12 @@ 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 Kit
|
||||
gpui-component = { version = "0.7.1", features = ["tree-sitter-languages"] }
|
||||
gpui-base = "0.7.1"
|
||||
# GPUI Kit; exact pins keep the graph on the gpui-pre 0.3.8 closure that
|
||||
# native-theme-gpui 0.6.1 is verified against (0.7.3 and 0.8 pin 0.3.9).
|
||||
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" }
|
||||
settings = { path = "crates/settings" }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::{Error, anyhow, bail};
|
||||
@@ -24,8 +25,6 @@ use crate::repos::RepoListStore;
|
||||
use crate::state::{GitPushMode, GitPushPolicy, GitRef, PushStepStatus, SyncState};
|
||||
|
||||
pub const USER_KEYRING: &str = "Signed Safe Storage";
|
||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
|
||||
|
||||
const PUMP_DEBOUNCE: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -199,13 +198,9 @@ impl Backend {
|
||||
} else if content.starts_with("bunker://") {
|
||||
let (base, keys) = extract_master_key(&content)?;
|
||||
let uri = NostrConnectUri::parse(base)?;
|
||||
let mut signer = NostrConnect::new(
|
||||
uri,
|
||||
keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
)?;
|
||||
signer.auth_url_handler(SignedAuthUrlHandler);
|
||||
let signer = NostrConnect::builder(uri, keys)
|
||||
.auth_url_handler(Arc::new(SignedAuthUrlHandler))
|
||||
.build()?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(signer, cx);
|
||||
@@ -366,13 +361,9 @@ impl Backend {
|
||||
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mut signer = NostrConnect::new(
|
||||
connect_uri,
|
||||
keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
)?;
|
||||
signer.auth_url_handler(SignedAuthUrlHandler);
|
||||
let signer = NostrConnect::builder(connect_uri, keys)
|
||||
.auth_url_handler(Arc::new(SignedAuthUrlHandler))
|
||||
.build()?;
|
||||
|
||||
// Verify the bunker responds before persisting the credential.
|
||||
let public_key = signer.get_public_key_async().await?;
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
use std::cell::Cell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px,
|
||||
};
|
||||
use gpui::{App, Entity, PathPromptOptions, SharedString, Subscription, Window, div, px};
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::input::{
|
||||
Input, InputEvent, InputState, NumberInput, NumberInputEvent, StepAction,
|
||||
};
|
||||
use gpui_component::input::{Input, InputState};
|
||||
use gpui_component::select::{Select, SelectEvent, SelectState};
|
||||
use gpui_component::separator::Separator;
|
||||
use gpui_component::setting::NumberFieldOptions;
|
||||
use gpui_component::switch::Switch;
|
||||
use gpui_component::{
|
||||
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, ThemeRegistry, WindowExt, h_flex,
|
||||
v_flex,
|
||||
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, WindowExt, h_flex, v_flex,
|
||||
};
|
||||
use settings::{AppearanceMode, EventFetchingStrategy, Settings, SettingsStore};
|
||||
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))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
struct SettingsControls {
|
||||
appearance: 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>,
|
||||
/// The effective create-repository folder, shown in a disabled input.
|
||||
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 =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("wss://relay.example.com"));
|
||||
|
||||
let default_folder = cx.new(|cx| {
|
||||
let folder = settings
|
||||
.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 {
|
||||
appearance,
|
||||
event_fetching,
|
||||
light_theme,
|
||||
dark_theme,
|
||||
font_size,
|
||||
mono_font_size,
|
||||
radius,
|
||||
radius_lg,
|
||||
grasp_server_input,
|
||||
default_folder,
|
||||
_subscriptions: subscriptions,
|
||||
@@ -320,8 +151,6 @@ fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement
|
||||
.w_full()
|
||||
.child(appearance_section(controls, cx))
|
||||
.child(Separator::horizontal())
|
||||
.child(theme_section(&settings, controls, cx))
|
||||
.child(Separator::horizontal())
|
||||
.child(tab_bar_section(&settings, cx))
|
||||
.child(Separator::horizontal())
|
||||
.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.
|
||||
fn tab_bar_section(settings: &Settings, cx: &App) -> impl IntoElement {
|
||||
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();
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
match appearance {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ gpui.workspace = true
|
||||
gpui_platform.workspace = true
|
||||
dock = { workspace = true }
|
||||
gpui-component.workspace = true
|
||||
native-theme-gpui.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
+22
-9
@@ -5,6 +5,7 @@ use dock::TAB_BAR_HEIGHT;
|
||||
use gpui::*;
|
||||
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
|
||||
use gpui_platform::application;
|
||||
use native_theme_gpui::{SystemTheme, apply_system_theme};
|
||||
use settings::{AppearanceMode, SettingsStore};
|
||||
|
||||
actions!(signed, [Quit]);
|
||||
@@ -25,18 +26,33 @@ fn main() {
|
||||
// The persisted settings must load before the theme is applied.
|
||||
let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), 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);
|
||||
|
||||
for (name, content) in Assets.themes() {
|
||||
if let Err(err) = registry.load_themes_from_str(&content) {
|
||||
tracing::error!("Failed to load theme {name}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
match SystemTheme::from_system() {
|
||||
Ok(mut system) => {
|
||||
let native = system.light.defaults.font.size;
|
||||
if native > 0.0 {
|
||||
let scaling = settings.theme.font_size / native;
|
||||
system.accessibility.text_scaling_factor *= scaling;
|
||||
}
|
||||
apply_system_theme(&system, cx);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("native theme unavailable ({error})");
|
||||
|
||||
let light_theme = registry
|
||||
.themes()
|
||||
.get(settings.theme.light_theme.as_str())
|
||||
.cloned();
|
||||
|
||||
let dark_theme = registry
|
||||
.themes()
|
||||
.get(settings.theme.dark_theme.as_str())
|
||||
@@ -53,19 +69,15 @@ fn main() {
|
||||
if let Some(light) = light_theme {
|
||||
theme.light_theme = light;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"{} theme is missing from the registry",
|
||||
settings.theme.light_theme
|
||||
);
|
||||
tracing::warn!("{} theme is missing", settings.theme.light_theme);
|
||||
}
|
||||
|
||||
if let Some(dark) = dark_theme {
|
||||
theme.dark_theme = dark;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"{} theme is missing from the registry",
|
||||
settings.theme.dark_theme
|
||||
);
|
||||
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match settings.appearance {
|
||||
@@ -76,6 +88,7 @@ fn main() {
|
||||
|
||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||
std::fs::create_dir_all(paths::repos_dir()).ok();
|
||||
|
||||
signed_state::init(
|
||||
paths::nostr_dir(),
|
||||
paths::repos_dir().clone(),
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
# 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`/`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) |
|
||||
| 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-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
|
||||
```
|
||||
|
||||
- This is the state of `master` before `eb4fbb0`, the version set the
|
||||
connector was verified against (2026-10-08 stamp). The exact `=0.7.1`
|
||||
pins are required, not a style choice: a caret `"0.7.1"` resolves 0.7.3,
|
||||
whose `gpui-pre =0.3.9` pin drags the graph onto 0.3.9, outside the
|
||||
closure the connector is verified against. The three `gpui-pre-*` family
|
||||
crates are published in lockstep; all three 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 `Deref` removal in `crates/dock/src/dock_area.rs` is restored; the
|
||||
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
|
||||
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
|
||||
// native_theme_gpui re-exports SystemTheme from native-theme.
|
||||
match native_theme_gpui::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.
|
||||
|
||||
Sizing note: gpui-component derives the window rem from `theme.font_size`, so
|
||||
every rem-relative element scales with it. The OS-reported UI font (13 on
|
||||
macOS) is smaller than the 16 this app is designed around, so the native path
|
||||
multiplies `system.accessibility.text_scaling_factor` by
|
||||
`settings.theme.font_size / native_size` before applying — the connector's
|
||||
supported scaling knob, which also preserves any OS accessibility scaling.
|
||||
The same factor scales `mono_font_size`, so code text follows the UI size
|
||||
(the pre-migration app used 13 for mono while the UI was 16).
|
||||
|
||||
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()` and `theme_options()` are deleted outright: with the pickers
|
||||
and number fields gone they have no remaining callers. The fallback logic
|
||||
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
|
||||
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 the `native-theme-gpui` workspace dep |
|
||||
| `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 |
|
||||
| `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 --workspace`, `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.
|
||||
Reference in New Issue
Block a user