gpui_component/theme/
registry.rs1use crate::{Theme, ThemeColor, ThemeConfig, ThemeMode, ThemeSet, highlighter::HighlightTheme};
2#[allow(unused)]
3use anyhow::Result;
4use gpui::{App, Global, SharedString};
5use std::{
6 collections::HashMap,
7 path::PathBuf,
8 rc::Rc,
9 sync::{Arc, LazyLock},
10};
11
12const DEFAULT_THEME: &str = include_str!("./default-theme.json");
13pub(crate) static DEFAULT_THEME_COLORS: LazyLock<
14 HashMap<ThemeMode, (Arc<ThemeColor>, Arc<HighlightTheme>)>,
15> = LazyLock::new(|| {
16 let mut colors = HashMap::new();
17
18 let themes: Vec<ThemeConfig> = serde_json::from_str::<ThemeSet>(DEFAULT_THEME)
19 .expect("Failed to parse themes/default.json")
20 .themes;
21
22 for theme in themes {
23 let mut theme_color = ThemeColor::default();
24 theme_color.apply_config(&theme, &ThemeColor::default());
25
26 let highlight_theme = HighlightTheme {
27 name: theme.name.to_string(),
28 appearance: theme.mode,
29 style: theme.highlight.unwrap_or_default(),
30 };
31
32 colors.insert(
33 theme.mode,
34 (Arc::new(theme_color), Arc::new(highlight_theme)),
35 );
36 }
37
38 colors
39});
40
41pub(super) fn init(cx: &mut App) {
42 cx.set_global(ThemeRegistry::default());
43 ThemeRegistry::global_mut(cx).init_default_themes();
44
45 cx.observe_global::<ThemeRegistry>(|cx| {
47 let mode = Theme::global(cx).mode;
48 let light_theme = Theme::global(cx).light_theme.name.clone();
49 let dark_theme = Theme::global(cx).dark_theme.name.clone();
50
51 if let Some(theme) = ThemeRegistry::global(cx)
52 .themes()
53 .get(&light_theme)
54 .cloned()
55 {
56 Theme::global_mut(cx).light_theme = theme;
57 }
58 if let Some(theme) = ThemeRegistry::global(cx).themes().get(&dark_theme).cloned() {
59 Theme::global_mut(cx).dark_theme = theme;
60 }
61
62 let theme_name = if mode.is_dark() {
63 dark_theme
64 } else {
65 light_theme
66 };
67
68 tracing::info!("Reload active theme: {:?}...", theme_name);
69 Theme::change(mode, None, cx);
70 cx.refresh_windows();
71 })
72 .detach();
73}
74
75#[derive(Default, Debug)]
76pub struct ThemeRegistry {
77 themes_dir: PathBuf,
78 default_themes: HashMap<ThemeMode, Rc<ThemeConfig>>,
79 themes: HashMap<SharedString, Rc<ThemeConfig>>,
80 has_custom_themes: bool,
81}
82
83impl Global for ThemeRegistry {}
84
85impl ThemeRegistry {
86 pub fn global(cx: &App) -> &Self {
87 cx.global::<Self>()
88 }
89
90 pub fn global_mut(cx: &mut App) -> &mut Self {
91 cx.global_mut::<Self>()
92 }
93
94 #[cfg(not(target_family = "wasm"))]
98 pub fn watch_dir<F>(themes_dir: PathBuf, cx: &mut App, on_load: F) -> Result<()>
99 where
100 F: Fn(&mut App) + 'static,
101 {
102 Self::global_mut(cx).themes_dir = themes_dir.clone();
103
104 cx.spawn(async move |cx| {
106 _ = cx.update(|cx| {
107 if let Err(err) = Self::_watch_themes_dir(themes_dir, cx) {
108 tracing::error!("Failed to watch themes directory: {}", err);
109 }
110
111 Self::reload_themes(cx);
112 on_load(cx);
113 });
114 })
115 .detach();
116
117 Ok(())
118 }
119
120 pub fn themes(&self) -> &HashMap<SharedString, Rc<ThemeConfig>> {
122 &self.themes
123 }
124
125 pub fn sorted_themes(&self) -> Vec<&Rc<ThemeConfig>> {
127 let mut themes = self.themes.values().collect::<Vec<_>>();
128 themes.sort_by(|a, b| {
130 b.is_default
131 .cmp(&a.is_default)
132 .then(a.mode.cmp(&b.mode))
133 .then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
134 });
135 themes
136 }
137
138 pub fn default_themes(&self) -> &HashMap<ThemeMode, Rc<ThemeConfig>> {
140 &self.default_themes
141 }
142
143 pub fn default_light_theme(&self) -> &Rc<ThemeConfig> {
144 &self.default_themes[&ThemeMode::Light]
145 }
146
147 pub fn default_dark_theme(&self) -> &Rc<ThemeConfig> {
148 &self.default_themes[&ThemeMode::Dark]
149 }
150
151 pub fn load_themes_from_str(&mut self, content: &str) -> anyhow::Result<()> {
152 let theme_set = serde_json::from_str::<ThemeSet>(content)?;
153 for theme in theme_set.themes {
154 if !self.themes.contains_key(&theme.name) {
155 let theme_name = theme.name.clone();
156 self.themes.insert(theme_name, Rc::new(theme));
157 self.has_custom_themes = true;
158 }
159 }
160 Ok(())
161 }
162
163 fn init_default_themes(&mut self) {
164 let default_themes: Vec<ThemeConfig> = serde_json::from_str::<ThemeSet>(DEFAULT_THEME)
165 .expect("failed to parse default theme.")
166 .themes;
167 for theme in default_themes.into_iter() {
168 if theme.mode.is_dark() {
169 self.default_themes.insert(ThemeMode::Dark, Rc::new(theme));
170 } else {
171 self.default_themes.insert(ThemeMode::Light, Rc::new(theme));
172 }
173 }
174 self.themes_dir = PathBuf::from("./themes");
175 self.themes = self
176 .default_themes
177 .values()
178 .map(|theme| {
179 let name = theme.name.clone();
180 (name, Rc::clone(theme))
181 })
182 .collect();
183 }
184
185 #[cfg(not(target_family = "wasm"))]
186 fn _watch_themes_dir(themes_dir: PathBuf, cx: &mut App) -> anyhow::Result<()> {
187 if !themes_dir.exists() {
188 std::fs::create_dir_all(&themes_dir)?;
189 }
190
191 let (tx, rx) = smol::channel::bounded(100);
192 let mut watcher =
193 notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
194 if let Ok(event) = &res {
195 match event.kind {
196 notify::EventKind::Create(_)
197 | notify::EventKind::Modify(_)
198 | notify::EventKind::Remove(_) => {
199 if let Err(err) = tx.send_blocking(res) {
200 tracing::error!("Failed to send theme event: {:?}", err);
201 }
202 }
203 _ => {}
204 }
205 }
206 })?;
207
208 cx.spawn(async move |cx| {
209 use notify::Watcher as _;
210
211 if let Err(err) = watcher.watch(&themes_dir, notify::RecursiveMode::Recursive) {
212 tracing::error!("Failed to watch themes directory: {:?}", err);
213 }
214
215 while (rx.recv().await).is_ok() {
216 tracing::info!("Reloading themes...");
217 _ = cx.update(Self::reload_themes);
218 }
219 })
220 .detach();
221
222 Ok(())
223 }
224
225 #[cfg(not(target_family = "wasm"))]
226 fn reload_themes(cx: &mut App) {
227 let registry = Self::global_mut(cx);
228 match registry.reload() {
229 Ok(_) => {
230 tracing::info!("Themes reloaded successfully.");
231 }
232 Err(e) => tracing::error!("Failed to reload themes: {:?}", e),
233 }
234 }
235
236 #[cfg(not(target_family = "wasm"))]
237 fn reload(&mut self) -> Result<()> {
239 let mut themes = vec![];
240
241 if self.themes_dir.exists() {
242 for entry in std::fs::read_dir(&self.themes_dir)? {
243 let entry = entry?;
244 let path = entry.path();
245 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") {
246 let file_content = std::fs::read_to_string(path.clone())?;
247
248 match serde_json::from_str::<ThemeSet>(&file_content) {
249 Ok(theme_set) => {
250 themes.extend(theme_set.themes);
251 }
252 Err(e) => {
253 tracing::error!(
254 "ignored invalid theme file: {}, {}",
255 path.display(),
256 e
257 );
258 }
259 }
260 }
261 }
262 }
263
264 self.themes.clear();
265 for theme in self.default_themes.values() {
266 self.themes
267 .insert(theme.name.clone(), Rc::new((**theme).clone()));
268 }
269
270 for theme in themes.iter() {
271 if self.themes.contains_key(&theme.name) {
272 continue;
273 }
274
275 if theme.is_default {
276 self.default_themes
277 .insert(theme.mode, Rc::new(theme.clone()));
278 }
279
280 self.has_custom_themes = true;
281 self.themes
282 .insert(theme.name.clone(), Rc::new(theme.clone()));
283 }
284
285 Ok(())
286 }
287}