1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::Arc;
4
5use anyhow::{anyhow, Context, Result};
6use derive_more::{Deref, DerefMut};
7use fs::Fs;
8use futures::StreamExt;
9use gpui::{AppContext, AssetSource, HighlightStyle, SharedString};
10use parking_lot::RwLock;
11use refineable::Refineable;
12use util::ResultExt;
13
14use crate::{
15 try_parse_color, Appearance, AppearanceContent, PlayerColor, PlayerColors, StatusColors,
16 SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
17 ThemeStyles,
18};
19
20#[derive(Debug, Clone)]
21pub struct ThemeMeta {
22 pub name: SharedString,
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
35/// Initializes the theme registry.
36pub fn init(assets: Box<dyn AssetSource>, cx: &mut AppContext) {
37 cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets))));
38}
39
40struct ThemeRegistryState {
41 themes: HashMap<SharedString, Arc<Theme>>,
42}
43
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 pub fn new(assets: Box<dyn AssetSource>) -> Self {
63 let registry = Self {
64 state: RwLock::new(ThemeRegistryState {
65 themes: HashMap::new(),
66 }),
67 assets,
68 };
69
70 // We're loading our new versions of the One themes by default, as
71 // we need them to be loaded for tests.
72 //
73 // These themes will get overwritten when `load_user_themes` is called
74 // when Zed starts, so the One variants used will be the ones ported from Zed1.
75 registry.insert_theme_families([crate::one_themes::one_family()]);
76
77 registry
78 }
79
80 fn insert_theme_families(&self, families: impl IntoIterator<Item = ThemeFamily>) {
81 for family in families.into_iter() {
82 self.insert_themes(family.themes);
83 }
84 }
85
86 fn insert_themes(&self, themes: impl IntoIterator<Item = Theme>) {
87 let mut state = self.state.write();
88 for theme in themes.into_iter() {
89 state.themes.insert(theme.name.clone(), Arc::new(theme));
90 }
91 }
92
93 #[allow(unused)]
94 fn insert_user_theme_families(&self, families: impl IntoIterator<Item = ThemeFamilyContent>) {
95 for family in families.into_iter() {
96 self.insert_user_themes(family.themes);
97 }
98 }
99
100 pub fn insert_user_themes(&self, themes: impl IntoIterator<Item = ThemeContent>) {
101 self.insert_themes(themes.into_iter().map(|user_theme| {
102 let mut theme_colors = match user_theme.appearance {
103 AppearanceContent::Light => ThemeColors::light(),
104 AppearanceContent::Dark => ThemeColors::dark(),
105 };
106 theme_colors.refine(&user_theme.style.theme_colors_refinement());
107
108 let mut status_colors = match user_theme.appearance {
109 AppearanceContent::Light => StatusColors::light(),
110 AppearanceContent::Dark => StatusColors::dark(),
111 };
112 status_colors.refine(&user_theme.style.status_colors_refinement());
113
114 let mut player_colors = match user_theme.appearance {
115 AppearanceContent::Light => PlayerColors::light(),
116 AppearanceContent::Dark => PlayerColors::dark(),
117 };
118 if !user_theme.style.players.is_empty() {
119 player_colors = PlayerColors(
120 user_theme
121 .style
122 .players
123 .into_iter()
124 .map(|player| PlayerColor {
125 cursor: player
126 .cursor
127 .as_ref()
128 .and_then(|color| try_parse_color(&color).ok())
129 .unwrap_or_default(),
130 background: player
131 .background
132 .as_ref()
133 .and_then(|color| try_parse_color(&color).ok())
134 .unwrap_or_default(),
135 selection: player
136 .selection
137 .as_ref()
138 .and_then(|color| try_parse_color(&color).ok())
139 .unwrap_or_default(),
140 })
141 .collect(),
142 );
143 }
144
145 let mut syntax_colors = match user_theme.appearance {
146 AppearanceContent::Light => SyntaxTheme::light(),
147 AppearanceContent::Dark => SyntaxTheme::dark(),
148 };
149 if !user_theme.style.syntax.is_empty() {
150 syntax_colors.highlights = user_theme
151 .style
152 .syntax
153 .iter()
154 .map(|(syntax_token, highlight)| {
155 (
156 syntax_token.clone(),
157 HighlightStyle {
158 color: highlight
159 .color
160 .as_ref()
161 .and_then(|color| try_parse_color(&color).ok()),
162 font_style: highlight.font_style.map(Into::into),
163 font_weight: highlight.font_weight.map(Into::into),
164 ..Default::default()
165 },
166 )
167 })
168 .collect::<Vec<_>>();
169 }
170
171 Theme {
172 id: uuid::Uuid::new_v4().to_string(),
173 name: user_theme.name.into(),
174 appearance: match user_theme.appearance {
175 AppearanceContent::Light => Appearance::Light,
176 AppearanceContent::Dark => Appearance::Dark,
177 },
178 styles: ThemeStyles {
179 system: SystemColors::default(),
180 colors: theme_colors,
181 status: status_colors,
182 player: player_colors,
183 syntax: Arc::new(syntax_colors),
184 accents: Vec::new(),
185 },
186 }
187 }));
188 }
189
190 pub fn clear(&mut self) {
191 self.state.write().themes.clear();
192 }
193
194 pub fn list_names(&self, _staff: bool) -> Vec<SharedString> {
195 self.state.read().themes.keys().cloned().collect()
196 }
197
198 pub fn list(&self, _staff: bool) -> Vec<ThemeMeta> {
199 self.state
200 .read()
201 .themes
202 .values()
203 .map(|theme| ThemeMeta {
204 name: theme.name.clone(),
205 appearance: theme.appearance(),
206 })
207 .collect()
208 }
209
210 pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
211 self.state
212 .read()
213 .themes
214 .get(name)
215 .ok_or_else(|| anyhow!("theme not found: {}", name))
216 .cloned()
217 }
218
219 /// Loads the themes bundled with the Zed binary and adds them to the registry.
220 pub fn load_bundled_themes(&self) {
221 let theme_paths = self
222 .assets
223 .list("themes/")
224 .expect("failed to list theme assets")
225 .into_iter()
226 .filter(|path| path.ends_with(".json"));
227
228 for path in theme_paths {
229 let Some(theme) = self.assets.load(&path).log_err() else {
230 continue;
231 };
232
233 let Some(theme_family) = serde_json::from_slice(&theme)
234 .with_context(|| format!("failed to parse theme at path \"{path}\""))
235 .log_err()
236 else {
237 continue;
238 };
239
240 self.insert_user_theme_families([theme_family]);
241 }
242 }
243
244 /// Loads the user themes from the specified directory and adds them to the registry.
245 pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
246 let mut theme_paths = fs
247 .read_dir(themes_path)
248 .await
249 .with_context(|| format!("reading themes from {themes_path:?}"))?;
250
251 while let Some(theme_path) = theme_paths.next().await {
252 let Some(theme_path) = theme_path.log_err() else {
253 continue;
254 };
255
256 let Some(reader) = fs.open_sync(&theme_path).await.log_err() else {
257 continue;
258 };
259
260 let Some(theme) = serde_json::from_reader(reader).log_err() else {
261 continue;
262 };
263
264 self.insert_user_theme_families([theme]);
265 }
266
267 Ok(())
268 }
269}
270
271impl Default for ThemeRegistry {
272 fn default() -> Self {
273 Self::new(Box::new(()))
274 }
275}