From 4eef0caae5deb3d608d4670af6457288490f85eb Mon Sep 17 00:00:00 2001 From: Thomas Ruprecht Date: Tue, 1 Nov 2022 22:41:23 +0100 Subject: [PATCH] feat(service): prefer recently/often used applications in search --- Cargo.lock | 1 + service/Cargo.toml | 1 + service/src/lib.rs | 90 +++++++++++++++++++++++++++++------------ service/src/priority.rs | 59 +++++++++++++++++++++++++++ service/src/recent.rs | 90 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +++++ 6 files changed, 225 insertions(+), 25 deletions(-) create mode 100644 service/src/priority.rs create mode 100644 service/src/recent.rs diff --git a/Cargo.lock b/Cargo.lock index 64a2baa..5311e1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1122,6 +1122,7 @@ dependencies = [ "anyhow", "async-oneshot", "async-trait", + "dirs 4.0.0", "flume", "futures", "futures_codec", diff --git a/service/Cargo.toml b/service/Cargo.toml index 9bf7db7..eb8c444 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" anyhow = "1.0.56" async-oneshot = "0.5.0" async-trait = "0.1.53" +dirs = "4.0.0" futures = "0.3.21" futures_codec = "0.4.1" gen-z = "0.1.0" diff --git a/service/src/lib.rs b/service/src/lib.rs index 6d7d6f9..a495e8a 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -3,12 +3,16 @@ mod client; mod plugins; +mod recent; +mod priority; pub use client::*; pub use plugins::config; pub use plugins::external::load; use crate::plugins::*; +use crate::recent::RecentUseStorage; +use crate::priority::Priority; use flume::{Receiver, Sender}; use futures::{future, SinkExt, Stream, StreamExt}; use pop_launcher::*; @@ -16,7 +20,7 @@ use regex::Regex; use slab::Slab; use std::{ collections::{HashMap, HashSet}, - io::{self, Write}, + io::{self, Write}, path::PathBuf, }; pub type PluginKey = usize; @@ -34,7 +38,38 @@ pub struct PluginHelp { pub help: Option, } + +pub fn ensure_cache_path() -> Result> { + let cachepath = dirs::home_dir().ok_or("failed to find home dir")?.join(".cache/pop-launcher"); + std::fs::create_dir_all(&cachepath)?; + Ok(cachepath.join("recent")) +} + +pub fn store_cache(storage: &RecentUseStorage) { + let write_recent = || -> Result<(), Box> { + let cachepath = ensure_cache_path()?; + Ok(serde_json::to_writer( + std::fs::File::create(cachepath)?, + storage + )?) + }; + if let Err(e)= write_recent() { + eprintln!("could not write to cache file\n{}", e); + } +} + pub async fn main() { + let cachepath = ensure_cache_path(); + let read_recent = || -> Result> { + let cachepath = std::fs::File::open(cachepath?)?; + Ok(serde_json::from_reader(cachepath)?) + }; + let recent = match read_recent() { + Ok(r) => r, + Err(e) => {eprintln!("could not read cache file\n{}", e); RecentUseStorage::default()} + }; + + // Listens for a stream of requests from stdin. let input_stream = json_input_stream(tokio::io::stdin()).filter_map(|result| { future::ready(match result { @@ -49,7 +84,7 @@ pub async fn main() { let (output_tx, output_rx) = flume::bounded(16); // Service will operate for as long as it is being awaited - let service = Service::new(output_tx.into_sink()).exec(input_stream); + let service = Service::new(output_tx.into_sink(), recent).exec(input_stream); // Responses from the service will be streamed to stdout let responder = async move { @@ -73,10 +108,11 @@ pub struct Service { output: O, plugins: Slab, search_scheduled: bool, + recent: RecentUseStorage, } impl + Unpin> Service { - pub fn new(output: O) -> Self { + pub fn new(output: O, recent: RecentUseStorage) -> Self { Self { active_search: Vec::new(), associated_list: HashMap::new(), @@ -86,6 +122,7 @@ impl + Unpin> Service { no_sort: false, plugins: Slab::new(), search_scheduled: false, + recent, } } @@ -241,16 +278,24 @@ impl + Unpin> Service { } async fn activate(&mut self, id: Indice) { + let mut ex = None; if let Some((plugin, meta)) = self.search_result(id as usize) { + ex = meta.cache_identifier(); let _ = plugin .sender_exec() .send_async(Request::Activate(meta.id)) .await; } + if let Some(e) = ex { + self.recent.add(&e); + store_cache(&self.recent); + } } async fn activate_context(&mut self, id: Indice, context: Indice) { + let mut ex = None; if let Some((plugin, meta)) = self.search_result(id as usize) { + ex = meta.cache_identifier(); let _ = plugin .sender_exec() .send_async(Request::ActivateContext { @@ -259,6 +304,10 @@ impl + Unpin> Service { }) .await; } + if let Some(e) = ex { + self.recent.add(&e); + store_cache(&self.recent); + } } fn append(&mut self, plugin: PluginKey, append: PluginSearchResult) { @@ -445,6 +494,7 @@ impl + Unpin> Service { ref mut no_sort, ref last_query, ref plugins, + ref recent, .. } = self; @@ -497,23 +547,6 @@ impl + Unpin> Service { }) } - let a_weight = calculate_weight(&a.1, query); - let b_weight = calculate_weight(&b.1, query); - - match a_weight.partial_cmp(&b_weight) { - Some(Ordering::Equal) => { - let a_len = a.1.name.len(); - let b_len = b.1.name.len(); - - a_len.cmp(&b_len) - } - Some(Ordering::Less) => Ordering::Greater, - Some(Ordering::Greater) => Ordering::Less, - None => Ordering::Greater, - } - }); - - active_search.sort_by(|a, b| { let plug1 = match plugins.get(a.0) { Some(plug) => plug, None => return Ordering::Greater, @@ -524,11 +557,18 @@ impl + Unpin> Service { None => return Ordering::Less, }; - plug1 - .config - .query - .priority - .cmp(&plug2.config.query.priority) + let get_prio = |sr: &PluginSearchResult, plg: &PluginConnector| -> Priority { + let ex = sr.cache_identifier(); + Priority { + plugin_priority: plg.config.query.priority, + match_score: calculate_weight(sr, query), + recent_use_index: ex.as_ref().map(|s| recent.get_recent(s)).unwrap_or(0), + use_freq: ex.as_ref().map(|s| recent.get_freq(s)).unwrap_or(0), + execlen: sr.name.len() + } + }; + + get_prio(&b.1, plug2).cmp(&get_prio(&a.1, plug1)) }) } diff --git a/service/src/priority.rs b/service/src/priority.rs new file mode 100644 index 0000000..e14c52a --- /dev/null +++ b/service/src/priority.rs @@ -0,0 +1,59 @@ +use std::cmp::Ordering; + +use crate::PluginPriority; + + +// holds all values used for ordering search results +pub struct Priority { + pub plugin_priority: PluginPriority, + pub match_score: f64, + pub recent_use_index: usize, + pub use_freq: usize, + pub execlen: usize, +} + + +fn signum(val: i32) -> f64 { + if val > 0 { return 1.0; } + if val < 0 { return -1.0; } + 0.0 +} + +impl Priority { + fn compute_value(&self, other: &Self) -> f64{ + // increases compared jw-score if this search result + // was activated more frequent or recent by constant values + let score = self.match_score + + 0.06 * signum(self.recent_use_index as i32 - other.recent_use_index as i32) + + 0.03 * signum(self.use_freq as i32 - other.use_freq as i32); + // score cannot surpass exact matches + if self.match_score < 1.0 { + return score.min(0.99); + } + return score; + } +} + +impl PartialEq for Priority { + fn eq(&self, other: &Self) -> bool { + self.plugin_priority == other.plugin_priority + && self.compute_value(other) == other.match_score + && self.execlen == other.execlen + } +} + +impl Eq for Priority {} + +impl PartialOrd for Priority { + fn partial_cmp(&self, other: &Self) -> Option { + (other.plugin_priority, self.compute_value(other), self.execlen).partial_cmp( + &(self.plugin_priority, other.match_score, other.execlen) + ) + } +} + +impl Ord for Priority { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} \ No newline at end of file diff --git a/service/src/recent.rs b/service/src/recent.rs new file mode 100644 index 0000000..602b0b3 --- /dev/null +++ b/service/src/recent.rs @@ -0,0 +1,90 @@ +use std::collections::{HashMap, hash_map::DefaultHasher}; +use std::hash::{Hasher, Hash}; +use serde::{Deserialize, Serialize, Serializer, Deserializer}; + +const SHORTTERM_CAP: usize = 20; +const LONGTERM_CAP: usize = 100; + +// Holds a long term storage that tracks how often a search +// result was activated, and a short term storage that stores +// the order of recently activated search results (higher +// vales are more recent). +// Keys for both mappings are hashes of the acvtivated result's +// command string. +#[derive(Debug, Default)] +pub struct RecentUseStorage { + long_term: HashMap, + short_term: HashMap, +} + + +fn hash_key(key: K) -> usize { + let mut hasher = DefaultHasher::new(); + key.hash(&mut hasher); + hasher.finish() as usize +} + + +impl RecentUseStorage { + pub fn add(&mut self, exec: &K) { + let key = hash_key(exec); + *self.long_term.entry(key).or_insert(0) += 1; + let short_term_idx = self.short_term.values().max().unwrap_or( &0)+1; + self.short_term.insert(key, short_term_idx); + self.trim() + } + + fn trim(&mut self) { + while self.short_term.len() > SHORTTERM_CAP { + let key = *self.short_term.iter().min_by_key(|kv| kv.1).unwrap().0; + self.short_term.remove(&key); + } + + while self.long_term.values().sum::() > LONGTERM_CAP { + let mut delete_keys = Vec::new(); + for (k, v) in self.long_term.iter_mut() { + *v /= 2; + if *v == 0 { + delete_keys.push(*k); + } + } + for k in delete_keys { + self.long_term.remove(&k); + } + } + } + + pub fn get_recent(&self, exec: &K) -> usize { + self.short_term.get(&hash_key(exec)).copied().unwrap_or(0) + } + + pub fn get_freq(&self, exec: &K) -> usize { + self.long_term.get(&hash_key(exec)).copied().unwrap_or(0) + } +} + +impl Serialize for RecentUseStorage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut stvec: Vec<_> = self.short_term.keys().copied().collect(); + stvec.sort_by_key(|k| self.short_term[k]); + (&self.long_term, stvec).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for RecentUseStorage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + type SerType = (HashMap, Vec); + let (long_term, stv) = SerType::deserialize(deserializer)?; + let short_term: HashMap<_, _> = stv.into_iter().enumerate().map(|(v,k)| (k,v)).collect(); + Ok(RecentUseStorage { + long_term, + short_term, + }) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 169562f..7941a73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,15 @@ pub struct PluginSearchResult { pub window: Option<(Generation, Indice)>, } +impl PluginSearchResult { + pub fn cache_identifier(&self) -> Option { + // the exec field may clash in multiple search results as the arguments + // are cut from the string + // self.exec.to_owned().unwrap_or_else(|| self.name.to_owned()) + self.exec.as_ref().map(|_| self.name.to_owned()) + } +} + // Sent to the input pipe of the launcher service, and disseminated to its plugins. #[derive(Debug, Deserialize, Serialize, Clone)] pub enum Request {