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, HighlightStyle, SharedString};
 10use parking_lot::RwLock;
 11use refineable::Refineable;
 12use util::ResultExt;
 13
 14use crate::{
 15    try_parse_color, AccentColors, Appearance, AppearanceContent, PlayerColors, StatusColors,
 16    SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
 17    ThemeStyles,
 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/// The global [`ThemeRegistry`].
 30///
 31/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when
 32/// inserting the [`ThemeRegistry`] into the context as a global.
 33///
 34/// This should not be exposed outside of this module.
 35#[derive(Default, Deref, DerefMut)]
 36struct GlobalThemeRegistry(Arc<ThemeRegistry>);
 37
 38impl Global for GlobalThemeRegistry {}
 39
 40struct ThemeRegistryState {
 41    themes: HashMap<SharedString, Arc<Theme>>,
 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: &AppContext) -> 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 AppContext) -> 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 AppContext) {
 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            }),
 74            assets,
 75        };
 76
 77        // We're loading the Zed default theme, as we need a theme to be loaded
 78        // for tests.
 79        registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]);
 80
 81        registry
 82    }
 83
 84    fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
 85        for family in families.into_iter() {
 86            self.insert_themes(family.themes);
 87        }
 88    }
 89
 90    fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
 91        let mut state = self.state.write();
 92        for theme in themes.into_iter() {
 93            state.themes.insert(theme.name.clone(), Arc::new(theme));
 94        }
 95    }
 96
 97    #[allow(unused)]
 98    fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
 99        for family in families.into_iter() {
100            self.insert_user_themes(family.themes);
101        }
102    }
103
104    /// Inserts user themes into the registry.
105    pub fn insert_user_themes(&self, themes: impl IntoIterator<Item = ThemeContent>) {
106        self.insert_themes(themes.into_iter().map(|user_theme| {
107            let mut theme_colors = match user_theme.appearance {
108                AppearanceContent::Light => ThemeColors::light(),
109                AppearanceContent::Dark => ThemeColors::dark(),
110            };
111            theme_colors.refine(&user_theme.style.theme_colors_refinement());
112
113            let mut status_colors = match user_theme.appearance {
114                AppearanceContent::Light => StatusColors::light(),
115                AppearanceContent::Dark => StatusColors::dark(),
116            };
117            status_colors.refine(&user_theme.style.status_colors_refinement());
118
119            let mut player_colors = match user_theme.appearance {
120                AppearanceContent::Light => PlayerColors::light(),
121                AppearanceContent::Dark => PlayerColors::dark(),
122            };
123            player_colors.merge(&user_theme.style.players);
124
125            let mut accent_colors = match user_theme.appearance {
126                AppearanceContent::Light => AccentColors::light(),
127                AppearanceContent::Dark => AccentColors::dark(),
128            };
129            accent_colors.merge(&user_theme.style.accents);
130
131            let syntax_highlights = user_theme
132                .style
133                .syntax
134                .iter()
135                .map(|(syntax_token, highlight)| {
136                    (
137                        syntax_token.clone(),
138                        HighlightStyle {
139                            color: highlight
140                                .color
141                                .as_ref()
142                                .and_then(|color| try_parse_color(color).ok()),
143                            background_color: highlight
144                                .background_color
145                                .as_ref()
146                                .and_then(|color| try_parse_color(color).ok()),
147                            font_style: highlight.font_style.map(Into::into),
148                            font_weight: highlight.font_weight.map(Into::into),
149                            ..Default::default()
150                        },
151                    )
152                })
153                .collect::<Vec<_>>();
154            let syntax_theme =
155                SyntaxTheme::merge(Arc::new(SyntaxTheme::default()), syntax_highlights);
156
157            let window_background_appearance = user_theme
158                .style
159                .window_background_appearance
160                .map(Into::into)
161                .unwrap_or_default();
162
163            Theme {
164                id: uuid::Uuid::new_v4().to_string(),
165                name: user_theme.name.into(),
166                appearance: match user_theme.appearance {
167                    AppearanceContent::Light => Appearance::Light,
168                    AppearanceContent::Dark => Appearance::Dark,
169                },
170                styles: ThemeStyles {
171                    system: SystemColors::default(),
172                    window_background_appearance,
173                    accents: accent_colors,
174                    colors: theme_colors,
175                    status: status_colors,
176                    player: player_colors,
177                    syntax: syntax_theme,
178                },
179            }
180        }));
181    }
182
183    /// Removes the themes with the given names from the registry.
184    pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
185        self.state
186            .write()
187            .themes
188            .retain(|name, _| !themes_to_remove.contains(name))
189    }
190
191    /// Removes all themes from the registry.
192    pub fn clear(&self) {
193        self.state.write().themes.clear();
194    }
195
196    /// Returns the names of all themes in the registry.
197    pub fn list_names(&self, _staff: bool) -> Vec<SharedString> {
198        let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
199        names.sort();
200        names
201    }
202
203    /// Returns the metadata of all themes in the registry.
204    pub fn list(&self, _staff: bool) -> Vec<ThemeMeta> {
205        self.state
206            .read()
207            .themes
208            .values()
209            .map(|theme| ThemeMeta {
210                name: theme.name.clone(),
211                appearance: theme.appearance(),
212            })
213            .collect()
214    }
215
216    /// Returns the theme with the given name.
217    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
218        self.state
219            .read()
220            .themes
221            .get(name)
222            .ok_or_else(|| anyhow!("theme not found: {}", name))
223            .cloned()
224    }
225
226    /// Loads the themes bundled with the Zed binary and adds them to the registry.
227    pub fn load_bundled_themes(&self) {
228        let theme_paths = self
229            .assets
230            .list("themes/")
231            .expect("failed to list theme assets")
232            .into_iter()
233            .filter(|path| path.ends_with(".json"));
234
235        for path in theme_paths {
236            let Some(theme) = self.assets.load(&path).log_err().flatten() else {
237                continue;
238            };
239
240            let Some(theme_family) = serde_json::from_slice(&theme)
241                .with_context(|| format!("failed to parse theme at path \"{path}\""))
242                .log_err()
243            else {
244                continue;
245            };
246
247            self.insert_user_theme_families([theme_family]);
248        }
249    }
250
251    /// Loads the user themes from the specified directory and adds them to the registry.
252    pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
253        let mut theme_paths = fs
254            .read_dir(themes_path)
255            .await
256            .with_context(|| format!("reading themes from {themes_path:?}"))?;
257
258        while let Some(theme_path) = theme_paths.next().await {
259            let Some(theme_path) = theme_path.log_err() else {
260                continue;
261            };
262
263            self.load_user_theme(&theme_path, fs.clone())
264                .await
265                .log_err();
266        }
267
268        Ok(())
269    }
270
271    /// Asynchronously reads the user theme from the specified path.
272    pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
273        let reader = fs.open_sync(theme_path).await?;
274        let theme_family: ThemeFamilyContent = serde_json_lenient::from_reader(reader)?;
275
276        for theme in &theme_family.themes {
277            if theme
278                .style
279                .colors
280                .deprecated_scrollbar_thumb_background
281                .is_some()
282            {
283                log::warn!(
284                    r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
285                    theme_name = theme.name
286                )
287            }
288        }
289
290        Ok(theme_family)
291    }
292
293    /// Loads the user theme from the specified path and adds it to the registry.
294    pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
295        let theme = Self::read_user_theme(theme_path, fs).await?;
296
297        self.insert_user_theme_families([theme]);
298
299        Ok(())
300    }
301}
302
303impl Default for ThemeRegistry {
304    fn default() -> Self {
305        Self::new(Box::new(()))
306    }
307}