1use std::sync::Arc;
2use std::{fmt::Debug, path::Path};
3
4use anyhow::{anyhow, bail, Context, Result};
5use async_trait::async_trait;
6use collections::HashMap;
7use derive_more::{Deref, DerefMut};
8use fs::Fs;
9use futures::StreamExt;
10use gpui::{AppContext, AssetSource, Global, SharedString};
11use parking_lot::RwLock;
12use util::ResultExt;
13
14use crate::{
15 read_user_theme, refine_theme_family, Appearance, Theme, ThemeFamily, 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(Deref, DerefMut)]
34struct GlobalThemeRegistry(Arc<dyn ThemeRegistry>);
35
36impl Global for GlobalThemeRegistry {}
37
38/// A registry for themes.
39#[async_trait]
40pub trait ThemeRegistry: Send + Sync + 'static {
41 /// Returns the names of all themes in the registry.
42 fn list_names(&self) -> Vec<SharedString>;
43
44 /// Returns the metadata of all themes in the registry.
45 fn list(&self) -> Vec<ThemeMeta>;
46
47 /// Returns the theme with the given name.
48 fn get(&self, name: &str) -> Result<Arc<Theme>>;
49
50 /// Loads the user theme from the specified path and adds it to the registry.
51 async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()>;
52
53 /// Loads the user themes from the specified directory and adds them to the registry.
54 async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()>;
55
56 /// Removes the themes with the given names from the registry.
57 fn remove_user_themes(&self, themes_to_remove: &[SharedString]);
58}
59
60impl dyn ThemeRegistry {
61 /// Returns the global [`ThemeRegistry`].
62 pub fn global(cx: &AppContext) -> Arc<Self> {
63 cx.global::<GlobalThemeRegistry>().0.clone()
64 }
65
66 /// Returns the global [`ThemeRegistry`].
67 ///
68 /// Inserts a default [`ThemeRegistry`] if one does not yet exist.
69 pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
70 if let Some(registry) = cx.try_global::<GlobalThemeRegistry>() {
71 return registry.0.clone();
72 }
73
74 let registry = Arc::new(RealThemeRegistry::default());
75 cx.set_global(GlobalThemeRegistry(registry.clone()));
76
77 registry
78 }
79}
80
81struct RealThemeRegistryState {
82 themes: HashMap<SharedString, Arc<Theme>>,
83}
84
85/// The registry for themes.
86pub struct RealThemeRegistry {
87 state: RwLock<RealThemeRegistryState>,
88 assets: Box<dyn AssetSource>,
89}
90
91impl RealThemeRegistry {
92 /// Sets the global [`ThemeRegistry`].
93 pub(crate) fn set_global(self: Arc<Self>, cx: &mut AppContext) {
94 cx.set_global(GlobalThemeRegistry(self));
95 }
96
97 /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`].
98 pub fn new(assets: Box<dyn AssetSource>) -> Self {
99 let registry = Self {
100 state: RwLock::new(RealThemeRegistryState {
101 themes: HashMap::default(),
102 }),
103 assets,
104 };
105
106 // We're loading the Zed default theme, as we need a theme to be loaded
107 // for tests.
108 registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]);
109
110 registry
111 }
112
113 fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
114 for family in families.into_iter() {
115 self.insert_themes(family.themes);
116 }
117 }
118
119 fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
120 let mut state = self.state.write();
121 for theme in themes.into_iter() {
122 state.themes.insert(theme.name.clone(), Arc::new(theme));
123 }
124 }
125
126 #[allow(unused)]
127 fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
128 for family in families.into_iter() {
129 let refined_family = refine_theme_family(family);
130
131 self.insert_themes(refined_family.themes);
132 }
133 }
134
135 /// Removes all themes from the registry.
136 pub fn clear(&self) {
137 self.state.write().themes.clear();
138 }
139
140 /// Loads the themes bundled with the Zed binary and adds them to the registry.
141 pub fn load_bundled_themes(&self) {
142 let theme_paths = self
143 .assets
144 .list("themes/")
145 .expect("failed to list theme assets")
146 .into_iter()
147 .filter(|path| path.ends_with(".json"));
148
149 for path in theme_paths {
150 let Some(theme) = self.assets.load(&path).log_err().flatten() else {
151 continue;
152 };
153
154 let Some(theme_family) = serde_json::from_slice(&theme)
155 .with_context(|| format!("failed to parse theme at path \"{path}\""))
156 .log_err()
157 else {
158 continue;
159 };
160
161 self.insert_user_theme_families([theme_family]);
162 }
163 }
164}
165
166impl Default for RealThemeRegistry {
167 fn default() -> Self {
168 Self::new(Box::new(()))
169 }
170}
171
172#[async_trait]
173impl ThemeRegistry for RealThemeRegistry {
174 fn list_names(&self) -> Vec<SharedString> {
175 let mut names = self.state.read().themes.keys().cloned().collect::<Vec<_>>();
176 names.sort();
177 names
178 }
179
180 fn list(&self) -> Vec<ThemeMeta> {
181 self.state
182 .read()
183 .themes
184 .values()
185 .map(|theme| ThemeMeta {
186 name: theme.name.clone(),
187 appearance: theme.appearance(),
188 })
189 .collect()
190 }
191
192 fn get(&self, name: &str) -> Result<Arc<Theme>> {
193 self.state
194 .read()
195 .themes
196 .get(name)
197 .ok_or_else(|| anyhow!("theme not found: {}", name))
198 .cloned()
199 }
200
201 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 async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
210 let mut theme_paths = fs
211 .read_dir(themes_path)
212 .await
213 .with_context(|| format!("reading themes from {themes_path:?}"))?;
214
215 while let Some(theme_path) = theme_paths.next().await {
216 let Some(theme_path) = theme_path.log_err() else {
217 continue;
218 };
219
220 self.load_user_theme(&theme_path, fs.clone())
221 .await
222 .log_err();
223 }
224
225 Ok(())
226 }
227
228 fn remove_user_themes(&self, themes_to_remove: &[SharedString]) {
229 self.state
230 .write()
231 .themes
232 .retain(|name, _| !themes_to_remove.contains(name))
233 }
234}
235
236/// A theme registry that doesn't have any behavior.
237pub struct VoidThemeRegistry;
238
239#[async_trait]
240impl ThemeRegistry for VoidThemeRegistry {
241 fn list_names(&self) -> Vec<SharedString> {
242 Vec::new()
243 }
244
245 fn list(&self) -> Vec<ThemeMeta> {
246 Vec::new()
247 }
248
249 fn get(&self, name: &str) -> Result<Arc<Theme>> {
250 bail!("cannot retrieve theme {name:?} from a void theme registry")
251 }
252
253 async fn load_user_theme(&self, _theme_path: &Path, _fs: Arc<dyn Fs>) -> Result<()> {
254 Ok(())
255 }
256
257 async fn load_user_themes(&self, _themes_path: &Path, _fs: Arc<dyn Fs>) -> Result<()> {
258 Ok(())
259 }
260
261 fn remove_user_themes(&self, _themes_to_remove: &[SharedString]) {}
262}