From 5311409b40caffd39678b7ceedcf9161103f9837 Mon Sep 17 00:00:00 2001 From: Simon Struck Date: Mon, 22 Jul 2024 22:28:52 +0200 Subject: [PATCH 1/2] feat: add usage statistics for applications to improve search results --- README.md | 22 ++++++++---- src/execution_stats.rs | 78 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 48 +++++++++++++++++++++++--- 3 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 src/execution_stats.rs diff --git a/README.md b/README.md index ab77fe4..582e1ba 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,19 @@ Simply search for the application you wish to launch. ```ron // /applications.ron Config( - // Also show the Desktop Actions defined in the desktop files, e.g. "New Window" from LibreWolf - desktop_actions: true, - max_entries: 5, - // 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"), + // Limit amount of entries shown by the applications plugin (default: 5) + max_entries: 5, + // Whether to evaluate desktop actions as well as desktop applications (default: false) + desktop_actions: false, + // Whether to use a specific terminal or just the first terminal available (default: None) + terminal: None, + // Whether or not to put more often used applications higher in the search rankings (default: true) + use_usage_statistics: true, + // How much score to add for every usage of an application (default: 50) + // Each matching letter is 25 points + usage_score_multiplier: 50, + // Maximum amount of usages to count (default: 10) + // This is to limit the added score, so often used apps don't get too big of a boost + max_counted_usages: 10, ) -``` \ No newline at end of file +``` diff --git a/src/execution_stats.rs b/src/execution_stats.rs new file mode 100644 index 0000000..f412d50 --- /dev/null +++ b/src/execution_stats.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; +use std::fs; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use crate::Config; +use crate::scrubber::DesktopEntry; + +pub(crate) struct ExecutionStats { + weight_map: Arc>>, + max_weight: i64, + execution_statistics_path: String, +} + +impl ExecutionStats { + pub(crate) fn from_file_or_default(execution_statistics_path: &str, config: &Config) -> Self { + let execution_statistics: HashMap = fs::read_to_string(execution_statistics_path) + .map_err(|error| format!("Error parsing applications plugin config: {}", error)) + .and_then(|content: String| ron::from_str(&content) + .map_err(|error| format!("Error reading applications plugin config: {}", error))) + .unwrap_or_else(|error_message| { + format!("{}", error_message); + HashMap::new() + }); + + ExecutionStats { + weight_map: Arc::new(Mutex::new(execution_statistics)), + max_weight: config.max_counted_usages, + execution_statistics_path: execution_statistics_path.to_owned(), + } + } + + pub(crate) fn save(&self) -> Result<(), String> { + let path = Path::new(&self.execution_statistics_path); + if let Some(containing_folder) = path.parent() { + if !containing_folder.exists() { + fs::create_dir_all(containing_folder) + .map_err(|error| format!("Error creating containing folder for usage statistics: {:?}", error))?; + } + let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path) + .map_err(|error| format!("Error creating data file for usage statistics: {:?}", error))?; + let weight_map = self.weight_map.lock() + .map_err(|error| format!("Error locking file for usage statistics: {:?}", error))?; + let serialized_data = ron::to_string(&*weight_map) + .map_err(|error| format!("Error serializing usage statistics: {:?}", error))?; + file.write_all(serialized_data.as_bytes()) + .map_err(|error| format!("Error writing data file for usage statistics: {:?}", error)) + } else { + Err(format!("Error getting parent folder of: {:?}", path)) + } + } + + pub(crate) fn register_usage(&self, application: &DesktopEntry) { + { + let mut guard = self.weight_map.lock().unwrap(); + if let Some(count) = guard.get_mut(&application.exec) { + *count += 1; + } else { + guard.insert(application.exec.clone(), 1); + } + } + if let Err(error_message) = self.save() { + eprintln!("{}", error_message); + } + } + + pub(crate) fn get_weight(&self, application: &DesktopEntry) -> i64 { + let weight = *self.weight_map.lock().unwrap().get(&application.exec).unwrap_or(&0); + + if weight < self.max_weight { + weight + } else { + self.max_weight + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1db5daf..d22b556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,30 @@ +use std::{env, fs, process::Command}; + use abi_stable::std_types::{ROption, RString, RVec}; -use anyrun_plugin::{anyrun_interface::HandleResult, *}; use fuzzy_matcher::FuzzyMatcher; -use scrubber::DesktopEntry; use serde::Deserialize; -use std::{env, fs, process::Command}; + +use anyrun_plugin::{*, anyrun_interface::HandleResult}; +use scrubber::DesktopEntry; + +use crate::execution_stats::ExecutionStats; #[derive(Deserialize)] pub struct Config { - desktop_actions: bool, + /// Limit amount of entries shown by the applications plugin (default: 5) max_entries: usize, + /// Whether to evaluate desktop actions as well as desktop applications (default: false) + desktop_actions: bool, + /// Whether to use a specific terminal or just the first terminal available (default: None) terminal: Option, + /// Whether to put more often used applications higher in the search rankings (default: true) + use_usage_statistics: bool, + /// How much score to add for every usage of an application (default: 50) + /// Each matching letter is 25 points + usage_score_multiplier: i64, + /// Maximum amount of usages to count (default: 10) + /// This is to limit the added score, so often used apps don't get too big of a boost + max_counted_usages: i64, } impl Default for Config { @@ -18,6 +33,9 @@ impl Default for Config { desktop_actions: false, max_entries: 5, terminal: None, + use_usage_statistics: true, + usage_score_multiplier: 50, + max_counted_usages: 10, } } } @@ -25,9 +43,11 @@ impl Default for Config { pub struct State { config: Config, entries: Vec<(DesktopEntry, u64)>, + execution_stats: Option, } mod scrubber; +mod execution_stats; const SENSIBLE_TERMINALS: &[&str] = &["alacritty", "foot", "kitty", "wezterm", "wterm"]; @@ -45,6 +65,11 @@ pub fn handler(selection: Match, state: &State) -> HandleResult { }) .unwrap(); + // count the usage for the statistics + if let Some(stats) = &state.execution_stats { + stats.register_usage(&entry); + } + if entry.term { match &state.config.terminal { Some(term) => { @@ -101,12 +126,20 @@ pub fn init(config_dir: RString) -> State { } }; + // only load execution stats, if needed + let execution_stats = if config.use_usage_statistics { + let execution_stats_path = format!("{}/execution_statistics.ron", config_dir); + Some(ExecutionStats::from_file_or_default(&execution_stats_path, &config)) + } else { + None + }; + let entries = scrubber::scrubber(&config).unwrap_or_else(|why| { eprintln!("Failed to load desktop entries: {}", why); Vec::new() }); - State { config, entries } + State { config, entries, execution_stats } } #[get_matches] @@ -131,6 +164,11 @@ pub fn get_matches(input: RString, state: &State) -> RVec { let mut score = (app_score * 25 + keyword_score) - entry.offset; + // add score for often used apps + if let Some(stats) = &state.execution_stats { + score += stats.get_weight(entry) * state.config.usage_score_multiplier; + } + // prioritize actions if entry.desc.is_some() { score *= 2; From 120984f26b0bbd9aa61b9c476a15eab1566898ee Mon Sep 17 00:00:00 2001 From: Simon Struck Date: Mon, 22 Jul 2024 22:29:16 +0200 Subject: [PATCH 2/2] chore(deps): update Cargo.lock --- Cargo.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 716dbde..0179e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c910a59fc096407041f5d1afc61132106e38868d1feea27e52f1045ee1e6c16" dependencies = [ "quote", - "syn 2.0.41", + "syn 2.0.72", ] [[package]] @@ -264,18 +264,18 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -339,22 +339,22 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.72", ] [[package]] @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote",