1use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct GitignoreConfig {
15 pub last_updated: u64,
17 pub cache_duration: u64,
19 pub user_overrides: HashMap<String, Vec<String>>,
21 pub check_internet: bool,
23}
24
25impl Default for GitignoreConfig {
26 fn default() -> Self {
27 Self {
28 last_updated: 0,
29 cache_duration: 86400, user_overrides: HashMap::new(),
31 check_internet: true,
32 }
33 }
34}
35
36#[derive(Debug, Serialize, Deserialize, Clone)]
38pub struct GitignoreTemplate {
39 pub key: String,
40 pub name: String,
41 pub contents: String,
42 pub file_name: String,
43}
44
45pub struct GitignoreManager {
47 config_path: PathBuf,
48 templates_path: PathBuf,
49 config: GitignoreConfig,
50 templates: HashMap<String, GitignoreTemplate>,
51}
52
53impl GitignoreManager {
54 pub fn new() -> Result<Self> {
60 let home_dir = dirs::home_dir()
61 .context("Could not determine home directory")?;
62 let flatten_dir = home_dir.join(".flatten");
63
64 if !flatten_dir.exists() {
66 std::fs::create_dir_all(&flatten_dir)
67 .context("Failed to create .flatten directory")?;
68 }
69
70 let config_path = flatten_dir.join("config.json");
71 let templates_path = flatten_dir.join("templates.json");
72
73 let mut manager = Self {
74 config_path,
75 templates_path,
76 config: GitignoreConfig::default(),
77 templates: HashMap::new(),
78 };
79
80 manager.load_config()?;
82
83 manager.load_templates()?;
85
86 Ok(manager)
87 }
88
89 pub async fn check_internet_connectivity(&self) -> bool {
104 let client = reqwest::Client::builder()
106 .timeout(std::time::Duration::from_secs(5))
107 .build();
108
109 match client {
110 Ok(client) => {
111 match client.get("https://www.google.com/generate_204").send().await {
112 Ok(response) => response.status().is_success(),
113 Err(_) => false,
114 }
115 }
116 Err(_) => false,
117 }
118 }
119
120 fn load_config(&mut self) -> Result<()> {
122 if self.config_path.exists() {
123 let content = std::fs::read_to_string(&self.config_path)
124 .context("Failed to read config file")?;
125 self.config = serde_json::from_str(&content)
126 .context("Failed to parse config file")?;
127 } else {
128 self.save_config()?;
130 }
131 Ok(())
132 }
133
134 fn save_config(&self) -> Result<()> {
136 let content = serde_json::to_string_pretty(&self.config)
137 .context("Failed to serialize config")?;
138 std::fs::write(&self.config_path, content)
139 .context("Failed to write config file")?;
140 Ok(())
141 }
142
143 fn load_templates(&mut self) -> Result<()> {
145 if self.templates_path.exists() {
146 let content = std::fs::read_to_string(&self.templates_path)
147 .context("Failed to read templates file")?;
148 self.templates = serde_json::from_str(&content)
149 .context("Failed to parse templates file")?;
150 }
151 Ok(())
152 }
153
154 fn save_templates(&self) -> Result<()> {
156 let content = serde_json::to_string_pretty(&self.templates)
157 .context("Failed to serialize templates")?;
158 std::fs::write(&self.templates_path, content)
159 .context("Failed to write templates file")?;
160 Ok(())
161 }
162
163 fn needs_update(&self) -> bool {
165 let current_time = SystemTime::now()
166 .duration_since(UNIX_EPOCH)
167 .unwrap_or_default()
168 .as_secs();
169
170 current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
171 }
172
173 async fn fetch_templates(&mut self) -> Result<()> {
175 let client = reqwest::Client::new();
176
177 let list_url = "https://www.toptal.com/developers/gitignore/api/list?format=json";
179 let list_response = client.get(list_url)
180 .send()
181 .await
182 .context("Failed to fetch template list")?;
183
184 let template_list: HashMap<String, serde_json::Value> = list_response
185 .json()
186 .await
187 .context("Failed to parse template list")?;
188
189 for (key, _) in template_list {
191 let template_url = format!("https://www.toptal.com/developers/gitignore/api/{}", key);
192
193 match client.get(&template_url).send().await {
194 Ok(response) => {
195 if let Ok(content) = response.text().await {
196 let template = GitignoreTemplate {
197 key: key.clone(),
198 name: key.clone(), contents: content,
200 file_name: format!("{}.gitignore", key),
201 };
202 self.templates.insert(key, template);
203 }
204 }
205 Err(e) => {
206 eprintln!("Warning: Failed to fetch template {}: {}", key, e);
207 }
208 }
209
210 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
212 }
213
214 self.config.last_updated = SystemTime::now()
216 .duration_since(UNIX_EPOCH)
217 .unwrap_or_default()
218 .as_secs();
219
220 self.save_templates()?;
222 self.save_config()?;
223
224 Ok(())
225 }
226
227 pub async fn update_if_needed(&mut self) -> Result<()> {
240 if self.needs_update() {
241 println!("🔄 Updating gitignore templates...");
242
243 if self.config.check_internet && !self.check_internet_connectivity().await {
245 println!("⚠️ No internet connection available, using cached templates");
246 return Ok(());
247 }
248
249 if let Err(e) = self.fetch_templates().await {
250 eprintln!("Warning: Failed to update templates: {}", e);
251 if self.templates.is_empty() {
253 return Err(e);
254 }
255 } else {
256 println!("✅ Templates updated successfully");
257 }
258 }
259 Ok(())
260 }
261
262 pub fn get_available_templates(&self) -> Vec<&str> {
264 self.templates.keys().map(|k| k.as_str()).collect()
265 }
266
267 pub fn get_patterns_for_templates(&self, template_keys: &[String]) -> Vec<String> {
269 let mut patterns = Vec::new();
270
271 for key in template_keys {
272 if let Some(template) = self.templates.get(key) {
273 let template_patterns = self.parse_gitignore_patterns(&template.contents);
275 patterns.extend(template_patterns);
276 }
277 }
278
279 for override_patterns in self.config.user_overrides.values() {
281 patterns.extend(override_patterns.clone());
282 }
283
284 patterns
285 }
286
287 fn parse_gitignore_patterns(&self, content: &str) -> Vec<String> {
289 content
290 .lines()
291 .filter(|line| {
292 !line.trim().is_empty() && !line.trim().starts_with('#') && !line.trim().starts_with("###")
294 })
295 .map(|line| line.trim().to_string())
296 .collect()
297 }
298
299
300
301 pub fn set_check_internet(&mut self, check: bool) -> Result<()> {
317 self.config.check_internet = check;
318 self.save_config()?;
319 Ok(())
320 }
321
322 pub async fn force_update(&mut self) -> Result<()> {
335 self.config.last_updated = 0; self.update_if_needed().await
337 }
338}