registry.rs

  1use std::sync::Arc;
  2use std::{fmt::Debug, path::Path};
  3
  4use anyhow::{Context as _, Result};
  5use collections::HashMap;
  6use derive_more::{Deref, DerefMut};
  7use fs::Fs;
  8use futures::StreamExt;
  9use gpui::{App, AssetSource, Global, SharedString};
 10use parking_lot::RwLock;
 11use thiserror::Error;
 12use util::ResultExt;
 13
 14use crate::{
 15    Appearance, AppearanceContent, ChevronIcons, DEFAULT_ICON_THEME_NAME, DirectoryIcons,
 16    IconDefinition, IconTheme, Theme, ThemeFamily, ThemeFamilyContent, default_icon_theme,
 17    read_icon_theme, read_user_theme, refine_theme_family,
 18};
 19
 20/// The metadata for a theme.
 21#[derive(Debug, Clone)]
 22pub struct ThemeMeta {
 23    /// The name of the theme.
 24    pub name: SharedString,
 25    /// The appearance of the theme.
 26    pub appearance: Appearance,
 27}
 28
 29/// An error indicating that the theme with the given name was not found.
 30#[derive(Debug, Error, Clone)]
 31#[error("theme not found: {0}")]
 32pub struct ThemeNotFoundError(pub SharedString);
 33
 34/// An error indicating that the icon theme with the given name was not found.
 35#[derive(Debug, Error, Clone)]
 36#[error("icon theme not found: {0}")]
 37pub struct IconThemeNotFoundError(pub SharedString);
 38
 39/// The global [`ThemeRegistry`].
 40///
 41/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when
 42/// inserting the [`ThemeRegistry`] into the context as a global.
 43///
 44/// This should not be exposed outside of this module.
 45#[derive(Default, Deref, DerefMut)]
 46struct GlobalThemeRegistry(Arc<ThemeRegistry>);
 47
 48impl Global for GlobalThemeRegistry {}
 49
 50struct ThemeRegistryState {
 51    themes: HashMap<SharedString, Arc<Theme>>,
 52    icon_themes: HashMap<SharedString, Arc<IconTheme>>,
 53    /// Whether the extensions have been loaded yet.
 54    extensions_loaded: bool,
 55}
 56
 57/// The registry for themes.
 58pub struct ThemeRegistry {
 59    state: RwLock<ThemeRegistryState>,
 60    assets: Box<dyn AssetSource>,
 61}
 62
 63impl ThemeRegistry {
 64    /// Returns the global [`ThemeRegistry`].
 65    pub fn global(cx: &App) -> Arc<Self> {
 66        cx.global::<GlobalThemeRegistry>().0.clone()
 67    }
 68
 69    /// Returns the global [`ThemeRegistry`].
 70    ///
 71    /// Inserts a default [`ThemeRegistry`] if one does not yet exist.
 72    pub fn default_global(cx: &mut App) -> Arc<Self> {
 73        cx.default_global::<GlobalThemeRegistry>().0.clone()
 74    }
 75
 76    /// Returns the global [`ThemeRegistry`] if it exists.
 77    pub fn try_global(cx: &mut App) -> Option<Arc<Self>> {
 78        cx.try_global::<GlobalThemeRegistry>().map(|t| t.0.clone())
 79    }
 80
 81    /// Sets the global [`ThemeRegistry`].
 82    pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut App) {
 83        cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
 84    }
 85
 86    /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`].
 87    pub fn new(assets: Box<dyn AssetSource>) -> Self {
 88        let registry = Self {
 89            state: RwLock::new(ThemeRegistryState {
 90                themes: HashMap::default(),
 91                icon_themes: HashMap::default(),
 92                extensions_loaded: false,
 93            }),
 94            assets,
 95        };
 96
 97        // We're loading the Zed default theme, as we need a theme to be loaded
 98        // for tests.
 99        registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]);
100
101        let default_icon_theme = crate::default_icon_theme();
102        registry
103            .state
104            .write()
105            .icon_themes
106            .insert(default_icon_theme.name.clone(), default_icon_theme);
107
108        registry
109    }
110
111    /// Returns whether the extensions have been loaded.
112    pub fn extensions_loaded(&self) -> bool {
113        self.state.read().extensions_loaded
114    }
115
116    /// Sets the flag indicating that the extensions have loaded.
117    pub fn set_extensions_loaded(&self) {
118        self.state.write().extensions_loaded = true;
119    }
120
121    fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
122        for family in families.into_iter() {
123            self.insert_themes(family.themes);
124        }
125    }
126
127    fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
128        let mut state = self.state.write();
129        for theme in themes.into_iter() {
130            state.themes.insert(theme.name.clone(), Arc::new(theme));
131        }
132    }
133
134    #[allow(unused)]
135    fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
136        for family in families.into_iter() {
137            let refined_family = refine_theme_family(family);
138
139            self.insert_themes(refined_family.themes);
140        }
141    }
142
143    /// Removes the themes with the given names from the registry.
144    pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
145        self.state
146            .write()
147            .themes
148            .retain(|name, _| !themes_to_remove.contains(name))
149    }
150
151    /// Removes all themes from the registry.
152    pub fn clear(&self) {
153        self.state.write().themes.clear();
154    }
155
156    /// Returns the names of all themes in the registry.
157    pub fn list_names(&self) -> Vec<SharedString> {
158        let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
159        names.sort();
160        names
161    }
162
163    /// Returns the metadata of all themes in the registry.
164    pub fn list(&self) -> Vec<ThemeMeta> {
165        self.state
166            .read()
167            .themes
168            .values()
169            .map(|theme| ThemeMeta {
170                name: theme.name.clone(),
171                appearance: theme.appearance(),
172            })
173            .collect()
174    }
175
176    /// Returns the theme with the given name.
177    pub fn get(&self, name: &str) -> Result<Arc<Theme>, ThemeNotFoundError> {
178        self.state
179            .read()
180            .themes
181            .get(name)
182            .ok_or_else(|| ThemeNotFoundError(name.to_string().into()))
183            .cloned()
184    }
185
186    /// Loads the themes bundled with the Zed binary and adds them to the registry.
187    pub fn load_bundled_themes(&self) {
188        let theme_paths = self
189            .assets
190            .list("themes/")
191            .expect("failed to list theme assets")
192            .into_iter()
193            .filter(|path| path.ends_with(".json"));
194
195        for path in theme_paths {
196            let Some(theme) = self.assets.load(&path).log_err().flatten() else {
197                continue;
198            };
199
200            let Some(theme_family) = serde_json::from_slice(&theme)
201                .with_context(|| format!("failed to parse theme at path \"{path}\""))
202                .log_err()
203            else {
204                continue;
205            };
206
207            self.insert_user_theme_families([theme_family]);
208        }
209    }
210
211    /// Loads the user themes from the specified directory and adds them to the registry.
212    pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
213        let mut theme_paths = fs
214            .read_dir(themes_path)
215            .await
216            .with_context(|| format!("reading themes from {themes_path:?}"))?;
217
218        while let Some(theme_path) = theme_paths.next().await {
219            let Some(theme_path) = theme_path.log_err() else {
220                continue;
221            };
222
223            self.load_user_theme(&theme_path, fs.clone())
224                .await
225                .log_err();
226        }
227
228        Ok(())
229    }
230
231    /// Loads the user theme from the specified path and adds it to the registry.
232    pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
233        let theme = read_user_theme(theme_path, fs).await?;
234
235        self.insert_user_theme_families([theme]);
236
237        Ok(())
238    }
239
240    /// Returns the default icon theme.
241    pub fn default_icon_theme(&self) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
242        self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
243    }
244
245    /// Returns the metadata of all icon themes in the registry.
246    pub fn list_icon_themes(&self) -> Vec<ThemeMeta> {
247        self.state
248            .read()
249            .icon_themes
250            .values()
251            .map(|theme| ThemeMeta {
252                name: theme.name.clone(),
253                appearance: theme.appearance,
254            })
255            .collect()
256    }
257
258    /// Returns the icon theme with the specified name.
259    pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
260        self.state
261            .read()
262            .icon_themes
263            .get(name)
264            .ok_or_else(|| IconThemeNotFoundError(name.to_string().into()))
265            .cloned()
266    }
267
268    /// Removes the icon themes with the given names from the registry.
269    pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) {
270        self.state
271            .write()
272            .icon_themes
273            .retain(|name, _| !icon_themes_to_remove.contains(name))
274    }
275
276    /// Loads the icon theme from the specified path and adds it to the registry.
277    ///
278    /// The `icons_root_dir` parameter indicates the root directory from which
279    /// the relative paths to icons in the theme should be resolved against.
280    pub async fn load_icon_theme(
281        &self,
282        icon_theme_path: &Path,
283        icons_root_dir: &Path,
284        fs: Arc<dyn Fs>,
285    ) -> Result<()> {
286        let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
287
288        let resolve_icon_path = |path: SharedString| {
289            icons_root_dir
290                .join(path.as_ref())
291                .to_string_lossy()
292                .to_string()
293                .into()
294        };
295
296        let default_icon_theme = default_icon_theme();
297
298        let mut state = self.state.write();
299        for icon_theme in icon_theme_family.themes {
300            let mut file_stems = default_icon_theme.file_stems.clone();
301            file_stems.extend(icon_theme.file_stems);
302
303            let mut file_suffixes = default_icon_theme.file_suffixes.clone();
304            file_suffixes.extend(icon_theme.file_suffixes);
305
306            let mut named_directory_icons = default_icon_theme.named_directory_icons.clone();
307            named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map(
308                |(key, value)| {
309                    (
310                        key,
311                        DirectoryIcons {
312                            collapsed: value.collapsed.map(resolve_icon_path),
313                            expanded: value.expanded.map(resolve_icon_path),
314                        },
315                    )
316                },
317            ));
318
319            let icon_theme = IconTheme {
320                id: uuid::Uuid::new_v4().to_string(),
321                name: icon_theme.name.into(),
322                appearance: match icon_theme.appearance {
323                    AppearanceContent::Light => Appearance::Light,
324                    AppearanceContent::Dark => Appearance::Dark,
325                },
326                directory_icons: DirectoryIcons {
327                    collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
328                    expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
329                },
330                named_directory_icons,
331                chevron_icons: ChevronIcons {
332                    collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
333                    expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
334                },
335                file_stems,
336                file_suffixes,
337                file_icons: icon_theme
338                    .file_icons
339                    .into_iter()
340                    .map(|(key, icon)| {
341                        (
342                            key,
343                            IconDefinition {
344                                path: resolve_icon_path(icon.path),
345                            },
346                        )
347                    })
348                    .collect(),
349            };
350
351            state
352                .icon_themes
353                .insert(icon_theme.name.clone(), Arc::new(icon_theme));
354        }
355
356        Ok(())
357    }
358}
359
360impl Default for ThemeRegistry {
361    fn default() -> Self {
362        Self::new(Box::new(()))
363    }
364}