use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
const API_LIST_URL: &str = "https://www.toptal.com/developers/gitignore/api/list?format=json";
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct ManagerConfig {
pub last_updated: u64,
pub cache_duration: u64,
}
impl Default for ManagerConfig {
fn default() -> Self {
Self {
last_updated: 0,
cache_duration: 86_400, }
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Template {
pub key: String,
pub name: String,
pub contents: String,
}
#[derive(Debug, Deserialize)]
struct ToptalEntry {
name: String,
#[serde(default)]
contents: String,
}
#[derive(Debug)]
pub struct TemplateManager {
config_path: PathBuf,
templates_path: PathBuf,
config: ManagerConfig,
templates: HashMap<String, Template>,
}
impl TemplateManager {
pub fn new() -> Result<Self> {
let home_dir = dirs::home_dir().context("Could not determine home directory")?;
let flatten_dir = home_dir.join(".flatten");
std::fs::create_dir_all(&flatten_dir).context("Failed to create .flatten directory")?;
let config_path = flatten_dir.join("manager_config.json");
let templates_path = flatten_dir.join("templates_cache.json");
let mut manager = Self {
config_path,
templates_path,
config: ManagerConfig::default(),
templates: HashMap::new(),
};
manager.load_config()?;
manager.load_templates()?;
Ok(manager)
}
fn load_config(&mut self) -> Result<()> {
if self.config_path.exists() {
let content = std::fs::read_to_string(&self.config_path)
.context("Failed to read config file")?;
self.config = serde_json::from_str(&content).unwrap_or_default();
} else {
self.save_config()?;
}
Ok(())
}
fn save_config(&self) -> Result<()> {
let content =
serde_json::to_string_pretty(&self.config).context("Failed to serialize config")?;
std::fs::write(&self.config_path, content).context("Failed to write config file")?;
Ok(())
}
fn load_templates(&mut self) -> Result<()> {
if self.templates_path.exists() {
let content = std::fs::read_to_string(&self.templates_path)
.context("Failed to read templates file")?;
self.templates = serde_json::from_str(&content).unwrap_or_default();
}
Ok(())
}
fn save_templates(&self) -> Result<()> {
let content =
serde_json::to_string_pretty(&self.templates).context("Failed to serialize templates")?;
std::fs::write(&self.templates_path, content).context("Failed to write templates file")?;
Ok(())
}
fn needs_update(&self) -> bool {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
if self.templates.is_empty() {
return true;
}
current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
}
pub async fn update_if_needed(&mut self) -> Result<()> {
if !self.needs_update() {
return Ok(());
}
match self.fetch_templates().await {
Ok(()) => {
},
Err(e) => {
if self.templates.is_empty() {
return Err(e.context("Failed to fetch initial templates and cache is empty"));
} else {
}
}
}
Ok(())
}
pub async fn force_update(&mut self) -> Result<()> {
println!("🔄 Force updating exclusion templates from API...");
match self.fetch_templates().await {
Ok(_) => {
println!("✅ Templates updated successfully");
Ok(())
}
Err(e) => {
Err(e)
}
}
}
async fn fetch_templates(&mut self) -> Result<()> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
let response = client.get(API_LIST_URL)
.send()
.await
.context("Failed to connect to templates API")?;
let api_data: HashMap<String, ToptalEntry> = response
.json()
.await
.context("Failed to parse templates JSON")?;
if api_data.is_empty() {
return Err(anyhow::anyhow!("Received empty templates list from API"));
}
self.templates = api_data
.into_iter()
.map(|(key, entry)| {
(
key.clone(),
Template {
key,
name: entry.name,
contents: entry.contents,
},
)
})
.collect();
self.config.last_updated = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
self.save_templates()?;
self.save_config()?;
Ok(())
}
pub fn get_available_templates(&self) -> Vec<String> {
self.templates.keys().cloned().collect()
}
pub fn get_template_contents(&self, key: &str) -> Option<&str> {
self.templates.get(key).map(|t| t.contents.as_str())
}
}