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 /// Sets the global [`ThemeRegistry`].
77 pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut App) {
78 cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
79 }
80
81 /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`].
82 pub fn new(assets: Box<dyn AssetSource>) -> Self {
83 let registry = Self {
84 state: RwLock::new(ThemeRegistryState {
85 themes: HashMap::default(),
86 icon_themes: HashMap::default(),
87 extensions_loaded: false,
88 }),
89 assets,
90 };
91
92 // We're loading the Zed default theme, as we need a theme to be loaded
93 // for tests.
94 registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]);
95
96 let default_icon_theme = crate::default_icon_theme();
97 registry
98 .state
99 .write()
100 .icon_themes
101 .insert(default_icon_theme.name.clone(), default_icon_theme);
102
103 registry
104 }
105
106 /// Returns whether the extensions have been loaded.
107 pub fn extensions_loaded(&self) -> bool {
108 self.state.read().extensions_loaded
109 }
110
111 /// Sets the flag indicating that the extensions have loaded.
112 pub fn set_extensions_loaded(&self) {
113 self.state.write().extensions_loaded = true;
114 }
115
116 fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
117 for family in families.into_iter() {
118 self.insert_themes(family.themes);
119 }
120 }
121
122 fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
123 let mut state = self.state.write();
124 for theme in themes.into_iter() {
125 state.themes.insert(theme.name.clone(), Arc::new(theme));
126 }
127 }
128
129 #[allow(unused)]
130 fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
131 for family in families.into_iter() {
132 let refined_family = refine_theme_family(family);
133
134 self.insert_themes(refined_family.themes);
135 }
136 }
137
138 /// Removes the themes with the given names from the registry.
139 pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
140 self.state
141 .write()
142 .themes
143 .retain(|name, _| !themes_to_remove.contains(name))
144 }
145
146 /// Removes all themes from the registry.
147 pub fn clear(&self) {
148 self.state.write().themes.clear();
149 }
150
151 /// Returns the names of all themes in the registry.
152 pub fn list_names(&self) -> Vec<SharedString> {
153 let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
154 names.sort();
155 names
156 }
157
158 /// Returns the metadata of all themes in the registry.
159 pub fn list(&self) -> Vec<ThemeMeta> {
160 self.state
161 .read()
162 .themes
163 .values()
164 .map(|theme| ThemeMeta {
165 name: theme.name.clone(),
166 appearance: theme.appearance(),
167 })
168 .collect()
169 }
170
171 /// Returns the theme with the given name.
172 pub fn get(&self, name: &str) -> Result<Arc<Theme>, ThemeNotFoundError> {
173 self.state
174 .read()
175 .themes
176 .get(name)
177 .ok_or_else(|| ThemeNotFoundError(name.to_string().into()))
178 .cloned()
179 }
180
181 /// Loads the themes bundled with the Zed binary and adds them to the registry.
182 pub fn load_bundled_themes(&self) {
183 let theme_paths = self
184 .assets
185 .list("themes/")
186 .expect("failed to list theme assets")
187 .into_iter()
188 .filter(|path| path.ends_with(".json"));
189
190 for path in theme_paths {
191 let Some(theme) = self.assets.load(&path).log_err().flatten() else {
192 continue;
193 };
194
195 let Some(theme_family) = serde_json::from_slice(&theme)
196 .with_context(|| format!("failed to parse theme at path \"{path}\""))
197 .log_err()
198 else {
199 continue;
200 };
201
202 self.insert_user_theme_families([theme_family]);
203 }
204 }
205
206 /// Loads the user themes from the specified directory and adds them to the registry.
207 pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
208 let mut theme_paths = fs
209 .read_dir(themes_path)
210 .await
211 .with_context(|| format!("reading themes from {themes_path:?}"))?;
212
213 while let Some(theme_path) = theme_paths.next().await {
214 let Some(theme_path) = theme_path.log_err() else {
215 continue;
216 };
217
218 self.load_user_theme(&theme_path, fs.clone())
219 .await
220 .log_err();
221 }
222
223 Ok(())
224 }
225
226 /// Loads the user theme from the specified path and adds it to the registry.
227 pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
228 let theme = read_user_theme(theme_path, fs).await?;
229
230 self.insert_user_theme_families([theme]);
231
232 Ok(())
233 }
234
235 /// Returns the default icon theme.
236 pub fn default_icon_theme(&self) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
237 self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
238 }
239
240 /// Returns the metadata of all icon themes in the registry.
241 pub fn list_icon_themes(&self) -> Vec<ThemeMeta> {
242 self.state
243 .read()
244 .icon_themes
245 .values()
246 .map(|theme| ThemeMeta {
247 name: theme.name.clone(),
248 appearance: theme.appearance,
249 })
250 .collect()
251 }
252
253 /// Returns the icon theme with the specified name.
254 pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>, IconThemeNotFoundError> {
255 self.state
256 .read()
257 .icon_themes
258 .get(name)
259 .ok_or_else(|| IconThemeNotFoundError(name.to_string().into()))
260 .cloned()
261 }
262
263 /// Removes the icon themes with the given names from the registry.
264 pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) {
265 self.state
266 .write()
267 .icon_themes
268 .retain(|name, _| !icon_themes_to_remove.contains(name))
269 }
270
271 /// Loads the icon theme from the specified path and adds it to the registry.
272 ///
273 /// The `icons_root_dir` parameter indicates the root directory from which
274 /// the relative paths to icons in the theme should be resolved against.
275 pub async fn load_icon_theme(
276 &self,
277 icon_theme_path: &Path,
278 icons_root_dir: &Path,
279 fs: Arc<dyn Fs>,
280 ) -> Result<()> {
281 let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
282
283 let resolve_icon_path = |path: SharedString| {
284 icons_root_dir
285 .join(path.as_ref())
286 .to_string_lossy()
287 .to_string()
288 .into()
289 };
290
291 let default_icon_theme = default_icon_theme();
292
293 let mut state = self.state.write();
294 for icon_theme in icon_theme_family.themes {
295 let mut file_stems = default_icon_theme.file_stems.clone();
296 file_stems.extend(icon_theme.file_stems);
297
298 let mut file_suffixes = default_icon_theme.file_suffixes.clone();
299 file_suffixes.extend(icon_theme.file_suffixes);
300
301 let mut named_directory_icons = default_icon_theme.named_directory_icons.clone();
302 named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map(
303 |(key, value)| {
304 (
305 key,
306 DirectoryIcons {
307 collapsed: value.collapsed.map(resolve_icon_path),
308 expanded: value.expanded.map(resolve_icon_path),
309 },
310 )
311 },
312 ));
313
314 let icon_theme = IconTheme {
315 id: uuid::Uuid::new_v4().to_string(),
316 name: icon_theme.name.into(),
317 appearance: match icon_theme.appearance {
318 AppearanceContent::Light => Appearance::Light,
319 AppearanceContent::Dark => Appearance::Dark,
320 },
321 directory_icons: DirectoryIcons {
322 collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
323 expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
324 },
325 named_directory_icons,
326 chevron_icons: ChevronIcons {
327 collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
328 expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
329 },
330 file_stems,
331 file_suffixes,
332 file_icons: icon_theme
333 .file_icons
334 .into_iter()
335 .map(|(key, icon)| {
336 (
337 key,
338 IconDefinition {
339 path: resolve_icon_path(icon.path),
340 },
341 )
342 })
343 .collect(),
344 };
345
346 state
347 .icon_themes
348 .insert(icon_theme.name.clone(), Arc::new(icon_theme));
349 }
350
351 Ok(())
352 }
353}
354
355impl Default for ThemeRegistry {
356 fn default() -> Self {
357 Self::new(Box::new(()))
358 }
359}