use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Copy, Default)]
pub struct Entry {
pub last: u64,
pub count: u32,
}
#[derive(Default)]
pub struct History {
entries: HashMap<String, Entry>,
}
impl History {
pub fn get(&self, key: &str) -> Option<Entry> {
self.entries.get(key).copied()
}
pub fn rank(&self, key: &str) -> (i64, String) {
let last = self.get(key).map(|e| e.last).unwrap_or(0);
(-(last as i64), key.to_string())
}
}
pub fn path() -> PathBuf {
let base = dirs::state_dir()
.or_else(dirs::data_dir)
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/state"));
base.join("easysql").join("history")
}
pub fn load() -> History {
load_from(&path())
}
pub fn record(key: &str) {
let p = path();
if let Some(dir) = p.parent() {
let _ = fs::create_dir_all(dir);
}
let _ = record_in(&p, key, now());
}
pub fn rename(old_key: &str, new_key: &str) {
let _ = rename_in(&path(), old_key, new_key);
}
pub fn rename_in(path: &Path, old_key: &str, new_key: &str) -> std::io::Result<()> {
if old_key == new_key {
return Ok(());
}
let mut h = load_from(path);
let Some(e) = h.entries.remove(old_key) else {
return Ok(());
};
let slot = h.entries.entry(new_key.to_string()).or_default();
slot.last = slot.last.max(e.last);
slot.count += e.count;
write_all(path, &h)
}
pub fn load_from(path: &Path) -> History {
let mut entries = HashMap::new();
let Ok(text) = fs::read_to_string(path) else {
return History { entries };
};
for line in text.lines() {
let mut f = line.split_whitespace();
let (Some(last), Some(count), Some(key)) = (f.next(), f.next(), f.next()) else {
continue;
};
let (Ok(last), Ok(count)) = (last.parse(), count.parse()) else {
continue;
};
entries.insert(key.to_string(), Entry { last, count });
}
History { entries }
}
pub fn record_in(path: &Path, key: &str, at: u64) -> std::io::Result<()> {
let mut h = load_from(path);
let e = h.entries.entry(key.to_string()).or_default();
e.last = at;
e.count += 1;
write_all(path, &h)
}
fn write_all(path: &Path, h: &History) -> std::io::Result<()> {
let mut rows: Vec<(&String, &Entry)> = h.entries.iter().collect();
rows.sort_by(|a, b| a.0.cmp(b.0));
let body: String = rows
.iter()
.map(|(key, e)| format!("{} {} {}\n", e.last, e.count, key))
.collect();
fs::write(path, body)
}
pub fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn ago(then: u64, now: u64) -> String {
let secs = now.saturating_sub(then);
match secs {
0..=59 => "just now".to_string(),
60..=3599 => format!("{}m ago", secs / 60),
3600..=86399 => format!("{}h ago", secs / 3600),
86400..=2591999 => format!("{}d ago", secs / 86400),
2592000..=31535999 => format!("{}mo ago", secs / 2592000),
_ => format!("{}y ago", secs / 31536000),
}
}