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>;
#[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),
}
pub trait CleanPlaceholders {
fn exec_clean(&self) -> Result<Cow<str>>;
}
impl<'a> CleanPlaceholders for DesktopEntry<'a> {
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()))?,
"",
))
}
}
pub fn get_default_entry_dirs() -> Option<impl Iterator<Item = PathBuf>> {
panic::catch_unwind(freedesktop_desktop_entry::default_paths).ok()
}
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)
}
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))
}