Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions examples/applications.ron
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,
Copy link

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?

Copy link
Author

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.

)
78 changes: 78 additions & 0 deletions plugins/applications/src/execution_stats.rs
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);

if weight < self.max_weight {
weight
} else {
self.max_weight
}
}
}
48 changes: 43 additions & 5 deletions plugins/applications/src/lib.rs
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,
Copy link

Choose a reason for hiding this comment

The 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:

0: 0% weighting - No effect
1: 100% weighting - Default
1.5: 150% weighting - Preference for weighted entries

This also makes the use_usage_statistics redundant. Though I like verbosity, so maybe we should keep it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would definitely keep use_usage_statistics to enable users to keep filling the usage-file without doing anything with it.
Knowing developers, I'm sure someone will discover this feature and use it this way.

As mentioned in #176 (comment) I like the idea of logarithms, but the usage_score_multiplier and max_counted_usages still needs to exist to balance the score with the score from the fuzzymatcher from the search-results.
If these values (or a version of them) are not exposed, the recency score could overwhelm the actual matches to the query, thus always showing the most used entries.
In reverse, it could also not have any effect at all, if the multiplier is too low.

/// 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 {
Expand All @@ -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"];

Expand All @@ -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) => {
Expand Down Expand Up @@ -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]
Expand All @@ -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;
Expand Down