registry.rs

  1use std::sync::Arc;
  2use std::{fmt::Debug, path::Path};
  3
  4use anyhow::{anyhow, Context, Result};
  5use collections::HashMap;
  6use derive_more::{Deref, DerefMut};
  7use fs::Fs;
  8use futures::StreamExt;
  9use gpui::{AppContext, AssetSource, Global, SharedString};
 10use parking_lot::RwLock;
 11use util::ResultExt;
 12
 13use crate::{refine_theme_family, Appearance, Theme, ThemeFamily, ThemeFamilyContent};
 14
 15/// The metadata for a theme.
 16#[derive(Debug, Clone)]
 17pub struct ThemeMeta {
 18    /// The name of the theme.
 19    pub name: SharedString,
 20    /// The appearance of the theme.
 21    pub appearance: Appearance,
 22}
 23
 24/// The global [`ThemeRegistry`].
 25///
 26/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when
 27/// inserting the [`ThemeRegistry`] into the context as a global.
 28///
 29/// This should not be exposed outside of this module.
 30#[derive(Default, Deref, DerefMut)]
 31struct GlobalThemeRegistry(Arc<ThemeRegistry>);
 32
 33impl Global for GlobalThemeRegistry {}
 34
 35struct ThemeRegistryState {
 36    themes: HashMap<SharedString, Arc<Theme>>,
 37}
 38
 39/// The registry for themes.
 40pub struct ThemeRegistry {
 41    state: RwLock<ThemeRegistryState>,
 42    assets: Box<dyn AssetSource>,
 43}
 44
 45impl ThemeRegistry {
 46    /// Returns the global [`ThemeRegistry`].
 47    pub fn global(cx: &AppContext) -> Arc<Self> {
 48        cx.global::<GlobalThemeRegistry>().0.clone()
 49    }
 50
 51    /// Returns the global [`ThemeRegistry`].
 52    ///
 53    /// Inserts a default [`ThemeRegistry`] if one does not yet exist.
 54    pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
 55        cx.default_global::<GlobalThemeRegistry>().0.clone()
 56    }
 57
 58    /// Sets the global [`ThemeRegistry`].
 59    pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
 60        cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
 61    }
 62
 63    /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`].
 64    pub fn new(assets: Box<dyn AssetSource>) -> Self {
 65        let registry = Self {
 66            state: RwLock::new(ThemeRegistryState {
 67                themes: HashMap::default(),
 68            }),
 69            assets,
 70        };
 71
 72        // We're loading the Zed default theme, as we need a theme to be loaded
 73        // for tests.
 74        registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]);
 75
 76        registry
 77    }
 78
 79    fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
 80        for family in families.into_iter() {
 81            self.insert_themes(family.themes);
 82        }
 83    }
 84
 85    fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
 86        let mut state = self.state.write();
 87        for theme in themes.into_iter() {
 88            state.themes.insert(theme.name.clone(), Arc::new(theme));
 89        }
 90    }
 91
 92    #[allow(unused)]
 93    fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
 94        for family in families.into_iter() {
 95            let refined_family = refine_theme_family(family);
 96
 97            self.insert_themes(refined_family.themes);
 98        }
 99    }
100
101    /// Removes the themes with the given names from the registry.
102    pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
103        self.state
104            .write()
105            .themes
106            .retain(|name, _| !themes_to_remove.contains(name))
107    }
108
109    /// Removes all themes from the registry.
110    pub fn clear(&self) {
111        self.state.write().themes.clear();
112    }
113
114    /// Returns the names of all themes in the registry.
115    pub fn list_names(&self, _staff: bool) -> Vec<SharedString> {
116        let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
117        names.sort();
118        names
119    }
120
121    /// Returns the metadata of all themes in the registry.
122    pub fn list(&self, _staff: bool) -> Vec<ThemeMeta> {
123        self.state
124            .read()
125            .themes
126            .values()
127            .map(|theme| ThemeMeta {
128                name: theme.name.clone(),
129                appearance: theme.appearance(),
130            })
131            .collect()
132    }
133
134    /// Returns the theme with the given name.
135    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
136        self.state
137            .read()
138            .themes
139            .get(name)
140            .ok_or_else(|| anyhow!("theme not found: {}", name))
141            .cloned()
142    }
143
144    /// Loads the themes bundled with the Zed binary and adds them to the registry.
145    pub fn load_bundled_themes(&self) {
146        let theme_paths = self
147            .assets
148            .list("themes/")
149            .expect("failed to list theme assets")
150            .into_iter()
151            .filter(|path| path.ends_with(".json"));
152
153        for path in theme_paths {
154            let Some(theme) = self.assets.load(&path).log_err().flatten() else {
155                continue;
156            };
157
158            let Some(theme_family) = serde_json::from_slice(&theme)
159                .with_context(|| format!("failed to parse theme at path \"{path}\""))
160                .log_err()
161            else {
162                continue;
163            };
164
165            self.insert_user_theme_families([theme_family]);
166        }
167    }
168
169    /// Loads the user themes from the specified directory and adds them to the registry.
170    pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
171        let mut theme_paths = fs
172            .read_dir(themes_path)
173            .await
174            .with_context(|| format!("reading themes from {themes_path:?}"))?;
175
176        while let Some(theme_path) = theme_paths.next().await {
177            let Some(theme_path) = theme_path.log_err() else {
178                continue;
179            };
180
181            self.load_user_theme(&theme_path, fs.clone())
182                .await
183                .log_err();
184        }
185
186        Ok(())
187    }
188
189    /// Asynchronously reads the user theme from the specified path.
190    pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
191        let reader = fs.open_sync(theme_path).await?;
192        let theme_family: ThemeFamilyContent = serde_json_lenient::from_reader(reader)?;
193
194        for theme in &theme_family.themes {
195            if theme
196                .style
197                .colors
198                .deprecated_scrollbar_thumb_background
199                .is_some()
200            {
201                log::warn!(
202                    r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
203                    theme_name = theme.name
204                )
205            }
206        }
207
208        Ok(theme_family)
209    }
210
211    /// Loads the user theme from the specified path and adds it to the registry.
212    pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
213        let theme = Self::read_user_theme(theme_path, fs).await?;
214
215        self.insert_user_theme_families([theme]);
216
217        Ok(())
218    }
219}
220
221impl Default for ThemeRegistry {
222    fn default() -> Self {
223        Self::new(Box::new(()))
224    }
225}