1pub mod adapters;
55mod color;
56mod error;
57mod gradient;
58mod loader;
59mod resolver;
60mod schema;
61
62pub mod builtins;
63
64use std::collections::HashMap;
65use std::path::Path;
66use std::sync::{Arc, LazyLock};
67
68use parking_lot::RwLock;
69
70pub use color::ThemeColor;
72pub use error::ThemeError;
73pub use gradient::Gradient;
74pub use schema::{ThemeMeta, ThemeVariant};
75pub use style::ThemeStyle;
76
77mod style;
78
79#[derive(Debug, Clone)]
81pub struct Theme {
82 pub meta: ThemeMeta,
84
85 palette: HashMap<String, ThemeColor>,
87
88 tokens: HashMap<String, ThemeColor>,
90
91 styles: HashMap<String, ThemeStyle>,
93
94 gradients: HashMap<String, Gradient>,
96}
97
98impl Theme {
99 #[must_use]
103 pub fn color(&self, token: &str) -> ThemeColor {
104 self.tokens
105 .get(token)
106 .or_else(|| self.palette.get(token))
107 .copied()
108 .unwrap_or(ThemeColor::FALLBACK)
109 }
110
111 #[must_use]
115 pub fn style(&self, name: &str) -> ThemeStyle {
116 self.styles.get(name).cloned().unwrap_or_default()
117 }
118
119 #[must_use]
123 pub fn gradient(&self, name: &str, t: f32) -> ThemeColor {
124 self.gradients
125 .get(name)
126 .map_or(ThemeColor::FALLBACK, |g| g.at(t))
127 }
128
129 #[must_use]
131 pub fn get_gradient(&self, name: &str) -> Option<&Gradient> {
132 self.gradients.get(name)
133 }
134
135 #[must_use]
137 pub fn has_token(&self, token: &str) -> bool {
138 self.tokens.contains_key(token) || self.palette.contains_key(token)
139 }
140
141 #[must_use]
143 pub fn has_style(&self, name: &str) -> bool {
144 self.styles.contains_key(name)
145 }
146
147 #[must_use]
149 pub fn has_gradient(&self, name: &str) -> bool {
150 self.gradients.contains_key(name)
151 }
152
153 #[must_use]
155 pub fn token_names(&self) -> Vec<&str> {
156 self.tokens.keys().map(String::as_str).collect()
157 }
158
159 #[must_use]
161 pub fn style_names(&self) -> Vec<&str> {
162 self.styles.keys().map(String::as_str).collect()
163 }
164
165 #[must_use]
167 pub fn gradient_names(&self) -> Vec<&str> {
168 self.gradients.keys().map(String::as_str).collect()
169 }
170
171 #[must_use]
173 pub fn builtin_neon() -> Self {
174 builtins::silkcircuit_neon()
175 }
176}
177
178impl Default for Theme {
179 fn default() -> Self {
180 Self::builtin_neon()
181 }
182}
183
184static ACTIVE_THEME: LazyLock<RwLock<Arc<Theme>>> =
190 LazyLock::new(|| RwLock::new(Arc::new(Theme::builtin_neon())));
191
192#[must_use]
194pub fn current() -> Arc<Theme> {
195 ACTIVE_THEME.read().clone()
196}
197
198pub fn set_theme(theme: Theme) {
200 *ACTIVE_THEME.write() = Arc::new(theme);
201}
202
203pub fn load_theme(path: &Path) -> Result<(), ThemeError> {
208 let theme = loader::load_from_file(path)?;
209 set_theme(theme);
210 Ok(())
211}
212
213pub fn load_theme_by_name(name: &str) -> Result<(), ThemeError> {
218 if let Some(theme) = builtins::load_by_name(name) {
220 set_theme(theme);
221 return Ok(());
222 }
223
224 for path in discovery_paths() {
226 let theme_path = path.join(format!("{name}.toml"));
227 if theme_path.exists() {
228 return load_theme(&theme_path);
229 }
230 }
231
232 Err(ThemeError::ThemeNotFound {
233 name: name.to_string(),
234 })
235}
236
237#[must_use]
239pub fn list_available_themes() -> Vec<ThemeInfo> {
240 let mut themes: Vec<ThemeInfo> = builtins::builtin_names()
242 .iter()
243 .map(|(name, display_name)| {
244 let theme = builtins::load_by_name(name).expect("builtin theme should load");
245 ThemeInfo {
246 name: (*name).to_string(),
247 display_name: (*display_name).to_string(),
248 variant: theme.meta.variant,
249 author: theme.meta.author.clone().unwrap_or_default(),
250 description: theme.meta.description.clone().unwrap_or_default(),
251 builtin: true,
252 path: None,
253 }
254 })
255 .collect();
256
257 for dir in discovery_paths() {
259 if let Ok(entries) = std::fs::read_dir(&dir) {
260 for entry in entries.flatten() {
261 let path = entry.path();
262 if path.extension().is_some_and(|ext| ext == "toml")
263 && let Ok(theme) = loader::load_from_file(&path)
264 {
265 let name = path
266 .file_stem()
267 .and_then(|s| s.to_str())
268 .unwrap_or("unknown")
269 .to_string();
270
271 themes.push(ThemeInfo {
272 name,
273 display_name: theme.meta.name,
274 variant: theme.meta.variant,
275 author: theme.meta.author.unwrap_or_default(),
276 description: theme.meta.description.unwrap_or_default(),
277 builtin: false,
278 path: Some(path),
279 });
280 }
281 }
282 }
283 }
284
285 themes
286}
287
288#[derive(Debug, Clone)]
290pub struct ThemeInfo {
291 pub name: String,
293 pub display_name: String,
295 pub variant: ThemeVariant,
297 pub author: String,
299 pub description: String,
301 pub builtin: bool,
303 pub path: Option<std::path::PathBuf>,
305}
306
307fn discovery_paths() -> Vec<std::path::PathBuf> {
313 let mut paths = Vec::new();
314
315 if let Some(home) = dirs::home_dir() {
317 paths.push(home.join(".config/git-iris/themes"));
318 }
319
320 if let Some(xdg_config) = dirs::config_dir() {
322 let xdg_path = xdg_config.join("git-iris/themes");
323 if !paths.contains(&xdg_path) {
324 paths.push(xdg_path);
325 }
326 }
327
328 paths
329}
330
331#[cfg(test)]
332mod tests;