desk_exec/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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
//! # Desk-exec
//!
//! Desk-exec provides an API to search for and execute programs from their XDG desktop entry
//! files.
//!
//! ## Overview
//!
//! This API is designed around the 'DesktopEntry' struct from the 'freedesktop_desktop_entry'
//! crate. It provides the functionality to do a substring search for list of directories, and
//! execute the program within a desktop entry.
use std::{
borrow::Cow,
ops::Not,
panic,
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
result,
};
use freedesktop_desktop_entry::{DesktopEntry, Iter};
use regex::Regex;
use thiserror::Error;
pub type Result<T, E = DesktopEntryError> = result::Result<T, E>;
/// Represents all possible error variants when trying to execute a desktop entry
#[derive(Debug, Error)]
pub enum DesktopEntryError {
#[error("failed to find the 'Exec' value at '{0}'")]
MissingExec(PathBuf),
#[error("failed to execute the program '{0}' at '{1}'")]
InvalidExec(String, PathBuf),
#[error("failed to parse the 'Exec' value at '{0}'")]
InvalidExecSyntax(PathBuf),
#[error("internal regex error when parsing the 'Exec' value at {0}")]
Regex(PathBuf),
}
/// Gives the ability for a desktop entry type to provide an executable name cleaned from any
/// placeholder values
pub trait CleanPlaceholders {
fn exec_clean(&self) -> Result<Cow<str>>;
}
impl<'a> CleanPlaceholders for DesktopEntry<'a> {
/// Returns the name of the entries executable without any placeholder values.
fn exec_clean(&self) -> Result<Cow<str>> {
let output = Regex::new("%[a-zA-Z]")
.map_err(|_| DesktopEntryError::Regex(self.path.to_path_buf()))?;
Ok(output.replace_all(
self.exec()
.ok_or_else(|| DesktopEntryError::MissingExec(self.path.to_path_buf()))?,
"",
))
}
}
/// Panic-less wrapper for 'fredesktop_desktop_entry::default_paths()'. Returns the default XDG
/// data directories, where desktop entries are stored on most systems.
pub fn get_default_entry_dirs() -> Option<impl Iterator<Item = PathBuf>> {
panic::catch_unwind(freedesktop_desktop_entry::default_paths).ok()
}
/// Searches a list of directories for any desktop entry files that match the 'program_name'
/// pattern. Will return early with the first match found when 'get_first' is true.
///
/// # Examples
///
/// ```
/// use desk_exec::{search_for_entries, get_default_entry_dirs};
/// use freedesktop_desktop_entry::get_languages_from_env;
///
/// let dirs = get_default_entry_dirs().unwrap();
/// let locales = get_languages_from_env();
///
/// let entries = search_for_entries("program", dirs, &locales, false).unwrap_or_default();
///
/// for entry in entries {
/// println!("{}", entry.path.display());
/// }
/// ```
pub fn search_for_entries<'a>(
program_name: &str,
dirs: impl Iterator<Item = PathBuf>,
locales: &[String],
get_first: bool,
) -> Option<Vec<DesktopEntry<'a>>> {
let mut entries = Vec::new();
for file in Iter::new(dirs) {
let entry = match DesktopEntry::from_path(file.clone(), Some(locales)) {
Ok(entry) => entry,
Err(_) => continue,
};
if let (Ok(program_path), Ok(file_path)) =
(Path::new(program_name).canonicalize(), file.canonicalize())
{
if program_path == file_path {
entries.clear();
entries.push(entry);
return Some(entries);
}
}
if entry.no_display() {
continue;
}
if match_entry_name(program_name, &entry, locales) {
entries.push(entry);
if get_first {
return Some(entries);
}
}
}
entries.is_empty().not().then_some(entries)
}
/// Attempts to execute the program within a desktop entry. The executed program will be detached
/// from the terminal if 'detach' is true.
///
/// # Returns
///
/// If execution is successful and the program is not detached, the 'ExitStatus' of the executed
/// program will be returned.
///
/// # Examples
///
/// ```
/// use desk_exec::exec_entry;
/// use freedesktop_desktop_entry::DesktopEntry;
///
/// let entry = DesktopEntry::from_appid("example_appid");
///
/// match exec_entry(&entry, false) {
/// Ok(Some(exit_status)) => {
/// eprintln!("Program executed with code: '{}'", exit_status.code().unwrap_or_default());
/// }
/// Ok(None) => {
/// eprintln!("Program executed with no exit code.");
/// }
/// Err(_) => {
/// eprintln!("Program failed to execute.");
/// }
/// }
/// ```
pub fn exec_entry(entry: &DesktopEntry, detach: bool) -> Result<Option<ExitStatus>> {
let entry_exec_cmd = entry.exec_clean()?.to_string();
let mut entry_exec = entry_exec_cmd.split_whitespace();
let cmd = entry_exec
.next()
.ok_or_else(|| DesktopEntryError::InvalidExecSyntax(entry.path.to_path_buf()))?;
let args: Vec<&str> = entry_exec.collect();
let mut exec = Command::new(cmd);
exec.args(&args);
if detach {
exec.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|_| {
DesktopEntryError::InvalidExec(entry_exec_cmd, entry.path.to_path_buf())
})?;
Ok(None)
} else {
let status = exec.status().map_err(|_| {
DesktopEntryError::InvalidExec(entry_exec_cmd, entry.path.to_path_buf())
})?;
Ok(Some(status))
}
}
fn match_entry_name(program_name: &str, entry: &DesktopEntry, locales: &[String]) -> bool {
let program_name = program_name.to_lowercase();
entry
.name(locales)
.map_or(false, |name| name.to_lowercase().contains(&program_name))
|| entry.appid.to_lowercase().contains(&program_name)
|| entry
.generic_name(locales)
.map_or(false, |name| name.to_lowercase().contains(&program_name))
}