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}