feat: implement frecency tracking for app usage and enhance search functionality
This commit is contained in:
134
crates/plugins/plugin-apps/src/frecency.rs
Normal file
134
crates/plugins/plugin-apps/src/frecency.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Entry {
|
||||
count: u32,
|
||||
last_used: u64,
|
||||
}
|
||||
|
||||
pub struct FrecencyStore {
|
||||
path: PathBuf,
|
||||
data: Mutex<HashMap<String, Entry>>,
|
||||
}
|
||||
|
||||
impl FrecencyStore {
|
||||
pub fn new(path: PathBuf) -> Arc<Self> {
|
||||
let data = std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
Arc::new(Self { path, data: Mutex::new(data) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_for_test() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
path: PathBuf::from("/dev/null"),
|
||||
data: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load() -> Arc<Self> {
|
||||
let path = xdg::BaseDirectories::new()
|
||||
.map(|xdg| xdg.get_data_home())
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("k-launcher")
|
||||
.join("frecency.json");
|
||||
Self::new(path)
|
||||
}
|
||||
|
||||
pub fn record(&self, id: &str) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let mut data = self.data.lock().unwrap();
|
||||
let entry = data.entry(id.to_string()).or_insert(Entry { count: 0, last_used: 0 });
|
||||
entry.count += 1;
|
||||
entry.last_used = now;
|
||||
if let Some(parent) = self.path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Ok(json) = serde_json::to_string(&*data) {
|
||||
let _ = std::fs::write(&self.path, json);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frecency_score(&self, id: &str) -> u32 {
|
||||
let data = self.data.lock().unwrap();
|
||||
let Some(entry) = data.get(id) else { return 0 };
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let age_secs = now.saturating_sub(entry.last_used);
|
||||
let decay = if age_secs < 3600 { 4 } else if age_secs < 86400 { 2 } else { 1 };
|
||||
entry.count * decay
|
||||
}
|
||||
|
||||
pub fn top_ids(&self, n: usize) -> Vec<String> {
|
||||
let data = self.data.lock().unwrap();
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let mut scored: Vec<(String, u32)> = data
|
||||
.iter()
|
||||
.map(|(id, entry)| {
|
||||
let age_secs = now.saturating_sub(entry.last_used);
|
||||
let decay = if age_secs < 3600 { 4 } else if age_secs < 86400 { 2 } else { 1 };
|
||||
(id.clone(), entry.count * decay)
|
||||
})
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
scored.into_iter().take(n).map(|(id, _)| id).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_store() -> Arc<FrecencyStore> {
|
||||
Arc::new(FrecencyStore {
|
||||
path: PathBuf::from("/dev/null"),
|
||||
data: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_increments_count() {
|
||||
let store = make_store();
|
||||
store.record("app-firefox");
|
||||
store.record("app-firefox");
|
||||
let data = store.data.lock().unwrap();
|
||||
assert_eq!(data["app-firefox"].count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_updates_last_used() {
|
||||
let store = make_store();
|
||||
store.record("app-firefox");
|
||||
let data = store.data.lock().unwrap();
|
||||
assert!(data["app-firefox"].last_used > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_ids_returns_sorted_order() {
|
||||
let store = make_store();
|
||||
store.record("app-firefox");
|
||||
store.record("app-code");
|
||||
store.record("app-code");
|
||||
store.record("app-code");
|
||||
let top = store.top_ids(2);
|
||||
assert_eq!(top[0], "app-code");
|
||||
assert_eq!(top[1], "app-firefox");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user