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}