1#![warn(missing_docs)]
7
8use anyhow::{Context, Result};
9use directories::ProjectDirs;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13fn resolve_data_dir() -> Result<PathBuf> {
17 let project_dirs =
18 ProjectDirs::from("", "", "tca").context("Failed to determine project directories")?;
19 Ok(project_dirs.data_dir().to_path_buf())
20}
21
22pub fn get_data_dir() -> Result<PathBuf> {
26 let data_dir = resolve_data_dir()?;
27 if !data_dir.exists() {
28 fs::create_dir_all(&data_dir)
29 .with_context(|| format!("Failed to create data directory: {:?}", data_dir))?;
30 }
31 Ok(data_dir)
32}
33
34pub fn get_themes_dir() -> Result<PathBuf> {
38 let themes_dir = resolve_data_dir()?.join("themes");
39 if !themes_dir.exists() {
40 fs::create_dir_all(&themes_dir)
41 .with_context(|| format!("Failed to create themes directory: {:?}", themes_dir))?;
42 }
43 Ok(themes_dir)
44}
45
46pub fn list_themes() -> Result<Vec<PathBuf>> {
50 let themes_dir = get_themes_dir()?;
51
52 let mut themes = Vec::new();
53
54 if let Ok(entries) = fs::read_dir(&themes_dir) {
55 for entry in entries.flatten() {
56 let path = entry.path();
57 if !path.is_file() {
58 continue;
59 }
60 if let Some(ext) = path.extension() {
61 if ext == "toml" {
62 themes.push(path);
63 }
64 }
65 }
66 }
67
68 themes.sort();
69 Ok(themes)
70}
71
72pub fn find_theme(name: &str) -> Result<PathBuf> {
77 let themes_dir = get_themes_dir()?;
78
79 let candidate = if !name.ends_with(".toml") {
81 themes_dir.join(format!("{}.toml", name))
82 } else {
83 themes_dir.join(name)
84 };
85
86 if candidate.exists() && candidate.is_file() {
87 return Ok(candidate);
88 }
89
90 Err(anyhow::anyhow!(
91 "Theme '{}' not found in {:?}. Available themes: {:?}",
92 name,
93 themes_dir,
94 list_theme_names()?
95 ))
96}
97
98pub fn list_theme_names() -> Result<Vec<String>> {
100 let themes = list_themes()?;
101
102 Ok(themes
103 .iter()
104 .filter_map(|p| p.file_stem().and_then(|s| s.to_str()).map(String::from))
105 .collect())
106}
107
108pub fn load_theme_file(path_or_name: &str) -> Result<String> {
115 let path = Path::new(path_or_name);
116
117 if path.exists() && path.is_file() {
119 return fs::read_to_string(path)
120 .with_context(|| format!("Failed to read theme file: {:?}", path));
121 }
122
123 if let Ok(shared_path) = find_theme(path_or_name) {
125 return fs::read_to_string(&shared_path)
126 .with_context(|| format!("Failed to read theme file: {:?}", shared_path));
127 }
128
129 Err(anyhow::anyhow!(
130 "Theme '{}' not found. Searched:\n\
131 1. Exact path: {:?}\n\
132 2. Shared themes: {:?}\n\
133 Available shared themes: {:?}",
134 path_or_name,
135 path,
136 get_themes_dir()?,
137 list_theme_names()?
138 ))
139}
140
141pub fn load_all_from_dir(dir: &str) -> Result<Vec<tca_types::Theme>> {
145 let mut items: Vec<tca_types::Theme> = vec![];
146 for entry in fs::read_dir(dir)? {
147 let path = match entry {
148 Err(e) => {
149 eprintln!("Could not read dir entry: {}", e);
150 continue;
151 }
152 Ok(e) => e.path(),
153 };
154 if path.is_file() & path.extension().is_some_and(|x| x == "toml") {
155 match fs::read_to_string(&path) {
156 Err(e) => {
157 eprintln!("Could not read: {:?}.\nError: {}", path, e);
158 continue;
159 }
160 Ok(theme_str) => match toml::from_str(&theme_str) {
161 Err(e) => {
162 eprintln!("Could not parse: {:?}.\nError: {}", path, e);
163 continue;
164 }
165 Ok(item) => items.push(item),
166 },
167 }
168 }
169 }
170 Ok(items)
171}
172
173pub fn load_all_from_theme_dir() -> Result<Vec<tca_types::Theme>> {
178 let dir = get_themes_dir()?;
179 let dir_str = dir
180 .to_str()
181 .context("Data directory path is not valid UTF-8")?;
182 load_all_from_dir(dir_str)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_get_data_dir() {
191 let dir = get_data_dir().unwrap();
192 assert!(dir.exists());
193 assert!(dir.to_string_lossy().contains("tca"));
194 }
195
196 #[test]
197 fn test_get_themes_dir() {
198 let dir = get_themes_dir().unwrap();
199 assert!(dir.exists());
200 assert!(dir.ends_with("themes"));
201 }
202
203 #[test]
204 fn test_list_themes() {
205 let themes = list_themes().unwrap();
206 for theme_path in themes {
208 let ext = theme_path.extension().and_then(|s| s.to_str());
209 assert_eq!(ext, Some("toml"));
210 }
211 }
212
213 #[test]
214 fn test_list_theme_names() {
215 let names = list_theme_names().unwrap();
216 for name in names {
218 assert!(!name.contains('.'));
219 }
220 }
221}