Skip to main content

wisp/settings/
themes.rs

1use clankerdiff_ratatui::theme::{ReviewTheme, ThemeChoice};
2
3use crate::theme::{Theme, ThemeLoadError};
4use std::path::{Path, PathBuf};
5
6pub(crate) fn builtin_review_theme_choices() -> Vec<ThemeChoice> {
7    clankerdiff_ratatui::theme::ReviewTheme::catalog()
8        .into_iter()
9        .map(|descriptor| {
10            ThemeChoice::new(descriptor.name, ReviewTheme::builtin(&descriptor.id).expect("catalog theme"))
11        })
12        .collect()
13}
14
15pub(crate) fn review_theme_choices() -> Vec<ThemeChoice> {
16    let mut choices = builtin_review_theme_choices();
17    for file in list_theme_files() {
18        match load_theme_file(&file) {
19            Ok(theme) => choices.push(ThemeChoice::new(file.trim_end_matches(".json"), theme.review().clone())),
20            Err(error) => tracing::warn!(%file, %error, "Could not load review theme choice"),
21        }
22    }
23    choices
24}
25
26pub fn list_theme_files() -> Vec<String> {
27    let Some(directory) = themes_dir_path() else {
28        return Vec::new();
29    };
30    let Ok(entries) = std::fs::read_dir(directory) else {
31        return Vec::new();
32    };
33    let mut files = entries
34        .filter_map(Result::ok)
35        .filter_map(|entry| {
36            if !entry.file_type().ok()?.is_file() {
37                return None;
38            }
39            let name = entry.file_name().into_string().ok()?;
40            validate_file_name(&name).ok()?;
41            Some(name)
42        })
43        .collect::<Vec<_>>();
44    files.sort_unstable();
45    files
46}
47
48pub fn load_theme_file(file: &str) -> Result<Theme, ThemeLoadError> {
49    let path = resolve_theme_file_path_from_name(file)?;
50    if !std::fs::symlink_metadata(&path)?.file_type().is_file() {
51        return Err(ThemeLoadError::InvalidFile(file.into()));
52    }
53    Theme::load_from_path(&path)
54}
55
56pub(super) fn validate_file_name(file: &str) -> Result<(), ThemeLoadError> {
57    let path = Path::new(file);
58    if file.is_empty()
59        || file.trim() != file
60        || path.file_name().and_then(|name| name.to_str()) != Some(file)
61        || file.contains(['/', '\\'])
62        || path.extension().is_none_or(|extension| extension != "json")
63    {
64        return Err(ThemeLoadError::InvalidFile(file.into()));
65    }
66    Ok(())
67}
68
69fn resolve_theme_file_path_from_name(file: &str) -> Result<PathBuf, ThemeLoadError> {
70    validate_file_name(file)?;
71    themes_dir_path().map(|directory| directory.join(file)).ok_or_else(|| ThemeLoadError::InvalidFile(file.into()))
72}
73
74fn themes_dir_path() -> Option<PathBuf> {
75    Some(super::store()?.home().join("themes"))
76}