use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub struct Frecency {
entries: HashMap<u32, u32>,
path: PathBuf,
dirty: bool,
}
impl Frecency {
pub fn load() -> Self {
let path = Self::path();
let entries = Self::read(&path);
Self {
entries,
path,
dirty: false,
}
}
pub fn get(&self, codepoint: u32) -> u32 {
self.entries.get(&codepoint).copied().unwrap_or(0)
}
pub fn record(&mut self, codepoint: u32) {
*self.entries.entry(codepoint).or_insert(0) += 1;
self.dirty = true;
}
pub fn flush(&mut self) -> io::Result<()> {
if self.dirty {
self.write()?;
self.dirty = false;
}
Ok(())
}
}
impl Drop for Frecency {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[cfg(test)]
impl Frecency {
pub fn empty_for_testing() -> Self {
Self {
entries: HashMap::new(),
path: PathBuf::new(),
dirty: false,
}
}
}
impl Frecency {
pub fn path() -> PathBuf {
let base = std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_default();
if home.is_empty() {
return std::env::temp_dir();
}
PathBuf::from(home).join(".local/share")
});
let dir = base.join("glyf");
fs::create_dir_all(&dir).ok();
dir.join("frecency.bin")
}
fn read(path: &Path) -> HashMap<u32, u32> {
let Ok(bytes) = fs::read(path) else {
return HashMap::new();
};
if bytes.len() % 8 != 0 {
return HashMap::new();
}
bytes
.chunks_exact(8)
.map(|c| {
let mut buf = [0u8; 4];
buf.copy_from_slice(&c[..4]);
let cp = u32::from_le_bytes(buf);
buf.copy_from_slice(&c[4..8]);
let count = u32::from_le_bytes(buf);
(cp, count)
})
.collect()
}
fn write(&self) -> io::Result<()> {
let mut sorted: Vec<(u32, u32)> = self.entries.iter().map(|(&k, &v)| (k, v)).collect();
sorted.sort_unstable_by_key(|&(cp, _)| cp);
let mut bytes = Vec::with_capacity(sorted.len() * 8);
for &(cp, count) in &sorted {
bytes.extend_from_slice(&cp.to_le_bytes());
bytes.extend_from_slice(&count.to_le_bytes());
}
let tmp = self.path.with_extension("tmp");
fs::write(&tmp, &bytes)?;
fs::rename(&tmp, &self.path)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_frecency_returns_zero() {
let f = Frecency::empty_for_testing();
assert_eq!(f.get(0x0041), 0);
}
#[test]
fn record_then_get() {
let mut f = Frecency::empty_for_testing();
f.record(0x0041);
assert_eq!(f.get(0x0041), 1);
}
#[test]
fn record_multiple_times() {
let mut f = Frecency::empty_for_testing();
for _ in 0..5 {
f.record(0x0041);
}
assert_eq!(f.get(0x0041), 5);
}
#[test]
fn record_sets_dirty() {
let mut f = Frecency::empty_for_testing();
f.record(0x0041);
assert!(f.dirty);
}
#[test]
fn flush_noop_when_not_dirty() {
let mut f = Frecency::empty_for_testing();
assert!(f.flush().is_ok());
}
#[test]
fn record_multiple_codepoints() {
let mut f = Frecency::empty_for_testing();
f.record(0x0041);
f.record(0x1F600);
assert_eq!(f.get(0x0041), 1);
assert_eq!(f.get(0x1F600), 1);
assert_eq!(f.get(0x0042), 0);
}
}