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        if !themes_dir.exists() {
188            std::fs::create_dir_all(&themes_dir)?;
189        }
190
191        let (tx, rx) = smol::channel::bounded(100);
192        let mut watcher =
193            notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
194                if let Ok(event) = &res {
195                    match event.kind {
196                        notify::EventKind::Create(_)
197                        | notify::EventKind::Modify(_)
198                        | notify::EventKind::Remove(_) => {
199                            if let Err(err) = tx.send_blocking(res) {
200                                tracing::error!("Failed to send theme event: {:?}", err);
201                            }
202                        }
203                        _ => {}
204                    }
205                }
206            })?;
207
208        cx.spawn(async move |cx| {
209            use notify::Watcher as _;
210
211            if let Err(err) = watcher.watch(&themes_dir, notify::RecursiveMode::Recursive) {
212                tracing::error!("Failed to watch themes directory: {:?}", err);
213            }
214
215            while (rx.recv().await).is_ok() {
216                tracing::info!("Reloading themes...");
217                _ = cx.update(Self::reload_themes);
218            }
219        })
220        .detach();
221
222        Ok(())
223    }
224
225    #[cfg(not(target_family = "wasm"))]
226    fn reload_themes(cx: &mut App) {
227        let registry = Self::global_mut(cx);
228        match registry.reload() {
229            Ok(_) => {
230                tracing::info!("Themes reloaded successfully.");
231            }
232            Err(e) => tracing::error!("Failed to reload themes: {:?}", e),
233        }
234    }
235
236    #[cfg(not(target_family = "wasm"))]
237    /// Reload themes from the `themes_dir`.
238    fn reload(&mut self) -> Result<()> {
239        let mut themes = vec![];
240
241        if self.themes_dir.exists() {
242            for entry in std::fs::read_dir(&self.themes_dir)? {
243                let entry = entry?;
244                let path = entry.path();
245                if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") {
246                    let file_content = std::fs::read_to_string(path.clone())?;
247
248                    match serde_json::from_str::<ThemeSet>(&file_content) {
249                        Ok(theme_set) => {
250                            themes.extend(theme_set.themes);
251                        }
252                        Err(e) => {
253                            tracing::error!(
254                                "ignored invalid theme file: {}, {}",
255                                path.display(),
256                                e
257                            );
258                        }
259                    }
260                }
261            }
262        }
263
264        self.themes.clear();
265        for theme in self.default_themes.values() {
266            self.themes
267                .insert(theme.name.clone(), Rc::new((**theme).clone()));
268        }
269
270        for theme in themes.iter() {
271            if self.themes.contains_key(&theme.name) {
272                continue;
273            }
274
275            if theme.is_default {
276                self.default_themes
277                    .insert(theme.mode, Rc::new(theme.clone()));
278            }
279
280            self.has_custom_themes = true;
281            self.themes
282                .insert(theme.name.clone(), Rc::new(theme.clone()));
283        }
284
285        Ok(())
286    }
287}