Skip to main content

jolt_theme/
cache.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use crate::iterm2::{list_available_schemes, lookup_variant_pair, Iterm2Error, SchemeVariant};
8
9const CACHE_TTL_DAYS: u64 = 5;
10const CACHE_FILENAME: &str = "iterm2_schemes.json";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ThemeGroup {
14    pub name: String,
15    pub dark: Option<String>,
16    pub light: Option<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct CachedSchemeList {
21    pub timestamp: u64,
22    pub schemes: Vec<String>,
23    pub groups: Vec<ThemeGroup>,
24}
25
26impl CachedSchemeList {
27    pub fn is_expired(&self) -> bool {
28        let now = SystemTime::now()
29            .duration_since(UNIX_EPOCH)
30            .unwrap_or_default()
31            .as_secs();
32        let age_days = (now - self.timestamp) / 86400;
33        age_days >= CACHE_TTL_DAYS
34    }
35
36    pub fn age_description(&self) -> String {
37        let now = SystemTime::now()
38            .duration_since(UNIX_EPOCH)
39            .unwrap_or_default()
40            .as_secs();
41        let age_secs = now.saturating_sub(self.timestamp);
42
43        if age_secs < 60 {
44            "just now".to_string()
45        } else if age_secs < 3600 {
46            format!("{} minutes ago", age_secs / 60)
47        } else if age_secs < 86400 {
48            format!("{} hours ago", age_secs / 3600)
49        } else {
50            format!("{} days ago", age_secs / 86400)
51        }
52    }
53}
54
55fn cache_path(cache_dir: &Path) -> std::path::PathBuf {
56    cache_dir.join(CACHE_FILENAME)
57}
58
59pub fn load_cached_schemes(cache_dir: &Path) -> Option<CachedSchemeList> {
60    let path = cache_path(cache_dir);
61    if !path.exists() {
62        return None;
63    }
64
65    let content = fs::read_to_string(&path).ok()?;
66    serde_json::from_str(&content).ok()
67}
68
69pub fn save_cached_schemes(cache_dir: &Path, cache: &CachedSchemeList) -> std::io::Result<()> {
70    fs::create_dir_all(cache_dir)?;
71    let path = cache_path(cache_dir);
72    let content = serde_json::to_string_pretty(cache)
73        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
74    fs::write(path, content)
75}
76
77pub fn group_schemes(schemes: &[String]) -> Vec<ThemeGroup> {
78    let scheme_set: std::collections::HashSet<&str> = schemes.iter().map(|s| s.as_str()).collect();
79    let mut grouped: HashMap<String, ThemeGroup> = HashMap::new();
80    let mut processed: std::collections::HashSet<&str> = std::collections::HashSet::new();
81
82    for scheme in schemes {
83        if processed.contains(scheme.as_str()) {
84            continue;
85        }
86
87        if let Some((dark, light)) = lookup_variant_pair(scheme) {
88            let group_name = derive_group_name(dark, light);
89            let has_dark = scheme_set.contains(dark);
90            let has_light = scheme_set.contains(light);
91
92            grouped.insert(
93                group_name.clone(),
94                ThemeGroup {
95                    name: group_name,
96                    dark: if has_dark {
97                        Some(dark.to_string())
98                    } else {
99                        None
100                    },
101                    light: if has_light {
102                        Some(light.to_string())
103                    } else {
104                        None
105                    },
106                },
107            );
108
109            processed.insert(dark);
110            processed.insert(light);
111        }
112    }
113
114    for scheme in schemes {
115        if processed.contains(scheme.as_str()) {
116            continue;
117        }
118
119        let group_name = scheme.clone();
120        let variant = detect_variant_from_name(scheme);
121
122        grouped.insert(
123            group_name.clone(),
124            ThemeGroup {
125                name: group_name,
126                dark: if variant != SchemeVariant::Light {
127                    Some(scheme.clone())
128                } else {
129                    None
130                },
131                light: if variant == SchemeVariant::Light {
132                    Some(scheme.clone())
133                } else {
134                    None
135                },
136            },
137        );
138        processed.insert(scheme);
139    }
140
141    let mut groups: Vec<ThemeGroup> = grouped.into_values().collect();
142    groups.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
143    groups
144}
145
146fn derive_group_name(dark: &str, light: &str) -> String {
147    let dark_lower = dark.to_lowercase();
148    let light_lower = light.to_lowercase();
149
150    for suffix in [" dark", " light", " night", " day", " moon", " dawn"] {
151        if dark_lower.ends_with(suffix) {
152            return dark[..dark.len() - suffix.len()].to_string();
153        }
154        if light_lower.ends_with(suffix) {
155            return light[..light.len() - suffix.len()].to_string();
156        }
157    }
158
159    if dark.len() <= light.len() {
160        dark.to_string()
161    } else {
162        light.to_string()
163    }
164}
165
166fn detect_variant_from_name(name: &str) -> SchemeVariant {
167    let lower = name.to_lowercase();
168    if lower.contains("light") || lower.contains("day") || lower.contains("dawn") {
169        SchemeVariant::Light
170    } else if lower.contains("dark") || lower.contains("night") || lower.contains("moon") {
171        SchemeVariant::Dark
172    } else {
173        SchemeVariant::Unknown
174    }
175}
176
177pub fn fetch_and_cache_schemes(
178    cache_dir: &Path,
179    force: bool,
180) -> Result<CachedSchemeList, Iterm2Error> {
181    if !force {
182        if let Some(cached) = load_cached_schemes(cache_dir) {
183            if !cached.is_expired() {
184                return Ok(cached);
185            }
186        }
187    }
188
189    let schemes = list_available_schemes()?;
190    let groups = group_schemes(&schemes);
191
192    let timestamp = SystemTime::now()
193        .duration_since(UNIX_EPOCH)
194        .unwrap_or_default()
195        .as_secs();
196
197    let cache = CachedSchemeList {
198        timestamp,
199        schemes,
200        groups,
201    };
202
203    let _ = save_cached_schemes(cache_dir, &cache);
204
205    Ok(cache)
206}
207
208pub fn get_cached_or_empty(cache_dir: &Path) -> CachedSchemeList {
209    load_cached_schemes(cache_dir).unwrap_or_else(|| CachedSchemeList {
210        timestamp: 0,
211        schemes: Vec::new(),
212        groups: Vec::new(),
213    })
214}