1#![warn(missing_docs)]
7
8use anyhow::{Context, Result};
9use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14#[derive(Default, Debug, Serialize, Deserialize)]
16pub struct TcaConfig {
17 pub default_theme: Option<String>,
20 pub default_dark_theme: Option<String>,
22 pub default_light_theme: Option<String>,
24}
25
26fn config_file_path() -> Result<PathBuf> {
28 let strategy = choose_app_strategy(AppStrategyArgs {
29 top_level_domain: "org".to_string(),
30 author: "TCA".to_string(),
31 app_name: "tca".to_string(),
32 })?;
33 Ok(strategy.config_dir().join("tca.toml"))
34}
35
36impl TcaConfig {
37 pub fn load() -> Self {
41 let Ok(path) = config_file_path() else {
42 return Self::default();
43 };
44 let Ok(content) = fs::read_to_string(path) else {
45 return Self::default();
46 };
47 toml::from_str(&content).unwrap_or_default()
48 }
49
50 pub fn store(&self) {
52 let path = config_file_path().expect("Could not determine TCA config path.");
53 if let Some(parent) = path.parent() {
54 fs::create_dir_all(parent).expect("Could not create TCA config directory.");
55 }
56 let content = toml::to_string(self).expect("Could not serialize TCA config.");
57 fs::write(&path, content).expect("Could not save TCA config.");
58 }
59
60 pub fn mode_aware_theme(&self) -> Option<String> {
63 use terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode};
66 match theme_mode(QueryOptions::default()).ok() {
67 Some(ThemeMode::Dark) => self
68 .default_dark_theme
69 .clone()
70 .or(self.default_theme.clone()),
71 Some(ThemeMode::Light) => self
72 .default_light_theme
73 .clone()
74 .or(self.default_theme.clone()),
75 None => self.default_theme.clone(),
76 }
77 }
78}
79
80pub fn get_themes_dir() -> Result<PathBuf> {
84 let strategy = choose_app_strategy(AppStrategyArgs {
85 top_level_domain: "org".to_string(),
86 author: "TCA".to_string(),
87 app_name: "tca-themes".to_string(),
88 })
89 .unwrap();
90 let data_dir = strategy.data_dir();
91 fs::create_dir_all(&data_dir)?;
92
93 Ok(data_dir)
94}
95
96pub fn list_themes() -> Result<Vec<PathBuf>> {
100 let themes_dir = get_themes_dir()?;
101
102 let mut themes = Vec::new();
103
104 if let Ok(entries) = fs::read_dir(&themes_dir) {
105 for entry in entries.flatten() {
106 let path = entry.path();
107 if !path.is_file() {
108 continue;
109 }
110 if let Some(ext) = path.extension() {
111 if ext == "toml" {
112 themes.push(path);
113 }
114 }
115 }
116 }
117
118 themes.sort();
119 Ok(themes)
120}
121
122pub fn find_theme(name: &str) -> Result<PathBuf> {
127 let themes_dir = get_themes_dir()?;
128
129 let candidate = if !name.ends_with(".toml") {
131 themes_dir.join(format!("{}.toml", name))
132 } else {
133 themes_dir.join(name)
134 };
135
136 if candidate.exists() && candidate.is_file() {
137 return Ok(candidate);
138 }
139
140 Err(anyhow::anyhow!(
141 "Theme '{}' not found in {:?}. Available themes: {:?}",
142 name,
143 themes_dir,
144 list_theme_names()?
145 ))
146}
147
148pub fn list_theme_names() -> Result<Vec<String>> {
150 let themes = list_themes()?;
151
152 Ok(themes
153 .iter()
154 .filter_map(|p| p.file_stem().and_then(|s| s.to_str()).map(String::from))
155 .collect())
156}
157
158pub fn load_theme_file(path_or_name: &str) -> Result<String> {
165 let path = Path::new(path_or_name);
166
167 if path.exists() && path.is_file() {
169 return fs::read_to_string(path)
170 .with_context(|| format!("Failed to read theme file: {:?}", path));
171 }
172
173 if let Ok(shared_path) = find_theme(path_or_name) {
175 return fs::read_to_string(&shared_path)
176 .with_context(|| format!("Failed to read theme file: {:?}", shared_path));
177 }
178
179 Err(anyhow::anyhow!(
180 "Theme '{}' not found. Searched:\n\
181 1. Exact path: {:?}\n\
182 2. Shared themes: {:?}\n\
183 Available shared themes: {:?}",
184 path_or_name,
185 path,
186 get_themes_dir()?,
187 list_theme_names()?
188 ))
189}
190
191pub fn load_all_from_dir(dir: &str) -> Result<Vec<tca_types::Theme>> {
195 let mut items: Vec<tca_types::Theme> = vec![];
196 for entry in fs::read_dir(dir)? {
197 let path = match entry {
198 Err(e) => {
199 eprintln!("Could not read dir entry: {}", e);
200 continue;
201 }
202 Ok(e) => e.path(),
203 };
204 if path.is_file() & path.extension().is_some_and(|x| x == "toml") {
205 match fs::read_to_string(&path) {
206 Err(e) => {
207 eprintln!("Could not read: {:?}.\nError: {}", path, e);
208 continue;
209 }
210 Ok(theme_str) => match toml::from_str(&theme_str) {
211 Err(e) => {
212 eprintln!("Could not parse: {:?}.\nError: {}", path, e);
213 continue;
214 }
215 Ok(item) => items.push(item),
216 },
217 }
218 }
219 }
220 Ok(items)
221}
222
223pub fn load_all_from_theme_dir() -> Result<Vec<tca_types::Theme>> {
228 let dir = get_themes_dir()?;
229 let dir_str = dir
230 .to_str()
231 .context("Data directory path is not valid UTF-8")?;
232 load_all_from_dir(dir_str)
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_get_themes_dir() {
241 let dir = get_themes_dir().unwrap();
242 assert!(dir.exists());
243 assert!(dir.ends_with("tca-themes"));
244 }
245
246 #[test]
247 fn test_list_themes() {
248 let themes = list_themes().unwrap();
249 for theme_path in themes {
251 let ext = theme_path.extension().and_then(|s| s.to_str());
252 assert_eq!(ext, Some("toml"));
253 }
254 }
255
256 #[test]
257 fn test_list_theme_names() {
258 let names = list_theme_names().unwrap();
259 for name in names {
261 assert!(!name.contains('.'));
262 }
263 }
264}