Skip to content
This repository was archived by the owner on Jun 25, 2025. It is now read-only.
Merged
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
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ Simply search for the application you wish to launch.
```ron
// <Anyrun config dir>/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,
)
```
```
78 changes: 78 additions & 0 deletions 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 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,
/// 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 @@ -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]
Expand All @@ -131,6 +164,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 *= 2;
Expand Down