Skip to main content

tca_types/
util.rs

1//! Utilities to help load and manage collections of TCA Themes.
2use 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/// Get the themes directory path, creating it if it does not exist.
11///
12/// Returns `$XDG_DATA_HOME/tca/themes/` (or platform equivalent).
13#[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/// Get all themes from a given directory.
27#[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/// Get all local user themes.
60#[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
68/// Get a vec of all available themes.
69pub fn all_themes() -> Vec<Theme> {
70    #[cfg(feature = "fs")]
71    {
72        // First we get all of the built in themes as a set, then update with locally installed ones.
73        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/// Find a path to a theme by name.
91///
92/// Converts name to kebab-case and searches for `<name>.yaml` in the
93/// themes directory.
94/// Returns the full path if found.
95#[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    // If no extension, also try with .yaml appended
101    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")]
118/// Finds a theme file by exact path or theme name and reads it into a String.
119pub fn load_theme_file(path_or_name: &str) -> Result<String> {
120    let path = Path::new(path_or_name);
121
122    // 1. Try exact path (handles absolute paths and relative paths from cwd)
123    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    // 2. Try shared themes directory
129    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}