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