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}