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))
}