1use crate::{Theme, ThemeMeta};
2use anyhow::{Context, Result};
3use gpui::{fonts, AssetSource, FontCache};
4use parking_lot::Mutex;
5use serde::Deserialize;
6use serde_json::Value;
7use std::{
8 borrow::Cow,
9 collections::HashMap,
10 sync::{
11 atomic::{AtomicUsize, Ordering::SeqCst},
12 Arc,
13 },
14};
15
16pub struct ThemeRegistry {
17 assets: Box<dyn AssetSource>,
18 themes: Mutex<HashMap<String, Arc<Theme>>>,
19 theme_data: Mutex<HashMap<String, Arc<Value>>>,
20 font_cache: Arc<FontCache>,
21 next_theme_id: AtomicUsize,
22}
23
24impl ThemeRegistry {
25 pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
26 let this = Arc::new(Self {
27 assets: Box::new(source),
28 themes: Default::default(),
29 theme_data: Default::default(),
30 next_theme_id: Default::default(),
31 font_cache,
32 });
33
34 this.themes.lock().insert(
35 settings::EMPTY_THEME_NAME.to_string(),
36 gpui::fonts::with_font_cache(this.font_cache.clone(), || {
37 let mut theme = Theme::default();
38 theme.meta.id = this.next_theme_id.fetch_add(1, SeqCst);
39 theme.meta.name = settings::EMPTY_THEME_NAME.into();
40 Arc::new(theme)
41 }),
42 );
43
44 this
45 }
46
47 pub fn list_names(&self, staff: bool) -> impl Iterator<Item = Cow<str>> + '_ {
48 let mut dirs = self.assets.list("themes/");
49
50 if !staff {
51 dirs = dirs
52 .into_iter()
53 .filter(|path| !path.starts_with("themes/staff"))
54 .collect()
55 }
56
57 fn get_name(path: &str) -> Option<&str> {
58 path.strip_prefix("themes/")?.strip_suffix(".json")
59 }
60
61 dirs.into_iter().filter_map(|path| match path {
62 Cow::Borrowed(path) => Some(Cow::Borrowed(get_name(path)?)),
63 Cow::Owned(path) => Some(Cow::Owned(get_name(&path)?.to_string())),
64 })
65 }
66
67 pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
68 self.list_names(staff).filter_map(|theme_name| {
69 self.get(theme_name.as_ref())
70 .ok()
71 .map(|theme| theme.meta.clone())
72 })
73 }
74
75 pub fn clear(&self) {
76 self.theme_data.lock().clear();
77 self.themes.lock().clear();
78 }
79
80 pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
81 if let Some(theme) = self.themes.lock().get(name) {
82 return Ok(theme.clone());
83 }
84
85 let asset_path = format!("themes/{}.json", name);
86 let theme_json = self
87 .assets
88 .load(&asset_path)
89 .with_context(|| format!("failed to load theme file {}", asset_path))?;
90
91 // Allocate into the heap directly, the Theme struct is too large to fit in the stack.
92 let mut theme = fonts::with_font_cache(self.font_cache.clone(), || {
93 let mut theme = Box::new(Theme::default());
94 let mut deserializer = serde_json::Deserializer::from_slice(&theme_json);
95 let result = Theme::deserialize_in_place(&mut deserializer, &mut theme);
96 result.map(|_| theme)
97 })?;
98
99 // Reset name to be the file path, so that we can use it to access the stored themes
100 theme.meta.name = name.into();
101 theme.meta.id = self.next_theme_id.fetch_add(1, SeqCst);
102 let theme: Arc<Theme> = theme.into();
103 self.themes.lock().insert(name.to_string(), theme.clone());
104 Ok(theme)
105 }
106}