ricecoder_storage/
theme.rs1use crate::error::{StorageError, StorageResult};
8use crate::manager::PathResolver;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct ThemePreference {
16 pub current_theme: String,
18 #[serde(default)]
20 pub last_updated: Option<String>,
21}
22
23impl Default for ThemePreference {
24 fn default() -> Self {
25 Self {
26 current_theme: "dark".to_string(),
27 last_updated: None,
28 }
29 }
30}
31
32pub struct ThemeStorage;
34
35impl ThemeStorage {
36 pub fn themes_directory() -> StorageResult<PathBuf> {
40 let global_path = PathResolver::resolve_global_path()?;
41 let themes_dir = global_path.join("themes");
42 Ok(themes_dir)
43 }
44
45 pub fn preference_file() -> StorageResult<PathBuf> {
49 let global_path = PathResolver::resolve_global_path()?;
50 let pref_file = global_path.join("theme.yaml");
51 Ok(pref_file)
52 }
53
54 pub fn load_preference() -> StorageResult<ThemePreference> {
58 let pref_file = Self::preference_file()?;
59
60 if !pref_file.exists() {
61 return Ok(ThemePreference::default());
62 }
63
64 let content = fs::read_to_string(&pref_file).map_err(|e| {
65 StorageError::io_error(
66 pref_file.clone(),
67 crate::error::IoOperation::Read,
68 e,
69 )
70 })?;
71
72 let preference: ThemePreference = serde_yaml::from_str(&content).map_err(|e| {
73 StorageError::parse_error(
74 pref_file.clone(),
75 "yaml",
76 e.to_string(),
77 )
78 })?;
79
80 Ok(preference)
81 }
82
83 pub fn save_preference(preference: &ThemePreference) -> StorageResult<()> {
87 let pref_file = Self::preference_file()?;
88
89 if let Some(parent) = pref_file.parent() {
91 fs::create_dir_all(parent).map_err(|e| {
92 StorageError::directory_creation_failed(
93 parent.to_path_buf(),
94 e,
95 )
96 })?;
97 }
98
99 let content = serde_yaml::to_string(preference).map_err(|e| {
100 StorageError::internal(format!("Failed to serialize theme preference: {}", e))
101 })?;
102
103 fs::write(&pref_file, content).map_err(|e| {
104 StorageError::io_error(
105 pref_file.clone(),
106 crate::error::IoOperation::Write,
107 e,
108 )
109 })?;
110
111 Ok(())
112 }
113
114 pub fn save_custom_theme(theme_name: &str, content: &str) -> StorageResult<PathBuf> {
118 let themes_dir = Self::themes_directory()?;
119
120 fs::create_dir_all(&themes_dir).map_err(|e| {
122 StorageError::directory_creation_failed(
123 themes_dir.clone(),
124 e,
125 )
126 })?;
127
128 let theme_file = themes_dir.join(format!("{}.yaml", theme_name));
129
130 fs::write(&theme_file, content).map_err(|e| {
131 StorageError::io_error(
132 theme_file.clone(),
133 crate::error::IoOperation::Write,
134 e,
135 )
136 })?;
137
138 Ok(theme_file)
139 }
140
141 pub fn load_custom_theme(theme_name: &str) -> StorageResult<String> {
145 let themes_dir = Self::themes_directory()?;
146 let theme_file = themes_dir.join(format!("{}.yaml", theme_name));
147
148 if !theme_file.exists() {
149 return Err(StorageError::validation_error(
150 "theme",
151 format!("Theme file not found: {}", theme_file.display()),
152 ));
153 }
154
155 fs::read_to_string(&theme_file).map_err(|e| {
156 StorageError::io_error(
157 theme_file.clone(),
158 crate::error::IoOperation::Read,
159 e,
160 )
161 })
162 }
163
164 pub fn delete_custom_theme(theme_name: &str) -> StorageResult<()> {
166 let themes_dir = Self::themes_directory()?;
167 let theme_file = themes_dir.join(format!("{}.yaml", theme_name));
168
169 if !theme_file.exists() {
170 return Err(StorageError::validation_error(
171 "theme",
172 format!("Theme file not found: {}", theme_file.display()),
173 ));
174 }
175
176 fs::remove_file(&theme_file).map_err(|e| {
177 StorageError::io_error(
178 theme_file.clone(),
179 crate::error::IoOperation::Delete,
180 e,
181 )
182 })?;
183
184 Ok(())
185 }
186
187 pub fn list_custom_themes() -> StorageResult<Vec<String>> {
189 let themes_dir = Self::themes_directory()?;
190
191 if !themes_dir.exists() {
192 return Ok(Vec::new());
193 }
194
195 let mut themes = Vec::new();
196
197 for entry in fs::read_dir(&themes_dir).map_err(|e| {
198 StorageError::io_error(
199 themes_dir.clone(),
200 crate::error::IoOperation::Read,
201 e,
202 )
203 })? {
204 let entry = entry.map_err(|e| {
205 StorageError::io_error(
206 themes_dir.clone(),
207 crate::error::IoOperation::Read,
208 e,
209 )
210 })?;
211
212 let path = entry.path();
213 if path.extension().map_or(false, |ext| ext == "yaml") {
214 if let Some(file_stem) = path.file_stem() {
215 if let Some(theme_name) = file_stem.to_str() {
216 themes.push(theme_name.to_string());
217 }
218 }
219 }
220 }
221
222 Ok(themes)
223 }
224
225 pub fn custom_theme_exists(theme_name: &str) -> StorageResult<bool> {
227 let themes_dir = Self::themes_directory()?;
228 let theme_file = themes_dir.join(format!("{}.yaml", theme_name));
229 Ok(theme_file.exists())
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use tempfile::TempDir;
237 use std::sync::Mutex;
238
239 static TEST_LOCK: Mutex<()> = Mutex::new(());
241
242 #[test]
243 fn test_theme_preference_default() {
244 let pref = ThemePreference::default();
245 assert_eq!(pref.current_theme, "dark");
246 }
247
248 #[test]
249 fn test_save_and_load_preference() {
250 let _lock = TEST_LOCK.lock().unwrap();
251 let temp_dir = TempDir::new().unwrap();
252 let home_path = temp_dir.path().to_string_lossy().to_string();
253 std::env::set_var("RICECODER_HOME", &home_path);
254
255 let pref = ThemePreference {
256 current_theme: "light".to_string(),
257 last_updated: Some("2025-12-09".to_string()),
258 };
259
260 ThemeStorage::save_preference(&pref).unwrap();
261 let loaded = ThemeStorage::load_preference().unwrap();
262
263 assert_eq!(loaded.current_theme, "light");
264 assert_eq!(loaded.last_updated, Some("2025-12-09".to_string()));
265
266 std::env::remove_var("RICECODER_HOME");
267 }
268
269 #[test]
270 fn test_load_preference_default_when_missing() {
271 let _lock = TEST_LOCK.lock().unwrap();
272 let temp_dir = TempDir::new().unwrap();
273 let home_path = temp_dir.path().to_string_lossy().to_string();
274 std::env::set_var("RICECODER_HOME", &home_path);
275
276 let loaded = ThemeStorage::load_preference().unwrap();
277 assert_eq!(loaded.current_theme, "dark");
278
279 std::env::remove_var("RICECODER_HOME");
280 }
281
282 #[test]
283 fn test_save_and_load_custom_theme() {
284 let _lock = TEST_LOCK.lock().unwrap();
285 let temp_dir = TempDir::new().unwrap();
286 let home_path = temp_dir.path().to_string_lossy().to_string();
287 std::env::set_var("RICECODER_HOME", &home_path);
288
289 let theme_content = "name: custom\ncolors:\n background: '#000000'";
290 let saved_path = ThemeStorage::save_custom_theme("custom", theme_content).unwrap();
291
292 assert!(saved_path.exists(), "File not found at: {}", saved_path.display());
294
295 let loaded = ThemeStorage::load_custom_theme("custom").unwrap();
297 assert_eq!(loaded, theme_content);
298
299 std::env::remove_var("RICECODER_HOME");
300 }
301
302 #[test]
303 fn test_delete_custom_theme() {
304 let _lock = TEST_LOCK.lock().unwrap();
305 let temp_dir = TempDir::new().unwrap();
306 let home_path = temp_dir.path().to_string_lossy().to_string();
307 std::env::set_var("RICECODER_HOME", &home_path);
308
309 let theme_content = "name: custom\ncolors:\n background: '#000000'";
310 ThemeStorage::save_custom_theme("custom", theme_content).unwrap();
311
312 assert!(ThemeStorage::custom_theme_exists("custom").unwrap());
313
314 ThemeStorage::delete_custom_theme("custom").unwrap();
315
316 assert!(!ThemeStorage::custom_theme_exists("custom").unwrap());
317
318 std::env::remove_var("RICECODER_HOME");
319 }
320
321 #[test]
322 fn test_list_custom_themes() {
323 let _lock = TEST_LOCK.lock().unwrap();
324 let temp_dir = TempDir::new().unwrap();
325 let home_path = temp_dir.path().to_string_lossy().to_string();
326 std::env::set_var("RICECODER_HOME", &home_path);
327
328 ThemeStorage::save_custom_theme("theme1", "name: theme1").unwrap();
329 ThemeStorage::save_custom_theme("theme2", "name: theme2").unwrap();
330
331 let themes = ThemeStorage::list_custom_themes().unwrap();
332 assert_eq!(themes.len(), 2);
333 assert!(themes.contains(&"theme1".to_string()));
334 assert!(themes.contains(&"theme2".to_string()));
335
336 std::env::remove_var("RICECODER_HOME");
337 }
338
339 #[test]
340 fn test_custom_theme_exists() {
341 let _lock = TEST_LOCK.lock().unwrap();
342 let temp_dir = TempDir::new().unwrap();
343 let home_path = temp_dir.path().to_string_lossy().to_string();
344 std::env::set_var("RICECODER_HOME", &home_path);
345
346 assert!(!ThemeStorage::custom_theme_exists("nonexistent").unwrap());
347
348 ThemeStorage::save_custom_theme("existing", "name: existing").unwrap();
349 assert!(ThemeStorage::custom_theme_exists("existing").unwrap());
350
351 std::env::remove_var("RICECODER_HOME");
352 }
353}