gravel_provider_program/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//! Program provider.
//! Searches for applications and allows you to launch them.
//!
//! ### Linux
//! Searches for .desktop files in `$XDG_DATA_DIRS` and `$XDG_DATA_HOME`
//!
//! Launches applications using gtk-launch.
//!
//! ### Windows
//! Searches for .lnk files in
//! - `%ProgramData%\Microsoft\Windows\Start Menu\Programs`
//! - `%APPDATA%\Microsoft\Windows\Start Menu\Programs`
//!
//! Launches applications using explorer.

use glob::{glob, Paths};
use gravel_ffi::prelude::*;
use itertools::Itertools;
use serde::Deserialize;
use std::{path::PathBuf, time::Duration};

#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(windows, path = "windows.rs")]
mod implementation;

const DEFAULT_CONFIG: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/config.yml"));

struct ProgramProvider {
	program_paths: Vec<PathBuf>,
	cache: HitCache,
}

#[gravel_provider("program")]
impl Provider for ProgramProvider {
	fn new(config: &PluginConfigAdapter<'_>) -> Self {
		let config = config.get::<Config>(DEFAULT_CONFIG);

		let program_paths = implementation::get_program_paths(&config).collect_vec();
		log::debug!("determined program paths: {program_paths:?}");

		Self {
			program_paths,
			cache: HitCache::default().max_age(Duration::from_secs(10)),
		}
	}

	fn query(&self, _query: &str) -> ProviderResult {
		fn inner(program_paths: &[PathBuf]) -> impl Iterator<Item = SimpleHit> + '_ {
			program_paths
				.iter()
				.filter_map(expand_glob)
				.flatten()
				.filter_map(Result::ok)
				// TODO: follow desktop file specification on deduplication
				.unique_by(|p| p.file_name().map(ToOwned::to_owned))
				.filter_map(|p| implementation::get_program(&p))
		}

		let cached = self.cache.get_or(|| inner(&self.program_paths));

		ProviderResult::from_cached(cached.get())
	}

	fn clear_caches(&self) {
		self.cache.clear();
	}
}

fn expand_glob(pattern: &PathBuf) -> Option<Paths> {
	glob(&pattern.to_string_lossy())
		.inspect_err(|e| log::error!("unable to expand glob {pattern:?}: {e}"))
		.ok()
}

#[derive(Deserialize, Debug)]
struct Config {
	#[cfg(windows)]
	pub windows: WindowsConfig,
}

#[cfg(windows)]
#[derive(Deserialize, Debug)]
struct WindowsConfig {
	shortcut_paths: Vec<String>,
}