diff --git a/examples/applications.ron b/examples/applications.ron new file mode 100644 index 00000000..8eae7046 --- /dev/null +++ b/examples/applications.ron @@ -0,0 +1,16 @@ +Config( + // 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, +) diff --git a/plugins/applications/src/execution_stats.rs b/plugins/applications/src/execution_stats.rs new file mode 100644 index 00000000..f412d504 --- /dev/null +++ b/plugins/applications/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/plugins/applications/src/lib.rs b/plugins/applications/src/lib.rs index 3dca60fe..b9c3c68a 100644 --- a/plugins/applications/src/lib.rs +++ b/plugins/applications/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) => { @@ -98,12 +123,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] @@ -128,6 +161,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 = score * 2;