1use std::path::{Path, PathBuf};
2
3mod parser;
4use parser::{DesktopEntry, ValueType};
5
6pub use parser::ParseError;
8
9#[derive(Debug, Clone)]
10pub enum ExecuteError {
11 NotExecutable(String),
12 TerminalNotFound,
13 InvalidCommand(String),
14 IoError(String),
15 ValidationFailed(String),
16}
17
18pub fn application_entry_paths() -> Vec<PathBuf> {
19 freedesktop_core::base_directories()
20 .iter()
21 .map(|path| path.join("applications"))
22 .filter(|path| path.exists())
23 .collect()
24}
25
26#[derive(Debug)]
27#[derive(Default)]
28pub struct ApplicationEntry {
29 inner: DesktopEntry,
30}
31
32
33impl ApplicationEntry {
34 pub fn name(&self) -> Option<String> {
36 self.get_string("Name")
37 }
38
39 pub fn id(&self) -> Option<String> {
45 let file_path = &self.inner.path;
46
47 if let Some(apps_pos) = file_path.to_string_lossy().find("/applications/") {
49 let after_apps = &file_path.to_string_lossy()[apps_pos + "/applications/".len()..];
50 if let Some(desktop_entry_path) = after_apps.strip_suffix(".desktop") {
51 return Some(desktop_entry_path.replace('/', "-"));
53 }
54 }
55
56 file_path.file_stem()
58 .map(|name| name.to_string_lossy().to_string())
59 }
60
61 pub fn exec(&self) -> Option<String> {
63 self.get_string("Exec")
64 }
65
66 pub fn icon(&self) -> Option<String> {
68 self.get_string("Icon")
69 }
70
71 pub fn get_string(&self, key: &str) -> Option<String> {
73 self.inner
74 .get_desktop_entry_group()
75 .and_then(|group| group.get_field(key))
76 .and_then(|value| match value {
77 ValueType::String(s) | ValueType::LocaleString(s) | ValueType::IconString(s) => {
78 Some(s.clone())
79 }
80 _ => None,
81 })
82 }
83
84 pub fn get_localized_string(&self, key: &str, locale: Option<&str>) -> Option<String> {
86 self.inner
87 .get_desktop_entry_group()
88 .and_then(|group| group.get_localized_field(key, locale))
89 .and_then(|value| match value {
90 ValueType::String(s) | ValueType::LocaleString(s) | ValueType::IconString(s) => {
91 Some(s.clone())
92 }
93 _ => None,
94 })
95 }
96
97 pub fn get_bool(&self, key: &str) -> Option<bool> {
99 self.inner
100 .get_desktop_entry_group()
101 .and_then(|group| group.get_field(key))
102 .and_then(|value| match value {
103 ValueType::Boolean(b) => Some(*b),
104 _ => None,
105 })
106 }
107
108 pub fn get_numeric(&self, key: &str) -> Option<f64> {
110 self.inner
111 .get_desktop_entry_group()
112 .and_then(|group| group.get_field(key))
113 .and_then(|value| match value {
114 ValueType::Numeric(n) => Some(*n),
115 _ => None,
116 })
117 }
118
119 pub fn get_vec(&self, key: &str) -> Option<Vec<String>> {
121 self.inner
122 .get_desktop_entry_group()
123 .and_then(|group| group.get_field(key))
124 .and_then(|value| match value {
125 ValueType::StringList(list) | ValueType::LocaleStringList(list) => {
126 Some(list.clone())
127 }
128 _ => None,
129 })
130 }
131
132 pub fn path(&self) -> &Path {
134 &self.inner.path
135 }
136
137 pub fn entry_type(&self) -> Option<String> {
139 self.get_string("Type")
140 }
141
142 pub fn generic_name(&self) -> Option<String> {
144 self.get_string("GenericName")
145 }
146
147 pub fn comment(&self) -> Option<String> {
149 self.get_string("Comment")
150 }
151
152 pub fn should_show(&self) -> bool {
153 !self.is_hidden() && !self.no_display()
154 }
155
156 pub fn is_hidden(&self) -> bool {
158 self.get_bool("Hidden").unwrap_or(false)
159 }
160
161 pub fn no_display(&self) -> bool {
163 self.get_bool("NoDisplay").unwrap_or(false)
164 }
165
166 pub fn mime_types(&self) -> Option<Vec<String>> {
168 self.get_vec("MimeType")
169 }
170
171 pub fn categories(&self) -> Option<Vec<String>> {
173 self.get_vec("Categories")
174 }
175
176 pub fn keywords(&self) -> Option<Vec<String>> {
178 self.get_vec("Keywords")
179 }
180
181 pub fn terminal(&self) -> bool {
183 self.get_bool("Terminal").unwrap_or(false)
184 }
185
186 pub fn path_dir(&self) -> Option<String> {
188 self.get_string("Path")
189 }
190
191 pub fn execute(&self) -> Result<(), ExecuteError> {
193 self.execute_with_files(&[])
194 }
195
196 pub fn execute_with_files(&self, files: &[&str]) -> Result<(), ExecuteError> {
198 self.execute_internal(files, &[])
199 }
200
201 pub fn execute_with_urls(&self, urls: &[&str]) -> Result<(), ExecuteError> {
203 self.execute_internal(&[], urls)
204 }
205
206 pub fn prepare_command(&self, files: &[&str], urls: &[&str]) -> Result<(String, Vec<String>), ExecuteError> {
208 self.validate_executable()?;
210
211 let (program, args) = self.parse_exec_command(files, urls)?;
213
214 let (final_program, final_args) = if self.terminal() {
216 self.wrap_with_terminal(&program, &args)?
217 } else {
218 (program, args)
219 };
220
221 Ok((final_program, final_args))
222 }
223
224 fn execute_internal(&self, files: &[&str], urls: &[&str]) -> Result<(), ExecuteError> {
225 self.validate_executable()?;
227
228 let (program, args) = self.parse_exec_command(files, urls)?;
230
231 let (final_program, final_args) = if self.terminal() {
233 self.wrap_with_terminal(&program, &args)?
234 } else {
235 (program, args)
236 };
237
238 let working_dir = self.path_dir();
240
241 spawn_detached_with_env(&final_program, &final_args, working_dir.as_deref())
243 .map_err(|e| ExecuteError::IoError(format!("Failed to spawn process: {}", e)))
244 }
245
246 fn validate_executable(&self) -> Result<(), ExecuteError> {
247 let exec = self.exec().ok_or_else(|| {
249 ExecuteError::NotExecutable("No Exec key found".to_string())
250 })?;
251
252 if exec.trim().is_empty() {
253 return Err(ExecuteError::NotExecutable("Exec key is empty".to_string()));
254 }
255
256 if let Some(try_exec) = self.get_string("TryExec") {
258 if !is_executable_available(&try_exec) {
259 return Err(ExecuteError::ValidationFailed(
260 format!("TryExec '{}' not found or not executable", try_exec)
261 ));
262 }
263 }
264
265 Ok(())
266 }
267
268 fn parse_exec_command(&self, files: &[&str], urls: &[&str]) -> Result<(String, Vec<String>), ExecuteError> {
269 let exec = self.exec().unwrap(); let expanded = self.expand_field_codes(&exec, files, urls);
273
274 parse_command_line(&expanded)
276 }
277
278 fn expand_field_codes(&self, exec: &str, files: &[&str], urls: &[&str]) -> String {
279 let mut result = String::new();
280 let mut chars = exec.chars().peekable();
281
282 while let Some(ch) = chars.next() {
283 if ch == '%' {
284 if let Some(&next_ch) = chars.peek() {
285 chars.next(); match next_ch {
287 '%' => result.push('%'),
288 'f' => {
289 if let Some(file) = files.first() {
290 result.push_str(&shell_escape(file));
291 }
292 },
293 'F' => {
294 for (i, file) in files.iter().enumerate() {
295 if i > 0 { result.push(' '); }
296 result.push_str(&shell_escape(file));
297 }
298 },
299 'u' => {
300 if let Some(url) = urls.first() {
301 result.push_str(&shell_escape(url));
302 }
303 },
304 'U' => {
305 for (i, url) in urls.iter().enumerate() {
306 if i > 0 { result.push(' '); }
307 result.push_str(&shell_escape(url));
308 }
309 },
310 'i' => {
311 if let Some(icon) = self.icon() {
312 result.push_str("--icon ");
313 result.push_str(&shell_escape(&icon));
314 }
315 },
316 'c' => {
317 if let Some(name) = self.name() {
318 result.push_str(&shell_escape(&name));
319 }
320 },
321 'k' => {
322 let path = self.path().to_string_lossy();
323 result.push_str(&shell_escape(&path));
324 },
325 'd' | 'D' | 'n' | 'N' | 'v' | 'm' => {},
327 _ => {
329 return format!("{}%{}{}", result, next_ch, chars.collect::<String>());
330 }
331 }
332 } else {
333 result.push(ch);
334 }
335 } else {
336 result.push(ch);
337 }
338 }
339
340 result
341 }
342
343 fn wrap_with_terminal(&self, program: &str, args: &[String]) -> Result<(String, Vec<String>), ExecuteError> {
344 let terminal = find_terminal().ok_or(ExecuteError::TerminalNotFound)?;
345
346 let mut terminal_args = vec!["-e".to_string()];
348 terminal_args.push(program.to_string());
349 terminal_args.extend(args.iter().cloned());
350
351 Ok((terminal, terminal_args))
352 }
353}
354
355impl ApplicationEntry {
356 pub fn all() -> Vec<ApplicationEntry> {
358 let mut entries: Vec<ApplicationEntry> = Vec::new();
359 for p in application_entry_paths() {
360 if let Ok(dir_entries) = std::fs::read_dir(p) {
361 for entry in dir_entries.filter_map(|e| e.ok()) {
362 if entry.path().extension().is_some_and(|ext| ext == "desktop") {
363 if let Ok(app_entry) = ApplicationEntry::try_from_path(entry.path()) {
364 entries.push(app_entry);
365 }
366 }
367 }
368 }
369 }
370 entries
371 }
372
373 pub fn from_path<P: AsRef<Path>>(path: P) -> Self {
375 Self::try_from_path(path).unwrap_or_else(|_| {
376 ApplicationEntry::default()
378 })
379 }
380
381 pub fn try_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ParseError> {
383 let desktop_entry = DesktopEntry::from_path(path)?;
384 Ok(ApplicationEntry {
385 inner: desktop_entry,
386 })
387 }
388}
389
390fn spawn_detached_with_env(program: &str, args: &[String], working_dir: Option<&str>) -> Result<(), std::io::Error> {
392 use std::process::{Command, Stdio};
393
394 #[cfg(unix)]
395 {
396 use std::os::unix::process::CommandExt;
397
398 let mut cmd = Command::new(program);
399 cmd.args(args)
400 .stdin(Stdio::null())
401 .stdout(Stdio::null())
402 .stderr(Stdio::null());
403
404 if let Some(dir) = working_dir {
406 cmd.current_dir(dir);
407 }
408
409 unsafe {
410 cmd.pre_exec(|| {
411 libc::setsid();
413 Ok(())
414 });
415 }
416
417 cmd.spawn()?;
418 Ok(())
419 }
420
421 #[cfg(not(unix))]
422 {
423 let mut cmd = Command::new(program);
424 cmd.args(args)
425 .stdin(Stdio::null())
426 .stdout(Stdio::null())
427 .stderr(Stdio::null());
428
429 if let Some(dir) = working_dir {
431 cmd.current_dir(dir);
432 }
433
434 cmd.spawn()?;
435 Ok(())
436 }
437}
438
439fn is_executable_available(executable: &str) -> bool {
441 use std::path::Path;
442
443 if Path::new(executable).is_absolute() {
444 Path::new(executable).exists()
446 } else {
447 which_command(executable).is_some()
449 }
450}
451
452fn which_command(executable: &str) -> Option<String> {
454 if let Ok(path_var) = std::env::var("PATH") {
455 for path_dir in path_var.split(':') {
456 let full_path = format!("{}/{}", path_dir, executable);
457 if std::path::Path::new(&full_path).exists() {
458 return Some(full_path);
459 }
460 }
461 }
462 None
463}
464
465fn find_terminal() -> Option<String> {
467 if let Ok(terminal) = std::env::var("TERMINAL") {
469 if is_executable_available(&terminal) {
470 return Some(terminal);
471 }
472 }
473
474 let terminals = [
476 "x-terminal-emulator", "gnome-terminal",
478 "konsole",
479 "xfce4-terminal",
480 "mate-terminal",
481 "lxterminal",
482 "rxvt-unicode",
483 "rxvt",
484 "xterm",
485 ];
486
487 for terminal in &terminals {
488 if is_executable_available(terminal) {
489 return Some(terminal.to_string());
490 }
491 }
492
493 None
494}
495
496fn shell_escape(s: &str) -> String {
498 if s.chars().any(|c| " \t\n'\"\\$`()[]{}?*~&|;<>".contains(c)) {
499 format!("'{}'", s.replace('\'', "'\"'\"'"))
500 } else {
501 s.to_string()
502 }
503}
504
505fn parse_command_line(command: &str) -> Result<(String, Vec<String>), ExecuteError> {
507 let mut parts = Vec::new();
508 let mut current = String::new();
509 let mut in_quotes = false;
510 let mut quote_char = '"';
511 let mut chars = command.chars().peekable();
512
513 while let Some(ch) = chars.next() {
514 match ch {
515 '"' | '\'' if !in_quotes => {
516 in_quotes = true;
517 quote_char = ch;
518 },
519 ch if ch == quote_char && in_quotes => {
520 in_quotes = false;
521 },
522 '\\' if in_quotes => {
523 if let Some(&next_ch) = chars.peek() {
525 chars.next();
526 match next_ch {
527 '"' | '\'' | '\\' | '$' | '`' => current.push(next_ch),
528 _ => {
529 current.push('\\');
530 current.push(next_ch);
531 }
532 }
533 } else {
534 current.push('\\');
535 }
536 },
537 ' ' | '\t' if !in_quotes => {
538 if !current.is_empty() {
539 parts.push(current);
540 current = String::new();
541 }
542 while chars.peek() == Some(&' ') || chars.peek() == Some(&'\t') {
544 chars.next();
545 }
546 },
547 _ => current.push(ch),
548 }
549 }
550
551 if !current.is_empty() {
552 parts.push(current);
553 }
554
555 if in_quotes {
556 return Err(ExecuteError::InvalidCommand("Unterminated quote".to_string()));
557 }
558
559 if parts.is_empty() {
560 return Err(ExecuteError::InvalidCommand("Empty command".to_string()));
561 }
562
563 let program = parts.remove(0);
564 Ok((program, parts))
565}