use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::fs_util;
#[derive(Debug, Clone)]
pub struct HistoryEntry {
pub alias: String,
pub last_connected: u64,
pub count: u32,
}
#[derive(Debug, Clone, Default)]
pub struct ConnectionHistory {
pub entries: HashMap<String, HistoryEntry>,
path: PathBuf,
}
impl ConnectionHistory {
pub fn load() -> Self {
let path = match Self::history_path() {
Some(p) => p,
None => return Self::default(),
};
if !path.exists() {
return Self {
entries: HashMap::new(),
path,
};
}
let content = fs::read_to_string(&path).unwrap_or_default();
let mut entries = HashMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.splitn(3, '\t').collect();
if parts.len() == 3 {
if let (Ok(ts), Ok(count)) = (parts[1].parse::<u64>(), parts[2].parse::<u32>()) {
entries.insert(
parts[0].to_string(),
HistoryEntry {
alias: parts[0].to_string(),
last_connected: ts,
count,
},
);
}
}
}
Self { entries, path }
}
pub fn record(&mut self, alias: &str) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let entry = self
.entries
.entry(alias.to_string())
.or_insert(HistoryEntry {
alias: alias.to_string(),
last_connected: 0,
count: 0,
});
entry.last_connected = now;
entry.count += 1;
let _ = self.save();
}
pub fn last_connected(&self, alias: &str) -> u64 {
self.entries.get(alias).map_or(0, |e| e.last_connected)
}
pub fn frecency_score(&self, alias: &str) -> f64 {
let entry = match self.entries.get(alias) {
Some(e) => e,
None => return 0.0,
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let age_hours = (now.saturating_sub(entry.last_connected)) as f64 / 3600.0;
let recency = 1.0 / (1.0 + age_hours / 24.0);
entry.count as f64 * recency
}
pub fn format_time_ago(timestamp: u64) -> String {
if timestamp == 0 {
return String::new();
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let diff = now.saturating_sub(timestamp);
if diff < 60 {
"<1m ago".to_string()
} else if diff < 3600 {
format!("{}m ago", diff / 60)
} else if diff < 86400 {
format!("{}h ago", diff / 3600)
} else if diff < 604800 {
format!("{}d ago", diff / 86400)
} else {
format!("{}w ago", diff / 604800)
}
}
fn save(&self) -> std::io::Result<()> {
let mut sorted: Vec<_> = self.entries.values().collect();
sorted.sort_by(|a, b| a.alias.cmp(&b.alias));
let mut content = String::new();
for (i, e) in sorted.iter().enumerate() {
if i > 0 {
content.push('\n');
}
content.push_str(&e.alias);
content.push('\t');
content.push_str(&e.last_connected.to_string());
content.push('\t');
content.push_str(&e.count.to_string());
}
fs_util::atomic_write(&self.path, content.as_bytes())
}
fn history_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".purple/history.tsv"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frecency_score_unknown_alias() {
let history = ConnectionHistory::default();
assert_eq!(history.frecency_score("unknown"), 0.0);
}
#[test]
fn test_format_time_ago_zero() {
assert_eq!(ConnectionHistory::format_time_ago(0), "");
}
#[test]
fn test_format_time_ago_recent() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
assert_eq!(ConnectionHistory::format_time_ago(now), "<1m ago");
assert_eq!(
ConnectionHistory::format_time_ago(now - 300),
"5m ago"
);
assert_eq!(
ConnectionHistory::format_time_ago(now - 7200),
"2h ago"
);
assert_eq!(
ConnectionHistory::format_time_ago(now - 172800),
"2d ago"
);
}
}