1use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13const API_LIST_URL: &str = "https://www.toptal.com/developers/gitignore/api/list?format=json";
14const API_TEMPLATE_URL_BASE: &str = "https://www.toptal.com/developers/gitignore/api/";
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
18#[serde(default)]
19pub struct ManagerConfig {
20 pub last_updated: u64,
22 pub cache_duration: u64,
24}
25
26impl Default for ManagerConfig {
27 fn default() -> Self {
28 Self {
29 last_updated: 0,
30 cache_duration: 86_400, }
32 }
33}
34
35#[derive(Debug, Serialize, Deserialize, Clone)]
37pub struct Template {
38 pub key: String,
40 pub name: String,
42 pub contents: String,
44}
45
46#[derive(Debug)]
48pub struct TemplateManager {
49 config_path: PathBuf,
50 templates_path: PathBuf,
51 config: ManagerConfig,
52 templates: HashMap<String, Template>,
53}
54
55impl TemplateManager {
56 pub fn new() -> Result<Self> {
75 let home_dir = dirs::home_dir().context("Could not determine home directory")?;
76 let flatten_dir = home_dir.join(".flatten");
77
78 std::fs::create_dir_all(&flatten_dir).context("Failed to create .flatten directory")?;
79
80 let config_path = flatten_dir.join("manager_config.json");
81 let templates_path = flatten_dir.join("templates_cache.json");
82
83 let mut manager = Self {
84 config_path,
85 templates_path,
86 config: ManagerConfig::default(),
87 templates: HashMap::new(),
88 };
89
90 manager.load_config()?;
91 manager.load_templates()?;
92
93 Ok(manager)
94 }
95
96 fn load_config(&mut self) -> Result<()> {
98 if self.config_path.exists() {
99 let content = std::fs::read_to_string(&self.config_path)
100 .context("Failed to read config file")?;
101 self.config =
102 serde_json::from_str(&content).context("Failed to parse config file")?;
103 } else {
104 self.save_config()?;
105 }
106 Ok(())
107 }
108
109 fn save_config(&self) -> Result<()> {
111 let content =
112 serde_json::to_string_pretty(&self.config).context("Failed to serialize config")?;
113 std::fs::write(&self.config_path, content).context("Failed to write config file")?;
114 Ok(())
115 }
116
117 fn load_templates(&mut self) -> Result<()> {
119 if self.templates_path.exists() {
120 let content = std::fs::read_to_string(&self.templates_path)
121 .context("Failed to read templates file")?;
122 self.templates =
123 serde_json::from_str(&content).context("Failed to parse templates file")?;
124 }
125 Ok(())
126 }
127
128 fn save_templates(&self) -> Result<()> {
130 let content =
131 serde_json::to_string_pretty(&self.templates).context("Failed to serialize templates")?;
132 std::fs::write(&self.templates_path, content).context("Failed to write templates file")?;
133 Ok(())
134 }
135
136 fn needs_update(&self) -> bool {
138 let current_time = SystemTime::now()
139 .duration_since(UNIX_EPOCH)
140 .expect("Time went backwards")
141 .as_secs();
142 current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
143 }
144
145 pub async fn update_if_needed(&mut self) -> Result<()> {
147 if self.needs_update() || self.templates.is_empty() {
148 println!("🔄 Updating exclusion templates...");
149 if let Err(e) = self.fetch_templates().await {
150 eprintln!("Warning: Failed to update templates: {}. Using cached version if available.", e);
151 } else {
152 println!("✅ Templates updated successfully");
153 }
154 }
155 Ok(())
156 }
157
158 pub async fn force_update(&mut self) -> Result<()> {
160 self.config.last_updated = 0; println!("🔄 Force updating exclusion templates...");
162 match self.fetch_templates().await {
163 Ok(()) => {
164 println!("✅ Templates updated successfully");
165 Ok(())
166 },
167 Err(e) => {
168 eprintln!("Error: Failed to update templates: {}", e);
169 Err(e)
170 }
171 }
172 }
173
174 async fn fetch_templates(&mut self) -> Result<()> {
176 let client = reqwest::Client::new();
177 let list_response = client.get(API_LIST_URL).send().await?.text().await?;
178 let template_keys: Vec<&str> = list_response.lines().collect();
179
180 for key in template_keys {
181 let template_url = format!("{}{}", API_TEMPLATE_URL_BASE, key);
182 match client.get(&template_url).send().await {
183 Ok(response) => {
184 if let Ok(content) = response.text().await {
185 let template = Template {
186 key: key.to_string(),
187 name: key.to_string(),
188 contents: content,
189 };
190 self.templates.insert(key.to_string(), template);
191 }
192 }
193 Err(e) => {
194 eprintln!("Warning: Failed to fetch template '{}': {}", key, e);
195 }
196 }
197 }
198
199 self.config.last_updated = SystemTime::now()
200 .duration_since(UNIX_EPOCH)
201 .expect("Time went backwards")
202 .as_secs();
203
204 self.save_templates()?;
205 self.save_config()?;
206 Ok(())
207 }
208
209 pub fn get_available_templates(&self) -> Vec<String> {
211 self.templates.keys().cloned().collect()
212 }
213
214 pub fn get_template_contents(&self, key: &str) -> Option<&str> {
216 self.templates.get(key).map(|t| t.contents.as_str())
217 }
218}