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