diff --git a/plugins/applications/README.md b/plugins/applications/README.md index ab77fe41..1fa9c15d 100644 --- a/plugins/applications/README.md +++ b/plugins/applications/README.md @@ -19,5 +19,7 @@ Config( // The terminal used for running terminal based desktop entries, if left as `None` a static list of terminals is used // to determine what terminal to use. terminal: Some("alacritty"), + // The history size for the application history, set to 0 to disable history + history_size: 50, ) ``` \ No newline at end of file diff --git a/plugins/applications/src/history.rs b/plugins/applications/src/history.rs new file mode 100644 index 00000000..c9cded42 --- /dev/null +++ b/plugins/applications/src/history.rs @@ -0,0 +1,67 @@ +use std::collections::VecDeque; +use std::fs; +use std::env; + +use crate::scrubber::DesktopEntry; + + +pub struct History(VecDeque); + +impl History { + pub fn new() -> Self { + Self(VecDeque::new()) + } + + pub fn load() -> Self { + + let path = format!( + "{}/.cache/anyrun-applications-history", + env::var("HOME").expect("Unable to determine HOME directory") + ); + + if let Ok(content) = fs::read_to_string(&path) { + let history: VecDeque = ron::from_str(&content) + .unwrap_or_else(|why| { + eprintln!("Error parsing history: {}", why); + VecDeque::new() + }); + return Self(history); + } + + Self::new() + } + + pub fn write(&self) { + + let path = format!( + "{}/.cache/anyrun-applications-history", + env::var("HOME").expect("Unable to determine HOME directory") + ); + + let content = ron::to_string(&self.0).unwrap_or_else(|why| { + eprintln!("Error serializing history: {}", why); + String::new() + }); + if let Err(why) = fs::write(&path, content) { + eprintln!("Error writing history: {}", why); + } + } + + pub fn add_entry(&mut self, entry: DesktopEntry) { + self.0.push_front(entry); + } + + pub fn truncate(&mut self, max_entries: usize) { + self.0.truncate(max_entries); + } + + pub fn get_entry_info(&self, entry: &DesktopEntry) -> Option<(usize, usize)> { + let index = self.0.iter().position(|x| x == entry)?; + let count = self.0.iter().filter(|x| *x == entry).count(); + Some((index, count)) + } + pub fn count(&self) -> usize { + self.0.len() + } +} + diff --git a/plugins/applications/src/lib.rs b/plugins/applications/src/lib.rs index 30cebb85..fcca878b 100644 --- a/plugins/applications/src/lib.rs +++ b/plugins/applications/src/lib.rs @@ -10,6 +10,7 @@ pub struct Config { desktop_actions: bool, max_entries: usize, terminal: Option, + history_size: usize, } impl Default for Config { @@ -18,21 +19,24 @@ impl Default for Config { desktop_actions: false, max_entries: 5, terminal: None, + history_size: 50, } } } pub struct State { - config: Config, + config: Config, entries: Vec<(DesktopEntry, u64)>, + history: history::History, } mod scrubber; +mod history; const SENSIBLE_TERMINALS: &[&str] = &["alacritty", "foot", "kitty", "wezterm", "wterm"]; #[handler] -pub fn handler(selection: Match, state: &State) -> HandleResult { +pub fn handler(selection: Match, state: &mut State) -> HandleResult { let entry = state .entries .iter() @@ -74,6 +78,11 @@ pub fn handler(selection: Match, state: &State) -> HandleResult { eprintln!("Error running desktop entry: {}", why); } + state.history.add_entry(entry.clone()); + state.history.truncate(state.config.history_size); + state.history.write(); + + HandleResult::Close } @@ -95,11 +104,15 @@ pub fn init(config_dir: RString) -> State { Vec::new() }); - State { config, entries } + let history = history::History::load(); + println!("Loaded {} history entries", history.count()); + + State { config, entries, history } } #[get_matches] pub fn get_matches(input: RString, state: &State) -> RVec { + let matcher = fuzzy_matcher::skim::SkimMatcherV2::default().smart_case(); let mut entries = state .entries @@ -116,9 +129,18 @@ pub fn get_matches(input: RString, state: &State) -> RVec { .keywords .iter() .map(|keyword| matcher.fuzzy_match(keyword, &input).unwrap_or(0)) - .sum::(); + .sum::(); - let mut score = (app_score * 25 + keyword_score) - entry.offset; + let history_score = state.history.get_entry_info(entry).map(|(index, count)| { + let recency_bias = i64::max(5-index as i64, 0); + (count as i64 + recency_bias) * 20 + }).unwrap_or(0); + + if app_score + keyword_score == 0 { + return None; + } + + let mut score = (app_score * 25 + keyword_score + history_score) - entry.offset; // prioritize actions if entry.desc.is_some() { @@ -135,17 +157,17 @@ pub fn get_matches(input: RString, state: &State) -> RVec { entries.sort_by(|a, b| b.2.cmp(&a.2)); - entries.truncate(state.config.max_entries); + entries.truncate(state.config.max_entries); entries - .into_iter() - .map(|(entry, id, _)| Match { - title: entry.name.clone().into(), - description: entry.desc.clone().map(|desc| desc.into()).into(), - use_pango: false, - icon: ROption::RSome(entry.icon.clone().into()), - id: ROption::RSome(id), - }) - .collect() + .into_iter() + .map(|(entry, id, _)| Match { + title: entry.name.clone().into(), + description: entry.desc.clone().map(|desc| desc.into()).into(), + use_pango: false, + icon: ROption::RSome(entry.icon.clone().into()), + id: ROption::RSome(id), + }) + .collect() } #[info] diff --git a/plugins/applications/src/scrubber.rs b/plugins/applications/src/scrubber.rs index d72065c3..767ff1e0 100644 --- a/plugins/applications/src/scrubber.rs +++ b/plugins/applications/src/scrubber.rs @@ -1,8 +1,10 @@ use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; +use serde::{Deserialize, Serialize}; + use crate::Config; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct DesktopEntry { pub exec: String, pub path: Option,