1use crate::{theme::Theme, BuiltinTheme};
3#[cfg(feature = "fs")]
4use anyhow::{Context, Result};
5#[cfg(feature = "fs")]
6use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
7#[cfg(feature = "fs")]
8use std::{collections::HashMap, fs, path::Path, path::PathBuf};
9
10#[cfg(feature = "fs")]
14pub fn user_themes_path() -> Result<PathBuf> {
15 let strategy = choose_app_strategy(AppStrategyArgs {
16 top_level_domain: "org".to_string(),
17 author: "TCA".to_string(),
18 app_name: "tca".to_string(),
19 })?;
20 let themes_dir = strategy.data_dir().join("themes");
21 fs::create_dir_all(&themes_dir)?;
22
23 Ok(themes_dir)
24}
25
26#[cfg(feature = "fs")]
28pub fn all_from_dir(dir: &Path) -> Vec<Theme> {
29 let mut items = Vec::new();
30 if let Ok(entries) = fs::read_dir(dir) {
31 for entry in entries {
32 let path = match entry {
33 Err(e) => {
34 eprintln!("Could not read dir entry: {}", e);
35 continue;
36 }
37 Ok(e) => e.path(),
38 };
39 if path.is_file() && path.extension().is_some_and(|x| x == "yaml") {
40 match fs::read_to_string(&path) {
41 Err(e) => {
42 eprintln!("Could not read: {:?}.\nError: {}", path, e);
43 continue;
44 }
45 Ok(theme_str) => match Theme::from_base24_str(&theme_str) {
46 Err(e) => {
47 eprintln!("Could not parse: {:?}.\nError: {}", path, e);
48 continue;
49 }
50 Ok(item) => items.push(item),
51 },
52 }
53 }
54 }
55 }
56 items
57}
58
59#[cfg(feature = "fs")]
61pub fn all_user_themes() -> Vec<Theme> {
62 let Ok(themes_dir) = user_themes_path() else {
63 return Vec::new();
64 };
65 all_from_dir(&themes_dir)
66}
67
68pub fn all_themes() -> Vec<Theme> {
70 #[cfg(feature = "fs")]
71 {
72 let mut themes: HashMap<_, _> = all_user_themes()
74 .into_iter()
75 .map(|t| (t.meta.name.clone(), t))
76 .collect();
77 for t in BuiltinTheme::iter() {
78 let theme = t.theme();
79 themes.entry(theme.meta.name.clone()).or_insert(theme);
80 }
81 themes.into_values().collect()
82 }
83
84 #[cfg(not(feature = "fs"))]
85 {
86 BuiltinTheme::iter().map(|t| t.theme()).collect()
87 }
88}
89
90#[cfg(feature = "fs")]
96pub fn find_theme_path(name: &str) -> Result<PathBuf> {
97 let themes_dir = user_themes_path()?;
98
99 let name = heck::AsKebabCase(name).to_string();
100 let candidate = if !name.ends_with(".yaml") {
102 themes_dir.join(format!("{}.yaml", name))
103 } else {
104 themes_dir.join(&name)
105 };
106 if candidate.exists() && candidate.is_file() {
107 return Ok(candidate);
108 }
109
110 Err(anyhow::anyhow!(
111 "Theme '{}' not found in {:?}.",
112 name,
113 themes_dir,
114 ))
115}
116
117#[cfg(feature = "fs")]
118pub fn load_theme_file(path_or_name: &str) -> Result<String> {
120 let path = Path::new(path_or_name);
121
122 if path.exists() && path.is_file() {
124 return fs::read_to_string(path)
125 .with_context(|| format!("Failed to read theme file: {:?}", path));
126 }
127
128 if let Ok(shared_path) = find_theme_path(path_or_name) {
130 return fs::read_to_string(&shared_path)
131 .with_context(|| format!("Failed to read theme file: {:?}", shared_path));
132 }
133
134 Err(anyhow::anyhow!(
135 "Theme '{}' not found. Searched:\n\
136 1. Exact path: {:?}\n\
137 2. Shared themes: {:?}\n",
138 path_or_name,
139 path,
140 user_themes_path()?,
141 ))
142}
143
144#[cfg(feature = "fs")]
145pub fn mode_aware_theme_name() -> Option<String> {
146 use crate::config::TcaConfig;
147 use terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode};
148 let cfg = TcaConfig::load();
149
150 match theme_mode(QueryOptions::default()).ok() {
151 Some(ThemeMode::Dark) => cfg
152 .tca
153 .default_dark_theme
154 .clone()
155 .or(cfg.tca.default_theme.clone()),
156 Some(ThemeMode::Light) => cfg
157 .tca
158 .default_light_theme
159 .clone()
160 .or(cfg.tca.default_theme.clone()),
161 None => cfg.tca.default_theme.clone(),
162 }
163}