1use std::path::Path;
2
3use crate::error::ThemeError;
4use crate::template::ThemeTemplates;
5
6const DEFAULT_THEME_CSS: &str = include_str!("../assets/default.css");
7
8#[derive(Debug, Clone)]
14pub struct Theme {
15 pub css: String,
17
18 pub templates: ThemeTemplates,
20}
21
22impl Theme {
23 pub fn default_theme() -> Self {
30 Self {
31 css: DEFAULT_THEME_CSS.to_string(),
32 templates: ThemeTemplates::default(),
33 }
34 }
35
36 pub fn from_path(path: &str) -> Result<Self, ThemeError> {
47 let dir = Path::new(path);
48 if !dir.exists() {
49 return Err(ThemeError::NotFound(path.to_string()));
50 }
51
52 let css_path = dir.join("tokens.css");
53 let css = std::fs::read_to_string(css_path)?;
54
55 let templates_path = dir.join("theme.json");
56 let templates = if templates_path.exists() {
57 let json = std::fs::read_to_string(&templates_path)?;
58 serde_json::from_str(&json)?
59 } else {
60 ThemeTemplates::default()
61 };
62
63 Ok(Self { css, templates })
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn default_theme_returns_all_30_token_slots() {
73 let theme = Theme::default_theme();
74 assert!(!theme.css.is_empty());
75 for token in crate::token::ALL_TOKENS {
79 assert!(
80 theme.css.contains(&format!("{token}:")),
81 "default.css missing declaration for {token}"
82 );
83 }
84 }
85
86 #[test]
87 fn default_theme_returns_all_none_templates() {
88 let theme = Theme::default_theme();
89 assert!(theme.templates.browse.is_none());
90 assert!(theme.templates.focus.is_none());
91 assert!(theme.templates.collect.is_none());
92 assert!(theme.templates.process.is_none());
93 assert!(theme.templates.summarize.is_none());
94 assert!(theme.templates.analyze.is_none());
95 assert!(theme.templates.track.is_none());
96 }
97
98 #[test]
99 fn from_path_loads_tokens_css_and_theme_json() {
100 let dir = tempfile::tempdir().unwrap();
101 let css_content = "@theme { --color-primary: oklch(55% 0.2 250); }";
102 let json_content = r#"{"browse": {"display": {"slots": ["title", "fields"]}}}"#;
103 std::fs::write(dir.path().join("tokens.css"), css_content).unwrap();
104 std::fs::write(dir.path().join("theme.json"), json_content).unwrap();
105
106 let theme = Theme::from_path(dir.path().to_str().unwrap()).unwrap();
107 assert_eq!(theme.css, css_content);
108 assert!(theme.templates.browse.is_some());
109 let browse = theme.templates.browse.unwrap();
110 assert_eq!(browse.display.slots, vec!["title", "fields"]);
111 }
112
113 #[test]
114 fn from_path_works_without_theme_json() {
115 let dir = tempfile::tempdir().unwrap();
116 let css_content = "@theme { --color-primary: oklch(55% 0.2 250); }";
117 std::fs::write(dir.path().join("tokens.css"), css_content).unwrap();
118
119 let theme = Theme::from_path(dir.path().to_str().unwrap()).unwrap();
120 assert_eq!(theme.css, css_content);
121 assert!(theme.templates.browse.is_none());
122 }
123
124 #[test]
125 fn from_path_returns_not_found_for_nonexistent_directory() {
126 let result = Theme::from_path("/nonexistent/path/that/does/not/exist");
127 assert!(matches!(result, Err(ThemeError::NotFound(_))));
128 }
129
130 #[test]
131 fn from_path_returns_json_error_for_invalid_theme_json() {
132 let dir = tempfile::tempdir().unwrap();
133 std::fs::write(dir.path().join("tokens.css"), "@theme {}").unwrap();
134 std::fs::write(dir.path().join("theme.json"), "not valid json{{").unwrap();
135
136 let result = Theme::from_path(dir.path().to_str().unwrap());
137 assert!(matches!(result, Err(ThemeError::Json(_))));
138 }
139
140 #[test]
141 fn from_path_returns_io_error_when_tokens_css_missing() {
142 let dir = tempfile::tempdir().unwrap();
143 std::fs::write(dir.path().join("theme.json"), "{}").unwrap();
145
146 let result = Theme::from_path(dir.path().to_str().unwrap());
147 assert!(matches!(result, Err(ThemeError::Io(_))));
148 }
149}