registry.rs

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