Watch for OS theme changes at runtime
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s

This commit is contained in:
2026-10-21 14:38:40 +07:00
parent 36e5a18982
commit d223879a2e
5 changed files with 90 additions and 27 deletions
Generated
+3
View File
@@ -5699,6 +5699,7 @@ dependencies = [
"fontdb",
"inventory",
"native-theme-derive",
"notify",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
@@ -8337,11 +8338,13 @@ version = "0.2.0-alpha"
dependencies = [
"assets",
"dock",
"flume 0.11.1",
"gpui-component",
"gpui-pre",
"gpui-pre-platform",
"gpui-pre-reqwest-client",
"metadata",
"native-theme",
"native-theme-gpui",
"paths",
"settings",
+1
View File
@@ -18,6 +18,7 @@ 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 = { version = "0.6.1", features = ["watch"] }
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
dock = { path = "crates/dock" }
+2
View File
@@ -38,7 +38,9 @@ gpui.workspace = true
gpui_platform.workspace = true
dock = { workspace = true }
gpui-component.workspace = true
native-theme.workspace = true
native-theme-gpui.workspace = true
flume.workspace = true
reqwest_client.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+76 -21
View File
@@ -5,8 +5,9 @@ use dock::TAB_BAR_HEIGHT;
use gpui::*;
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
use gpui_platform::application;
use native_theme::watch::ThemeSubscription;
use native_theme_gpui::{SystemTheme, apply_system_theme};
use settings::{AppearanceMode, SettingsStore};
use settings::{AppearanceMode, Settings, SettingsStore};
actions!(signed, [Quit]);
@@ -28,26 +29,21 @@ fn main() {
SettingsStore::set_global(store.clone(), cx);
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}");
{
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);
}
match apply_native_theme(&settings, cx) {
Ok(()) => start_theme_watcher(cx),
Err(error) => {
tracing::warn!("native theme unavailable ({error})");
let registry = ThemeRegistry::global_mut(cx);
let light_theme = registry
.themes()
.get(settings.theme.light_theme.as_str())
@@ -77,13 +73,13 @@ fn main() {
} else {
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
}
}
}
match settings.appearance {
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
match settings.appearance {
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
}
}
}
std::fs::create_dir_all(paths::nostr_dir()).ok();
@@ -128,3 +124,62 @@ fn main() {
cx.activate(true);
});
}
/// Apply the OS theme to the app, scaling the font size to match the system font size.
fn apply_native_theme(settings: &Settings, cx: &mut App) -> native_theme::Result<()> {
let mut system = SystemTheme::from_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);
match settings.appearance {
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
}
Ok(())
}
/// Holds the watcher subscription so it lives as long as the app.
struct ThemeWatcher {
_subscription: ThemeSubscription,
}
impl Global for ThemeWatcher {}
/// Re-apply the OS theme whenever it changes at runtime.
fn start_theme_watcher(cx: &mut App) {
let (sender, receiver) = flume::unbounded();
match native_theme::watch::on_theme_change(move |event| {
if let Err(error) = sender.send(event) {
tracing::warn!("theme change dropped ({error})");
}
}) {
Ok(subscription) => {
cx.set_global(ThemeWatcher {
_subscription: subscription,
});
cx.spawn(async move |cx| {
while receiver.recv_async().await.is_ok() {
tracing::info!("OS theme changed; re-applying");
cx.update(|cx| {
let store = SettingsStore::global(cx);
let settings = store.read(cx).settings().clone();
if let Err(error) = apply_native_theme(&settings, cx) {
tracing::warn!("failed to apply updated OS theme ({error})");
}
});
}
})
.detach();
}
Err(error) => {
tracing::warn!("OS theme watching unavailable ({error})")
}
}
}
+8 -6
View File
@@ -169,11 +169,14 @@ and overwriting them fights its observer.
- `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.
Known gap (addressed since): gpui-component's system-appearance follow is
driven by window-activation events. The app now enables native-theme's
`watch` feature and re-applies the theme on its signal (`start_theme_watcher`
in `desktop/src/main.rs`), so live OS changes apply without re-focusing or
restarting. macOS reports appearance changes only (`AppleInterfaceTheme-
ChangedNotification`); an accent-only change posts a different notification
the watcher does not observe yet — Windows (`UISettings::ColorValuesChanged`),
KDE (inotify on `kdeglobals`) and GNOME (portal `SettingChanged`) cover accent.
### 4.4 Settings dialog (`settings_dialog.rs`)
@@ -278,6 +281,5 @@ Unchanged:
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.