Skip to main content

app_rummage/
lib.rs

1use std::collections::HashMap;
2use std::num::NonZeroU32;
3use std::path::PathBuf;
4use std::rc::Rc;
5use std::sync::OnceLock;
6
7use nix::sys::inotify::{InitFlags, Inotify};
8
9use cache::{drain_inotify_events, ensure_watches, AppsCache, APPS_CACHE};
10use cgroup::running_xdg_conformant_apps;
11use desktop::installed_apps_impl;
12use matching::running_apps_by_process;
13
14mod cache;
15mod cgroup;
16mod desktop;
17mod env;
18mod exec;
19mod matching;
20
21fn app_id_replacement() -> &'static HashMap<&'static str, &'static str> {
22    static APP_ID_REPLACEMENT: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
23
24    APP_ID_REPLACEMENT
25        .get_or_init(|| HashMap::from([("gnome-system-monitor-kde", "org.gnome.SystemMonitor")]))
26}
27
28fn normalize_app_id(id: &str) -> &str {
29    if let Some(real_id) = app_id_replacement().get(id) {
30        real_id
31    } else {
32        id
33    }
34}
35
36/// A running process
37///
38/// Used to identify running applications. ApplicationEntry's exec field is compared against the
39/// process's executable path to determine if the process is an application.
40pub trait Process {
41    /// The process's PID
42    fn pid(&self) -> NonZeroU32;
43    /// The process's executable path
44    ///
45    /// It is expected that the path is canonical
46    fn executable_path(&self) -> Option<PathBuf>;
47    /// The process's name
48    ///
49    /// Essentially the name of the process as seen in `ps`
50    fn name(&self) -> &str;
51    /// The process's full argv as read from `/proc/<pid>/cmdline`
52    ///
53    /// Used to disambiguate processes that share an executable between multiple
54    /// applications (e.g. Chromium-based PWAs, custom launcher shortcuts for the
55    /// same binary with different args). Returning an empty slice disables argv
56    /// disambiguation for this process; matching degrades to "binary path only",
57    /// so processes in shared-binary buckets will be dropped rather than attributed
58    /// arbitrarily.
59    fn cmdline(&self) -> &[String] {
60        &[]
61    }
62}
63
64/// And data structure representing an application
65///
66/// It contains a subset of the fields from an XDG desktop entry file
67#[derive(Clone, Debug)]
68pub struct ApplicationEntry {
69    /// The application's id
70    ///
71    /// The filename of the desktop entry file without the `.desktop` extension and is used
72    /// to uniquely identify an application
73    pub id: Rc<str>,
74    /// The application's name
75    ///
76    /// The name that is displayed to the user in the application menu, taskbar, etc.
77    pub name: Rc<str>,
78    /// The application's executable
79    ///
80    /// The absolute path to the application's executable, obtained from the `Exec` field in
81    /// the desktop entry file
82    pub exec: Option<Rc<str>>,
83    /// The application's argv as declared in the desktop entry
84    ///
85    /// Tokens of the `Exec=` line starting from the primary binary (launcher prefixes such
86    /// as `flatpak`/`snap`/`env` are stripped).
87    ///
88    /// Used to tell apart applications that share an `exec` path. For example Chromium PWAs
89    /// (distinguished by `--app-id=`/`--app=`) or user-made launcher shortcuts for the
90    /// same binary with different args.
91    pub exec_args: Vec<Rc<str>>,
92    /// The application's icon
93    ///
94    /// If available, taken verbatim from the `Icon` field in the desktop entry file
95    pub icon: Option<Rc<str>>,
96}
97
98/// Returns a map of all applications available to the current user
99///
100/// Reads all desktop entry files in `$XDG_DATA_DIRS` and `$HOME/.local/share/applications`
101/// and returns a map of the applications found.
102///
103/// The result is cached per-thread and invalidated lazily through inotify: each call
104/// drains pending events on the watched `applications/` directories and only re-scans
105/// when a `.desktop` file was created, deleted, modified, or moved. The first call
106/// from a given thread does the initial scan and sets up the watches.
107pub fn installed_apps() -> HashMap<Rc<str>, ApplicationEntry> {
108    APPS_CACHE.with(|cell| {
109        let mut slot = cell.borrow_mut();
110
111        if let Some(cache) = slot.as_mut() {
112            let dirty = drain_inotify_events(cache);
113            ensure_watches(cache, env::xdg_data_dirs());
114            if dirty {
115                cache.apps = installed_apps_impl(env::xdg_data_dirs(), env::path());
116            }
117            return cache.apps.clone();
118        }
119
120        let apps = installed_apps_impl(env::xdg_data_dirs(), env::path());
121        match Inotify::init(InitFlags::IN_NONBLOCK | InitFlags::IN_CLOEXEC) {
122            Ok(inotify) => {
123                let mut cache = AppsCache {
124                    apps: apps.clone(),
125                    inotify,
126                    watched: HashMap::new(),
127                };
128                ensure_watches(&mut cache, env::xdg_data_dirs());
129                *slot = Some(cache);
130            }
131            Err(e) => {
132                log::warn!("inotify init failed ({e}); installed_apps() will not be cached");
133            }
134        }
135        apps
136    })
137}
138
139/// Returns a list of running applications
140///
141/// Requires a list of available applications and a list of running processes. The list of available
142/// applications can be obtained using `installed_apps`.
143///
144/// Returns a list of running applications along with the PIDs for each running app.
145///
146/// **Example:**
147/// ```
148/// use app_rummage::{installed_apps, running_apps};
149/// # struct MyProcess { pid: std::num::NonZeroU32, exe: Option<std::rc::Rc<str>> }
150/// # impl app_rummage::Process for MyProcess { fn pid(&self) -> std::num::NonZeroU32 { self.pid } fn executable_path(&self) -> Option<std::path::PathBuf> { self.exe.as_ref().map(|e| std::path::Path::new(e.as_ref()).to_owned()) } fn name(&self) -> &str { "" } }
151/// # fn running_processes() -> Vec<MyProcess> { vec![] }
152///
153/// let available_apps = installed_apps();
154/// let processes = running_processes();
155/// let running_apps = running_apps(&available_apps, &processes);
156/// for (app, pids) in running_apps {
157///    println!("{}: {:?}", app.name, pids);
158/// }
159/// ```
160pub fn running_apps<'a, P: Process + 'a>(
161    available_applications: &'a HashMap<Rc<str>, ApplicationEntry>,
162    processes: impl IntoIterator<Item = &'a P>,
163) -> Vec<(&'a ApplicationEntry, Vec<NonZeroU32>)> {
164    let mut running_apps = running_xdg_conformant_apps(available_applications);
165
166    let mut apps_by_proc = running_apps_by_process(available_applications, processes);
167    for (app_id, (app, pids)) in apps_by_proc.drain() {
168        let normalized_id = normalize_app_id(app_id.as_ref());
169        if !running_apps.contains_key(normalized_id) {
170            let key: Rc<str> = if normalized_id == app_id.as_ref() {
171                app_id
172            } else {
173                Rc::from(normalized_id)
174            };
175            running_apps.insert(key, (app, pids));
176        }
177    }
178
179    running_apps
180        .values_mut()
181        .map(|(app, pids)| (*app, std::mem::take(pids)))
182        .collect()
183}