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, SharedString};
10use parking_lot::RwLock;
11use util::ResultExt;
12
13use crate::{
14 read_user_theme, refine_theme_family, Appearance, IconTheme, Theme, ThemeFamily,
15 ThemeFamilyContent,
16};
17
18/// The metadata for a theme.
19#[derive(Debug, Clone)]
20pub struct ThemeMeta {
21 /// The name of the theme.
22 pub name: SharedString,
23 /// The appearance of the theme.
24 pub appearance: Appearance,
25}
26
27/// The global [`ThemeRegistry`].
28///
29/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when
30/// inserting the [`ThemeRegistry`] into the context as a global.
31///
32/// This should not be exposed outside of this module.
33#[derive(Default, Deref, DerefMut)]
34struct GlobalThemeRegistry(Arc<ThemeRegistry>);
35
36impl Global for GlobalThemeRegistry {}
37
38struct ThemeRegistryState {
39 themes: HashMap<SharedString, Arc<Theme>>,
40 icon_themes: HashMap<SharedString, Arc<IconTheme>>,
41}
42
43/// The registry for themes.
44pub struct ThemeRegistry {
45 state: RwLock<ThemeRegistryState>,
46 assets: Box<dyn AssetSource>,
47}
48
49impl ThemeRegistry {
50 /// Returns the global [`ThemeRegistry`].
51 pub fn global(cx: &AppContext) -> Arc<Self> {
52 cx.global::<GlobalThemeRegistry>().0.clone()
53 }
54
55 /// Returns the global [`ThemeRegistry`].
56 ///
57 /// Inserts a default [`ThemeRegistry`] if one does not yet exist.
58 pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
59 cx.default_global::<GlobalThemeRegistry>().0.clone()
60 }
61
62 /// Sets the global [`ThemeRegistry`].
63 pub(crate) fn set_global(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
64 cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
65 }
66
67 /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`].
68 pub fn new(assets: Box<dyn AssetSource>) -> Self {
69 let registry = Self {
70 state: RwLock::new(ThemeRegistryState {
71 themes: HashMap::default(),
72 icon_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 let default_icon_theme = crate::default_icon_theme();
82 registry.state.write().icon_themes.insert(
83 default_icon_theme.id.clone().into(),
84 Arc::new(default_icon_theme),
85 );
86
87 registry
88 }
89
90 fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
91 for family in families.into_iter() {
92 self.insert_themes(family.themes);
93 }
94 }
95
96 fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
97 let mut state = self.state.write();
98 for theme in themes.into_iter() {
99 state.themes.insert(theme.name.clone(), Arc::new(theme));
100 }
101 }
102
103 #[allow(unused)]
104 fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
105 for family in families.into_iter() {
106 let refined_family = refine_theme_family(family);
107
108 self.insert_themes(refined_family.themes);
109 }
110 }
111
112 /// Removes the themes with the given names from the registry.
113 pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
114 self.state
115 .write()
116 .themes
117 .retain(|name, _| !themes_to_remove.contains(name))
118 }
119
120 /// Removes all themes from the registry.
121 pub fn clear(&self) {
122 self.state.write().themes.clear();
123 }
124
125 /// Returns the names of all themes in the registry.
126 pub fn list_names(&self) -> Vec<SharedString> {
127 let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
128 names.sort();
129 names
130 }
131
132 /// Returns the metadata of all themes in the registry.
133 pub fn list(&self) -> Vec<ThemeMeta> {
134 self.state
135 .read()
136 .themes
137 .values()
138 .map(|theme| ThemeMeta {
139 name: theme.name.clone(),
140 appearance: theme.appearance(),
141 })
142 .collect()
143 }
144
145 /// Returns the theme with the given name.
146 pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
147 self.state
148 .read()
149 .themes
150 .get(name)
151 .ok_or_else(|| anyhow!("theme not found: {}", name))
152 .cloned()
153 }
154
155 /// Loads the themes bundled with the Zed binary and adds them to the registry.
156 pub fn load_bundled_themes(&self) {
157 let theme_paths = self
158 .assets
159 .list("themes/")
160 .expect("failed to list theme assets")
161 .into_iter()
162 .filter(|path| path.ends_with(".json"));
163
164 for path in theme_paths {
165 let Some(theme) = self.assets.load(&path).log_err().flatten() else {
166 continue;
167 };
168
169 let Some(theme_family) = serde_json::from_slice(&theme)
170 .with_context(|| format!("failed to parse theme at path \"{path}\""))
171 .log_err()
172 else {
173 continue;
174 };
175
176 self.insert_user_theme_families([theme_family]);
177 }
178 }
179
180 /// Loads the user themes from the specified directory and adds them to the registry.
181 pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
182 let mut theme_paths = fs
183 .read_dir(themes_path)
184 .await
185 .with_context(|| format!("reading themes from {themes_path:?}"))?;
186
187 while let Some(theme_path) = theme_paths.next().await {
188 let Some(theme_path) = theme_path.log_err() else {
189 continue;
190 };
191
192 self.load_user_theme(&theme_path, fs.clone())
193 .await
194 .log_err();
195 }
196
197 Ok(())
198 }
199
200 /// Loads the user theme from the specified path and adds it to the registry.
201 pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
202 let theme = read_user_theme(theme_path, fs).await?;
203
204 self.insert_user_theme_families([theme]);
205
206 Ok(())
207 }
208
209 /// Returns the icon theme with the specified name.
210 pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>> {
211 self.state
212 .read()
213 .icon_themes
214 .get(name)
215 .ok_or_else(|| anyhow!("icon theme not found: {name}"))
216 .cloned()
217 }
218}
219
220impl Default for ThemeRegistry {
221 fn default() -> Self {
222 Self::new(Box::new(()))
223 }
224}