diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e362c3..d10be7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ separately in [CHANGELOG-DEPS.md](CHANGELOG-DEPS.md). ## Unreleased +- **System theme follows the Linux desktop.** Ferail reads GPUI’s portal-backed + appearance instead of requiring GNOME settings schemas, and updates open + windows when the desktop switches between light and dark. + - **Going up to a parent folder selects the folder you came from and scrolls it into view, whichever way you go up.** Backspace and Back already did this; clicking a breadcrumb segment, a folder in the sidebar tree, or typing diff --git a/crates/ferail-gpui/src/boot.rs b/crates/ferail-gpui/src/boot.rs index f67a6e3..6422d9d 100644 --- a/crates/ferail-gpui/src/boot.rs +++ b/crates/ferail-gpui/src/boot.rs @@ -221,31 +221,53 @@ pub fn run_gui(args: screenshot::Args) { // 1. `--theme {light,dark}` CLI flag // 2. `FERAIL_THEME` env var (light / dark / system) // 3. Persisted `theme_pref` in app_state - // 4. macOS Appearance via `system_is_dark()` + // 4. System appearance let env_theme = std::env::var("FERAIL_THEME").ok().map(|s| s.to_lowercase()); let persisted_theme = crate::app_state::load().theme_pref; let resolve_string = |s: &str| -> Option { match s { "light" => Some(gpui_component::ThemeMode::Light), "dark" => Some(gpui_component::ThemeMode::Dark), - "system" | "auto" => Some(if crate::platform_shell::system_is_dark() { - gpui_component::ThemeMode::Dark - } else { - gpui_component::ThemeMode::Light - }), + "system" | "auto" => Some(crate::settings::ThemePref::System.resolve(cx)), _ => None, } }; let mode = theme_mode .or_else(|| env_theme.as_deref().and_then(resolve_string)) .or_else(|| persisted_theme.as_deref().and_then(resolve_string)) - .unwrap_or_else(|| { - if crate::platform_shell::system_is_dark() { - gpui_component::ThemeMode::Dark - } else { - gpui_component::ThemeMode::Light + .unwrap_or_else(|| crate::settings::ThemePref::System.resolve(cx)); + #[cfg(target_os = "linux")] + { + let configured = env_theme + .as_deref() + .filter(|value| matches!(*value, "light" | "dark" | "system" | "auto")) + .or(persisted_theme.as_deref()); + cx.set_global(crate::settings::FollowSystemTheme( + theme_mode.is_none() && !matches!(configured, Some("light" | "dark")), + )); + fn sync_system_theme(window: &mut gpui::Window, cx: &mut gpui::App) { + if cx + .try_global::() + .is_some_and(|follow| follow.0) + { + let mode: gpui_component::ThemeMode = window.appearance().into(); + if Theme::global(cx).mode != mode { + Theme::change(mode, Some(window), cx); + crate::shell::set_system_theme_pending(matches!( + mode, + gpui_component::ThemeMode::Dark + )); + } } - }); + } + cx.observe_new::(|_, window, cx| { + if let Some(window) = window { + sync_system_theme(window, cx); + window.observe_window_appearance(sync_system_theme).detach(); + } + }) + .detach(); + } Theme::change(mode, None, cx); // Sync native chrome (titlebars, traffic lights, menus) to the // app theme so secondary windows don't show system-dark chrome diff --git a/crates/ferail-gpui/src/settings.rs b/crates/ferail-gpui/src/settings.rs index 23701e2..ac20364 100644 --- a/crates/ferail-gpui/src/settings.rs +++ b/crates/ferail-gpui/src/settings.rs @@ -122,6 +122,11 @@ pub fn category_from_arg(arg: Option<&str>) -> SettingsCategory { // Theme preference: external API // ============================================================================= +#[cfg(target_os = "linux")] +pub(crate) struct FollowSystemTheme(pub bool); +#[cfg(target_os = "linux")] +impl gpui::Global for FollowSystemTheme {} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum ThemePref { Light, @@ -146,15 +151,22 @@ impl ThemePref { } } - fn resolve(self) -> ThemeMode { + pub(crate) fn resolve(self, _cx: &App) -> ThemeMode { match self { ThemePref::Light => ThemeMode::Light, ThemePref::Dark => ThemeMode::Dark, ThemePref::System => { - if crate::platform_shell::system_is_dark() { - ThemeMode::Dark - } else { - ThemeMode::Light + #[cfg(target_os = "linux")] + { + _cx.window_appearance().into() + } + #[cfg(not(target_os = "linux"))] + { + if crate::platform_shell::system_is_dark() { + ThemeMode::Dark + } else { + ThemeMode::Light + } } } } @@ -2611,7 +2623,12 @@ fn theme_tile(pref: ThemePref) -> impl IntoElement { // when given a Window. `cx.refresh_windows()` propagates // the same change to any other open window (e.g. the // main Shell while Settings is open in a second window). - let resolved = pref.resolve(); + let resolved = pref.resolve(cx); + #[cfg(target_os = "linux")] + { + cx.set_global(FollowSystemTheme(pref == ThemePref::System)); + crate::shell::set_system_theme_pending(matches!(resolved, ThemeMode::Dark)); + } Theme::change(resolved, Some(window), cx); // Match native chrome across all windows to the new theme. crate::platform_shell::set_app_appearance(matches!(