-
-
Notifications
You must be signed in to change notification settings - Fork 74
feat: Add usage statistics for applications to improve search results #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Mutex<HashMap<String, i64>>>, | ||
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<String, i64> = 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); | ||
AnyTimeTraveler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if weight < self.max_weight { | ||
weight | ||
} else { | ||
self.max_weight | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String>, | ||
/// 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since using a logarithm would disassociate the value from a specific ranking score, we should probably map this to something like:
This also makes the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would definitely keep As mentioned in #176 (comment) I like the idea of logarithms, but the |
||
/// 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,16 +33,21 @@ impl Default for Config { | |
desktop_actions: false, | ||
max_entries: 5, | ||
terminal: None, | ||
use_usage_statistics: true, | ||
usage_score_multiplier: 50, | ||
max_counted_usages: 10, | ||
} | ||
} | ||
} | ||
|
||
pub struct State { | ||
config: Config, | ||
entries: Vec<(DesktopEntry, u64)>, | ||
execution_stats: Option<ExecutionStats>, | ||
} | ||
|
||
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 { | ||
AnyTimeTraveler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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<Match> { | |
|
||
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; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the
max_counted_usages
option, since a low number will diminish the function of the history, the longer anyrun is used. If all applications have been launcher at least 10 times, where's the benefit?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The benefit is just over the applications that haven't been used at all.
Technically, it would be enough to be able to mark an application as favorite, to give it a static boost over other apps.
Since I did not want to add interactivity to the plugin (because it's massively more complicated and would entail editing the base application), I chose to use a simple algorithm that would count up to 10 usages to give an app the full bonus score.
This way, you can accidentally launch an app a few times, without it impacting the score too much.
There are probably more advanced ways to do this, but I only spent an evening on this and it's good enough for my current purpose.
In another comment, you mentioned logarithms. I like the idea of capping the upper limit that way, but there would still have to be an upper limit, as to not overwhelm the actual search matches from the fuzzymatcher.