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