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> {
128 let themes_dir = get_themes_dir()?;
129
130 let name = convert_case::ccase!(kebab, name);
131 let candidate = if !name.ends_with(".toml") {
133 themes_dir.join(format!("{}.toml", name))
134 } else {
135 themes_dir.join(&name)
136 };
137 if candidate.exists() && candidate.is_file() {
138 return Ok(candidate);
139 }
140
141 Err(anyhow::anyhow!(
142 "Theme '{}' not found in {:?}.",
143 name,
144 themes_dir,
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 path_or_name,
184 path,
185 get_themes_dir()?,
186 ))
187}
188
189pub fn load_all_from_dir(dir: &str) -> Result<Vec<tca_types::Theme>> {
193 let mut items: Vec<tca_types::Theme> = vec![];
194 for entry in fs::read_dir(dir)? {
195 let path = match entry {
196 Err(e) => {
197 eprintln!("Could not read dir entry: {}", e);
198 continue;
199 }
200 Ok(e) => e.path(),
201 };
202 if path.is_file() & path.extension().is_some_and(|x| x == "toml") {
203 match fs::read_to_string(&path) {
204 Err(e) => {
205 eprintln!("Could not read: {:?}.\nError: {}", path, e);
206 continue;
207 }
208 Ok(theme_str) => match toml::from_str(&theme_str) {
209 Err(e) => {
210 eprintln!("Could not parse: {:?}.\nError: {}", path, e);
211 continue;
212 }
213 Ok(item) => items.push(item),
214 },
215 }
216 }
217 }
218 Ok(items)
219}
220
221pub fn load_all_from_theme_dir() -> Result<Vec<tca_types::Theme>> {
226 let dir = get_themes_dir()?;
227 let dir_str = dir
228 .to_str()
229 .context("Data directory path is not valid UTF-8")?;
230 load_all_from_dir(dir_str)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_get_themes_dir() {
239 let dir = get_themes_dir().unwrap();
240 assert!(dir.exists());
241 assert!(dir.ends_with("tca-themes"));
242 }
243
244 #[test]
245 fn test_list_themes() {
246 let themes = list_themes().unwrap();
247 for theme_path in themes {
249 let ext = theme_path.extension().and_then(|s| s.to_str());
250 assert_eq!(ext, Some("toml"));
251 }
252 }
253
254 #[test]
255 fn test_list_theme_names() {
256 let names = list_theme_names().unwrap();
257 for name in names {
259 assert!(!name.contains('.'));
260 }
261 }
262}