Skip to main content

gpui_component/theme/
registry.rs

1use crate::{Theme, ThemeColor, ThemeConfig, ThemeMode, ThemeSet, highlighter::HighlightTheme};
2#[allow(unused)]
3use anyhow::Result;
4use gpui::{App, Global, SharedString};
5use std::{
6    collections::HashMap,
7    path::PathBuf,
8    rc::Rc,
9    sync::{Arc, LazyLock},
10};
11
12const DEFAULT_THEME: &str = include_str!("./default-theme.json");
13pub(crate) static DEFAULT_THEME_COLORS: LazyLock<
14    HashMap<ThemeMode, (Arc<ThemeColor>, Arc<HighlightTheme>)>,
15> = LazyLock::new(|| {
16    let mut colors = HashMap::new();
17
18    let themes: Vec<ThemeConfig> = serde_json::from_str::<ThemeSet>(DEFAULT_THEME)
19        .expect("Failed to parse themes/default.json")
20        .themes;
21
22    for theme in themes {
23        let mut theme_color = ThemeColor::default();
24        theme_color.apply_config(&theme, &ThemeColor::default());
25
26        let highlight_theme = HighlightTheme {
27            name: theme.name.to_string(),
28            appearance: theme.mode,
29            style: theme.highlight.unwrap_or_default(),
30        };
31
32        colors.insert(
33            theme.mode,
34            (Arc::new(theme_color), Arc::new(highlight_theme)),
35        );
36    }
37
38    colors
39});
40
41pub(super) fn init(cx: &mut App) {
42    cx.set_global(ThemeRegistry::default());
43    ThemeRegistry::global_mut(cx).init_default_themes();
44
45    // Observe changes to the theme registry to apply changes to the active theme
46    cx.observe_global::<ThemeRegistry>(|cx| {
47        let mode = Theme::global(cx).mode;
48        let light_theme = Theme::global(cx).light_theme.name.clone();
49        let dark_theme = Theme::global(cx).dark_theme.name.clone();
50
51        if let Some(theme) = ThemeRegistry::global(cx)
52            .themes()
53            .get(&light_theme)
54            .cloned()
55        {
56            Theme::global_mut(cx).light_theme = theme;
57        }
58        if let Some(theme) = ThemeRegistry::global(cx).themes().get(&dark_theme).cloned() {
59            Theme::global_mut(cx).dark_theme = theme;
60        }
61
62        let theme_name = if mode.is_dark() {
63            dark_theme
64        } else {
65            light_theme
66        };
67
68        tracing::info!("Reload active theme: {:?}...", theme_name);
69        Theme::change(mode, None, cx);
70        cx.refresh_windows();
71    })
72    .detach();
73}
74
75#[derive(Default, Debug)]
76pub struct ThemeRegistry {
77    themes_dir: PathBuf,
78    default_themes: HashMap<ThemeMode, Rc<ThemeConfig>>,
79    themes: HashMap<SharedString, Rc<ThemeConfig>>,
80    has_custom_themes: bool,
81}
82
83impl Global for ThemeRegistry {}
84
85impl ThemeRegistry {
86    pub fn global(cx: &App) -> &Self {
87        cx.global::<Self>()
88    }
89
90    pub fn global_mut(cx: &mut App) -> &mut Self {
91        cx.global_mut::<Self>()
92    }
93
94    /// Watch themes directory.
95    ///
96    /// And reload themes to trigger the `on_load` callback.
97    #[cfg(not(target_family = "wasm"))]
98    pub fn watch_dir<F>(themes_dir: PathBuf, cx: &mut App, on_load: F) -> Result<()>
99    where
100        F: Fn(&mut App) + 'static,
101    {
102        Self::global_mut(cx).themes_dir = themes_dir.clone();
103
104        // Load theme in the background.
105        cx.spawn(async move |cx| {
106            _ = cx.update(|cx| {
107                if let Err(err) = Self::_watch_themes_dir(themes_dir, cx) {
108                    tracing::error!("Failed to watch themes directory: {}", err);
109                }
110
111                Self::reload_themes(cx);
112                on_load(cx);
113            });
114        })
115        .detach();
116
117        Ok(())
118    }
119
120    /// Returns a reference to the map of themes (including default themes).
121    pub fn themes(&self) -> &HashMap<SharedString, Rc<ThemeConfig>> {
122        &self.themes
123    }
124
125    /// Returns a sorted list of themes.
126    pub fn sorted_themes(&self) -> Vec<&Rc<ThemeConfig>> {
127        let mut themes = self.themes.values().collect::<Vec<_>>();
128        // sort by is_default true first, then light first dark later, then by name case-insensitive
129        themes.sort_by(|a, b| {
130            b.is_default
131                .cmp(&a.is_default)
132                .then(a.mode.cmp(&b.mode))
133                .then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
134        });
135        themes
136    }
137
138    /// Returns a reference to the map of default themes.
139    pub fn default_themes(&self) -> &HashMap<ThemeMode, Rc<ThemeConfig>> {
140        &self.default_themes
141    }
142
143    pub fn default_light_theme(&self) -> &Rc<ThemeConfig> {
144        &self.default_themes[&ThemeMode::Light]
145    }
146
147    pub fn default_dark_theme(&self) -> &Rc<ThemeConfig> {
148        &self.default_themes[&ThemeMode::Dark]
149    }
150
151    pub fn load_themes_from_str(&mut self, content: &str) -> anyhow::Result<()> {
152        let theme_set = serde_json::from_str::<ThemeSet>(content)?;
153        for theme in theme_set.themes {
154            if !self.themes.contains_key(&theme.name) {
155                let theme_name = theme.name.clone();
156                self.themes.insert(theme_name, Rc::new(theme));
157                self.has_custom_themes = true;
158            }
159        }
160        Ok(())
161    }
162
163    fn init_default_themes(&mut self) {
164        let default_themes: Vec<ThemeConfig> = serde_json::from_str::<ThemeSet>(DEFAULT_THEME)
165            .expect("failed to parse default theme.")
166            .themes;
167        for theme in default_themes.into_iter() {
168            if theme.mode.is_dark() {
169                self.default_themes.insert(ThemeMode::Dark, Rc::new(theme));
170            } else {
171                self.default_themes.insert(ThemeMode::Light, Rc::new(theme));
172            }
173        }
174        self.themes_dir = PathBuf::from("./themes");
175        self.themes = self
176            .default_themes
177            .values()
178            .map(|theme| {
179                let name = theme.name.clone();
180                (name, Rc::clone(theme))
181            })
182            .collect();
183    }
184
185    #[cfg(not(target_family = "wasm"))]
186    fn _watch_themes_dir(themes_dir: PathBuf, cx: &mut App) -> anyhow::Result<()> {
187        use notify::Watcher as _;
188
189        if !themes_dir.exists() {
190            std::fs::create_dir_all(&themes_dir)?;
191        }
192
193        let (tx, rx) = smol::channel::bounded(1);
194        let mut watcher = notify::RecommendedWatcher::new(
195            move |res: notify::Result<notify::Event>| match res {
196                Ok(event) => {
197                    let touched = event.need_rescan()
198                        || matches!(
199                            event.kind,
200                            notify::EventKind::Any
201                                | notify::EventKind::Create(_)
202                                | notify::EventKind::Modify(_)
203                                | notify::EventKind::Remove(_)
204                        );
205                    if touched {
206                        let _ = tx.try_send(());
207                    }
208                }
209                Err(err) => tracing::error!("Failed to receive theme event: {err}"),
210            },
211            notify::Config::default().with_follow_symlinks(false),
212        )?;
213
214        cx.spawn(async move |cx| {
215            if let Err(err) = watcher.watch(&themes_dir, notify::RecursiveMode::NonRecursive) {
216                match err.kind {
217                    notify::ErrorKind::MaxFilesWatch => tracing::error!(
218                        "Theme file watch limit reached, theme hot reload is disabled: {err}"
219                    ),
220                    _ => tracing::error!("Failed to watch themes directory: {err}"),
221                }
222            }
223
224            while rx.recv().await.is_ok() {
225                tracing::info!("Reloading themes...");
226                _ = cx.update(Self::reload_themes);
227            }
228        })
229        .detach();
230
231        Ok(())
232    }
233
234    #[cfg(not(target_family = "wasm"))]
235    fn reload_themes(cx: &mut App) {
236        let registry = Self::global_mut(cx);
237        match registry.reload() {
238            Ok(_) => {
239                tracing::info!("Themes reloaded successfully.");
240            }
241            Err(e) => tracing::error!("Failed to reload themes: {:?}", e),
242        }
243    }
244
245    #[cfg(not(target_family = "wasm"))]
246    /// Reload themes from the `themes_dir`.
247    fn reload(&mut self) -> Result<()> {
248        let mut themes = vec![];
249
250        if self.themes_dir.exists() {
251            for entry in std::fs::read_dir(&self.themes_dir)? {
252                let entry = entry?;
253                let path = entry.path();
254                if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") {
255                    let file_content = std::fs::read_to_string(path.clone())?;
256
257                    match serde_json::from_str::<ThemeSet>(&file_content) {
258                        Ok(theme_set) => {
259                            themes.extend(theme_set.themes);
260                        }
261                        Err(e) => {
262                            tracing::error!(
263                                "ignored invalid theme file: {}, {}",
264                                path.display(),
265                                e
266                            );
267                        }
268                    }
269                }
270            }
271        }
272
273        self.themes.clear();
274        for theme in self.default_themes.values() {
275            self.themes
276                .insert(theme.name.clone(), Rc::new((**theme).clone()));
277        }
278
279        for theme in themes.iter() {
280            if self.themes.contains_key(&theme.name) {
281                continue;
282            }
283
284            if theme.is_default {
285                self.default_themes
286                    .insert(theme.mode, Rc::new(theme.clone()));
287            }
288
289            self.has_custom_themes = true;
290            self.themes
291                .insert(theme.name.clone(), Rc::new(theme.clone()));
292        }
293
294        Ok(())
295    }
296}