registry.rs

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