use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use crate::domain::error::{Error, Result};
use crate::util::fs::write_atomic;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Usage {
pub last_used: Option<i64>,
pub open_count: u64,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct UsageDoc {
#[serde(default)]
entries: Vec<UsageEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct UsageEntry {
path: String,
last_used: Option<i64>,
#[serde(default)]
open_count: u64,
}
pub fn load(path: &Path) -> HashMap<PathBuf, Usage> {
let Ok(text) = fs::read_to_string(path) else {
return HashMap::new();
};
let Ok(doc) = toml::from_str::<UsageDoc>(&text) else {
return HashMap::new();
};
doc.entries
.into_iter()
.map(|entry| {
(
PathBuf::from(entry.path),
Usage {
last_used: entry.last_used,
open_count: entry.open_count,
},
)
})
.collect()
}
pub fn record(path: &Path, repo_path: &Path) -> Result<()> {
let mut usage = load(path);
let entry = usage.entry(repo_path.to_path_buf()).or_default();
entry.last_used = Some(Utc::now().timestamp());
entry.open_count += 1;
save(path, &usage)
}
fn save(path: &Path, usage: &HashMap<PathBuf, Usage>) -> Result<()> {
let mut entries: Vec<UsageEntry> = usage
.iter()
.map(|(path, usage)| UsageEntry {
path: path.to_string_lossy().into_owned(),
last_used: usage.last_used,
open_count: usage.open_count,
})
.collect();
entries.sort_by(|a, b| a.path.cmp(&b.path));
let doc = UsageDoc { entries };
let text = toml::to_string_pretty(&doc)
.map_err(|e| Error::invalid(format!("serialise usage: {e}")))?;
write_atomic(path, &text, "usage state")
}
pub fn write_selected_repo(dest: &Path, repo_path: &Path) -> Result<()> {
let line = format!("{}\n", repo_path.to_string_lossy());
write_atomic(dest, &line, "selected-repo file")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_bumps_count_and_stamps_time() {
let dir = std::env::temp_dir()
.join(format!("hop-usage-test-{}", std::process::id()));
let file = dir.join("usage.toml");
let repo = PathBuf::from("/code/hop");
record(&file, &repo).unwrap();
record(&file, &repo).unwrap();
let usage = load(&file);
let entry = usage.get(&repo).unwrap();
assert_eq!(entry.open_count, 2);
assert!(entry.last_used.is_some());
let _ = fs::remove_dir_all(&dir);
}
}