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_non_empty_css_with_color_primary() {
73 let theme = Theme::default_theme();
74 assert!(!theme.css.is_empty());
75 assert!(theme.css.contains("--color-primary"));
76 }
77
78 #[test]
79 fn default_theme_returns_all_none_templates() {
80 let theme = Theme::default_theme();
81 assert!(theme.templates.browse.is_none());
82 assert!(theme.templates.focus.is_none());
83 assert!(theme.templates.collect.is_none());
84 assert!(theme.templates.process.is_none());
85 assert!(theme.templates.summarize.is_none());
86 assert!(theme.templates.analyze.is_none());
87 assert!(theme.templates.track.is_none());
88 }
89
90 #[test]
91 fn from_path_loads_tokens_css_and_theme_json() {
92 let dir = tempfile::tempdir().unwrap();
93 let css_content = "@theme { --color-primary: oklch(55% 0.2 250); }";
94 let json_content = r#"{"browse": {"display": {"slots": ["title", "fields"]}}}"#;
95 std::fs::write(dir.path().join("tokens.css"), css_content).unwrap();
96 std::fs::write(dir.path().join("theme.json"), json_content).unwrap();
97
98 let theme = Theme::from_path(dir.path().to_str().unwrap()).unwrap();
99 assert_eq!(theme.css, css_content);
100 assert!(theme.templates.browse.is_some());
101 let browse = theme.templates.browse.unwrap();
102 assert_eq!(browse.display.slots, vec!["title", "fields"]);
103 }
104
105 #[test]
106 fn from_path_works_without_theme_json() {
107 let dir = tempfile::tempdir().unwrap();
108 let css_content = "@theme { --color-primary: oklch(55% 0.2 250); }";
109 std::fs::write(dir.path().join("tokens.css"), css_content).unwrap();
110
111 let theme = Theme::from_path(dir.path().to_str().unwrap()).unwrap();
112 assert_eq!(theme.css, css_content);
113 assert!(theme.templates.browse.is_none());
114 }
115
116 #[test]
117 fn from_path_returns_not_found_for_nonexistent_directory() {
118 let result = Theme::from_path("/nonexistent/path/that/does/not/exist");
119 assert!(matches!(result, Err(ThemeError::NotFound(_))));
120 }
121
122 #[test]
123 fn from_path_returns_json_error_for_invalid_theme_json() {
124 let dir = tempfile::tempdir().unwrap();
125 std::fs::write(dir.path().join("tokens.css"), "@theme {}").unwrap();
126 std::fs::write(dir.path().join("theme.json"), "not valid json{{").unwrap();
127
128 let result = Theme::from_path(dir.path().to_str().unwrap());
129 assert!(matches!(result, Err(ThemeError::Json(_))));
130 }
131
132 #[test]
133 fn from_path_returns_io_error_when_tokens_css_missing() {
134 let dir = tempfile::tempdir().unwrap();
135 std::fs::write(dir.path().join("theme.json"), "{}").unwrap();
137
138 let result = Theme::from_path(dir.path().to_str().unwrap());
139 assert!(matches!(result, Err(ThemeError::Io(_))));
140 }
141}