1mod decoder;
5mod exec;
6mod generic_entry;
7mod iter;
8#[cfg(test)]
9mod tests;
10
11pub use self::iter::Iter;
12pub use decoder::{desktop_entry_from_path, group_entry_from_path, parse_line, DecodeError, Line};
13pub use exec::ExecError;
14pub use generic_entry::GenericEntry;
15use std::borrow::Cow;
16use std::collections::BTreeMap;
17use std::fmt::{self, Display, Formatter};
18use std::hash::{Hash, Hasher};
19use std::path::{Path, PathBuf};
20pub use unicase;
21use unicase::Ascii;
22use xdg::BaseDirectories;
23
24pub fn desktop_entries(locales: &[String]) -> Vec<DesktopEntry> {
26 Iter::new(default_paths())
27 .filter_map(|p| DesktopEntry::from_path(p, Some(&locales)).ok())
28 .collect::<Vec<_>>()
29}
30
31pub fn find_app_by_id<'a>(
37 entries: &'a [DesktopEntry],
38 app_id: Ascii<&str>,
39) -> Option<&'a DesktopEntry> {
40 let match_by_wm_class = entries.iter().find(|entry| entry.matches_wm_class(app_id));
44
45 match_by_wm_class
46 .or_else(|| entries.iter().find(|entry| entry.matches_id(app_id)))
48 .or_else(|| entries.iter().find(|entry| entry.matches_name(app_id)))
50 .or_else(|| {
52 entries
53 .iter()
54 .find(|entry| entry.exec().is_some_and(|exec| exec == app_id))
55 })
56 .or_else(|| {
58 entries.iter().find(|entry| {
59 entry.exec().is_some_and(|exec| {
60 exec.split_ascii_whitespace()
61 .next()
62 .is_some_and(|exec| exec == app_id)
63 })
64 })
65 })
66 .or_else(|| {
68 entries
69 .iter()
70 .find(|entry| entry.matches_snap_appname(app_id))
71 })
72}
73
74#[derive(Debug, Clone, Default)]
75pub struct Groups(pub BTreeMap<GroupName, Group>);
76pub type GroupName = String;
77
78impl Groups {
79 #[inline]
80 pub fn desktop_entry(&self) -> Option<&Group> {
81 self.0.get("Desktop Entry")
82 }
83
84 #[inline]
85 pub fn group(&self, key: &str) -> Option<&Group> {
86 self.0.get(key)
87 }
88}
89
90pub type Key = String;
91#[derive(Debug, Clone, Default)]
92pub struct Group(pub BTreeMap<Key, (Value, LocaleMap)>);
93
94impl Group {
95 pub fn localized_entry<L: AsRef<str>>(&self, key: &str, locales: &[L]) -> Option<&str> {
96 #[inline(never)]
97 fn inner<'a>(
98 this: &'a Group,
99 key: &str,
100 locales: &mut dyn Iterator<Item = &str>,
101 ) -> Option<&'a str> {
102 let (default_value, locale_map) = this.0.get(key)?;
103
104 for locale in locales {
105 match locale_map.get(locale) {
106 Some(value) => return Some(value),
107 None => {
108 if let Some(pos) = memchr::memchr(b'_', locale.as_bytes()) {
109 if let Some(value) = locale_map.get(&locale[..pos]) {
110 return Some(value);
111 }
112 }
113 }
114 }
115 }
116
117 Some(default_value)
118 }
119
120 inner(self, key, &mut locales.iter().map(AsRef::as_ref))
121 }
122
123 #[inline]
124 pub fn entry(&self, key: &str) -> Option<&str> {
125 self.0.get(key).map(|key| key.0.as_ref())
126 }
127
128 #[inline]
129 pub fn entry_bool(&self, key: &str) -> Option<bool> {
130 match self.entry(key)? {
131 "true" => Some(true),
132 "false" => Some(false),
133 _ => None,
134 }
135 }
136}
137
138pub type Locale = String;
139pub type LocaleMap = BTreeMap<Locale, Value>;
140pub type Value = String;
141
142#[derive(Debug, Clone)]
143pub struct DesktopEntry {
144 pub appid: String,
145 pub groups: Groups,
146 pub path: PathBuf,
147 pub ubuntu_gettext_domain: Option<String>,
148}
149
150impl Ord for DesktopEntry {
151 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
152 (&self.path, &self.appid).cmp(&(&other.path, &other.appid))
153 }
154}
155
156impl PartialOrd for DesktopEntry {
157 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
158 Some(self.path.cmp(&other.path))
159 }
160}
161
162impl PartialEq for DesktopEntry {
163 fn eq(&self, other: &Self) -> bool {
164 (&self.path, &self.appid) == (&other.path, &other.appid)
165 }
166}
167
168impl Eq for DesktopEntry {}
169
170impl Hash for DesktopEntry {
171 fn hash<H: Hasher>(&self, state: &mut H) {
172 self.appid.hash(state);
173 }
174}
175
176impl DesktopEntry {
177 #[inline]
180 pub fn from_appid(appid: String) -> DesktopEntry {
181 let name = appid.split('.').next_back().unwrap_or(&appid).to_string();
182
183 let mut de = DesktopEntry {
184 appid,
185 groups: Groups::default(),
186 path: PathBuf::from(""),
187 ubuntu_gettext_domain: None,
188 };
189 de.add_desktop_entry("Name".to_string(), name);
190 de
191 }
192
193 #[inline]
195 pub fn matches_wm_class(&self, id: Ascii<&str>) -> bool {
196 self.startup_wm_class()
197 .is_some_and(|wm_class| wm_class == id)
198 }
199
200 #[inline]
202 pub fn matches_snap_appname(&self, name: Ascii<&str>) -> bool {
203 self.snap_appname()
204 .is_some_and(|snap_name| snap_name == name)
205 }
206
207 #[inline]
209 pub fn matches_id(&self, id: Ascii<&str>) -> bool {
210 id == self.id()
212 || self.path.file_stem()
214 .and_then(|os_str| os_str.to_str())
215 .is_some_and(|name| {
216 name == id
217 || id.split('.').rev().next().is_some_and(|id| id == name)
219 })
220 }
221
222 #[inline]
224 pub fn matches_name(&self, name: Ascii<&str>) -> bool {
225 self.name::<&str>(&[])
226 .map(|n| n.as_ref() == name)
227 .unwrap_or_default()
228 }
229}
230
231impl DesktopEntry {
232 #[inline]
233 pub fn id(&self) -> &str {
234 self.appid.as_ref()
235 }
236
237 #[inline]
239 pub fn desktop_entry(&self, key: &str) -> Option<&str> {
240 self.groups.desktop_entry()?.entry(key)
241 }
242
243 #[inline]
244 pub fn desktop_entry_localized<'a, L: AsRef<str>>(
245 &'a self,
246 key: &str,
247 locales: &[L],
248 ) -> Option<Cow<'a, str>> {
249 Self::localized_entry(
250 self.ubuntu_gettext_domain.as_deref(),
251 self.groups.desktop_entry(),
252 key,
253 &mut locales.iter().map(AsRef::as_ref),
254 )
255 }
256
257 pub fn add_desktop_entry(&mut self, key: String, value: String) {
260 let action_key = "Desktop Entry";
261 let value = (value, LocaleMap::default());
262
263 match self.groups.0.get_mut(action_key) {
264 Some(keymap) => {
265 keymap.0.insert(key, value);
266 }
267 None => {
268 let mut keymap = Group::default();
269 keymap.0.insert(key, value);
270 self.groups.0.insert(action_key.to_string(), keymap);
271 }
272 }
273 }
274
275 #[inline]
276 pub fn name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
277 self.desktop_entry_localized("Name", locales)
278 }
279
280 #[inline]
281 pub fn generic_name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
282 self.desktop_entry_localized("GenericName", locales)
283 }
284
285 #[inline]
287 pub fn full_name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
288 self.desktop_entry_localized("X-GNOME-FullName", locales)
289 .filter(|name| !name.as_ref().is_empty())
290 .or_else(|| self.name(locales))
291 }
292
293 #[inline]
294 pub fn icon(&self) -> Option<&str> {
295 self.desktop_entry("Icon")
296 }
297
298 #[inline]
300 pub fn comment<'a, L: AsRef<str>>(&'a self, locales: &[L]) -> Option<Cow<'a, str>> {
301 self.desktop_entry_localized("Comment", locales)
302 }
303
304 #[inline]
305 pub fn exec(&self) -> Option<&str> {
306 self.desktop_entry("Exec")
307 }
308
309 #[inline]
311 pub fn try_exec(&self) -> Option<&str> {
312 self.desktop_entry("TryExec")
313 }
314
315 #[inline]
316 pub fn dbus_activatable(&self) -> bool {
317 self.desktop_entry_bool("DBusActivatable")
318 }
319
320 #[inline]
322 pub fn categories(&self) -> Option<Vec<&str>> {
323 self.desktop_entry("Categories")
324 .map(|e| e.split(';').collect())
325 }
326
327 #[inline]
329 pub fn keywords<'a, L: AsRef<str>>(&'a self, locales: &[L]) -> Option<Vec<Cow<'a, str>>> {
330 self.localized_entry_splitted(self.groups.desktop_entry(), "Keywords", locales)
331 }
332
333 #[inline]
335 pub fn mime_type(&self) -> Option<Vec<&str>> {
336 self.desktop_entry("MimeType")
337 .map(|e| e.split(';').collect())
338 }
339
340 #[inline]
342 pub fn implements(&self) -> Option<Vec<&str>> {
343 self.desktop_entry("Implements")
344 .map(|e| e.split(';').collect())
345 }
346
347 #[inline]
349 pub fn no_display(&self) -> bool {
350 self.desktop_entry_bool("NoDisplay")
351 }
352
353 #[inline]
355 pub fn only_show_in(&self) -> Option<Vec<&str>> {
356 self.desktop_entry("OnlyShowIn")
357 .map(|e| e.split(';').collect())
358 }
359
360 #[inline]
362 pub fn not_show_in(&self) -> Option<Vec<&str>> {
363 self.desktop_entry("NotShowIn")
364 .map(|e| e.split(';').collect())
365 }
366
367 #[inline]
369 pub fn hidden(&self) -> bool {
370 self.desktop_entry_bool("Hidden")
371 }
372
373 #[inline]
374 pub fn flatpak(&self) -> Option<&str> {
375 self.desktop_entry("X-Flatpak")
376 }
377
378 #[inline]
379 pub fn prefers_non_default_gpu(&self) -> bool {
380 self.desktop_entry_bool("PrefersNonDefaultGPU")
381 }
382
383 #[inline]
384 pub fn snap_appname(&self) -> Option<&str> {
385 self.desktop_entry("X-SnapAppName")
386 }
387
388 #[inline]
389 pub fn startup_notify(&self) -> bool {
390 self.desktop_entry_bool("StartupNotify")
391 }
392
393 #[inline]
394 pub fn startup_wm_class(&self) -> Option<&str> {
395 self.desktop_entry("StartupWMClass")
396 }
397
398 #[inline]
399 pub fn terminal(&self) -> bool {
400 self.desktop_entry_bool("Terminal")
401 }
402
403 #[inline]
405 pub fn single_main_window(&self) -> bool {
406 self.desktop_entry_bool("SingleMainWindow")
407 }
408
409 #[inline]
411 pub fn path(&self) -> Option<&str> {
412 self.desktop_entry("Path")
413 }
414
415 #[inline]
416 pub fn type_(&self) -> Option<&str> {
417 self.desktop_entry("Type")
418 }
419
420 pub fn url(&self) -> Option<&str> {
422 self.desktop_entry("URL")
423 }
424 pub fn version(&self) -> Option<&str> {
426 self.desktop_entry("Version")
427 }
428
429 #[inline]
430 pub fn actions(&self) -> Option<Vec<&str>> {
431 self.desktop_entry("Actions")
432 .map(|e| e.split(';').collect())
433 }
434
435 #[inline]
447 pub fn action_entry(&self, action: &str, key: &str) -> Option<&str> {
448 self.groups
449 .group(["Desktop Action ", action].concat().as_str())?
450 .entry(key)
451 }
452
453 pub fn action_entry_localized<L: AsRef<str>>(
454 &self,
455 action: &str,
456 key: &str,
457 locales: &[L],
458 ) -> Option<Cow<'_, str>> {
459 #[inline(never)]
460 fn inner<'a>(
461 this: &'a DesktopEntry,
462 action: &str,
463 key: &str,
464 locales: &mut dyn Iterator<Item = &str>,
465 ) -> Option<Cow<'a, str>> {
466 let group = this
467 .groups
468 .group(["Desktop Action ", action].concat().as_str());
469
470 DesktopEntry::localized_entry(
471 this.ubuntu_gettext_domain.as_deref(),
472 group,
473 key,
474 locales,
475 )
476 }
477
478 inner(self, action, key, &mut locales.iter().map(AsRef::as_ref))
479 }
480
481 #[inline]
482 pub fn action_name<'a, L: AsRef<str>>(
483 &'a self,
484 action: &str,
485 locales: &[L],
486 ) -> Option<Cow<'a, str>> {
487 self.action_entry_localized(action, "Name", locales)
488 }
489
490 #[inline]
491 pub fn action_exec(&self, action: &str) -> Option<&str> {
492 self.action_entry(action, "Exec")
493 }
494
495 #[inline]
496 fn desktop_entry_bool(&self, key: &str) -> bool {
497 self.desktop_entry(key).map_or(false, |v| v == "true")
498 }
499
500 #[inline(never)]
501 pub(crate) fn localized_entry<'a>(
502 #[cfg_attr(not(feature = "gettext"), allow(unused_variables))]
503 ubuntu_gettext_domain: Option<&str>,
504 group: Option<&'a Group>,
505 key: &str,
506 locales: &mut dyn Iterator<Item = &str>,
507 ) -> Option<Cow<'a, str>> {
508 let (default_value, locale_map) = group?.0.get(key)?;
509
510 for locale in locales {
511 match locale_map.get(locale) {
512 Some(value) => return Some(Cow::Borrowed(value)),
513 None => {
514 if let Some(pos) = memchr::memchr(b'_', locale.as_bytes()) {
515 if let Some(value) = locale_map.get(&locale[..pos]) {
516 return Some(Cow::Borrowed(value));
517 }
518 }
519 }
520 }
521 }
522 #[cfg(feature = "gettext")]
523 if let Some(domain) = ubuntu_gettext_domain {
524 return Some(Cow::Owned(dgettext(domain, default_value)));
525 }
526 Some(Cow::Borrowed(default_value))
527 }
528
529 #[inline(never)]
530 pub fn localized_entry_splitted<'a, L: AsRef<str>>(
531 &'a self,
532 group: Option<&'a Group>,
533 key: &str,
534 locales: &[L],
535 ) -> Option<Vec<Cow<'a, str>>> {
536 #[inline(never)]
537 fn inner<'a>(
538 #[cfg_attr(not(feature = "gettext"), allow(unused_variables))] this: &'a DesktopEntry,
539 group: Option<&'a Group>,
540 key: &str,
541 locales: &mut dyn Iterator<Item = &str>,
542 ) -> Option<Vec<Cow<'a, str>>> {
543 let (default_value, locale_map) = group?.0.get(key)?;
544
545 for locale in locales {
546 match locale_map.get(locale) {
547 Some(value) => {
548 return Some(value.split(';').map(Cow::Borrowed).collect());
549 }
550 None => {
551 if let Some(pos) = memchr::memchr(b'_', locale.as_bytes()) {
552 if let Some(value) = locale_map.get(&locale[..pos]) {
553 return Some(value.split(';').map(Cow::Borrowed).collect());
554 }
555 }
556 }
557 }
558 }
559 #[cfg(feature = "gettext")]
560 if let Some(domain) = &this.ubuntu_gettext_domain {
561 return Some(
562 dgettext(domain, default_value)
563 .split(';')
564 .map(|e| Cow::Owned(e.to_string()))
565 .collect(),
566 );
567 }
568
569 Some(default_value.split(';').map(Cow::Borrowed).collect())
570 }
571
572 inner(self, group, key, &mut locales.iter().map(AsRef::as_ref))
573 }
574}
575
576impl Display for DesktopEntry {
577 fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
578 for (group_name, group) in &self.groups.0 {
579 let _ = writeln!(formatter, "[{}]", group_name);
580
581 for (key, (value, localizations)) in &group.0 {
582 let _ = writeln!(formatter, "{}={}", key, value);
583 for (locale, localized) in localizations {
584 let _ = writeln!(formatter, "{}[{}]={}", key, locale, localized);
585 }
586 }
587 writeln!(formatter)?;
588 }
589
590 Ok(())
591 }
592}
593
594#[derive(Debug, Clone, PartialEq, Eq)]
595pub enum IconSource {
596 Name(String),
597 Path(PathBuf),
598}
599
600impl IconSource {
601 pub fn from_unknown(icon: &str) -> Self {
602 let icon_path = Path::new(icon);
603 if icon_path.is_absolute() && icon_path.exists() {
604 Self::Path(icon_path.into())
605 } else {
606 Self::Name(icon.into())
607 }
608 }
609}
610
611impl Default for IconSource {
612 #[inline]
613 fn default() -> Self {
614 Self::Name("application-default".to_string())
615 }
616}
617
618#[derive(Debug, Clone, Hash, PartialEq, Eq)]
619pub enum PathSource {
620 Local,
621 LocalDesktop,
622 LocalFlatpak,
623 LocalNix,
624 Nix,
625 System,
626 SystemLocal,
627 SystemFlatpak,
628 SystemSnap,
629 Other(String),
630}
631
632impl PathSource {
633 pub fn guess_from(path: &Path) -> PathSource {
637 let base_dirs = BaseDirectories::new();
638 let data_home = base_dirs.get_data_home().unwrap();
639 let mut nix_state = base_dirs.get_state_home().unwrap();
640 nix_state.push("nix");
641
642 if path.starts_with("/usr/share") {
643 PathSource::System
644 } else if path.starts_with("/usr/local/share") {
645 PathSource::SystemLocal
646 } else if path.starts_with("/var/lib/flatpak") {
647 PathSource::SystemFlatpak
648 } else if path.starts_with("/var/lib/snapd") {
649 PathSource::SystemSnap
650 } else if path.starts_with("/nix/var/nix/profiles/default")
651 || path.starts_with("/nix/store")
652 || path.starts_with("/run/current-system/sw")
653 {
654 PathSource::Nix
655 } else if path.to_string_lossy().contains("/flatpak/") {
656 PathSource::LocalFlatpak
657 } else if path.starts_with(data_home.as_path()) {
658 PathSource::Local
659 } else if path.starts_with("/nix/var/nix/profiles/per-user")
660 || path.to_string_lossy().contains(".nix")
661 || path.starts_with(nix_state.as_path())
662 {
663 PathSource::LocalNix
664 } else {
665 PathSource::Other(String::from("unknown"))
666 }
667 }
668}
669
670#[cold]
676pub fn default_paths() -> impl Iterator<Item = PathBuf> {
677 let base_dirs = BaseDirectories::new();
678 let mut data_dirs: Vec<PathBuf> = vec![];
679 data_dirs.push(base_dirs.get_data_home().unwrap());
680 data_dirs.append(&mut base_dirs.get_data_dirs());
681
682 data_dirs.into_iter().map(|d| d.join("applications"))
683}
684
685#[cfg(feature = "gettext")]
686#[inline]
687pub(crate) fn dgettext(domain: &str, message: &str) -> String {
688 use gettextrs::{setlocale, LocaleCategory};
689 setlocale(LocaleCategory::LcAll, "");
690 gettextrs::dgettext(domain, message)
691}
692
693#[cold]
696pub fn get_languages_from_env() -> Vec<String> {
697 let mut l = Vec::new();
698
699 if let Ok(lang) = std::env::var("LANG") {
700 l.push(lang);
701 }
702
703 if let Ok(lang) = std::env::var("LANGUAGES") {
704 lang.split(':').for_each(|lang| {
705 l.push(lang.to_owned());
706 })
707 }
708
709 l
710}
711
712pub fn current_desktop() -> Option<Vec<String>> {
713 std::env::var("XDG_CURRENT_DESKTOP").ok().map(|x| {
714 let x = x.to_ascii_lowercase();
715 if x == "unity" {
716 vec!["gnome".to_string()]
717 } else {
718 x.split(':').map(|e| e.to_string()).collect()
719 }
720 })
721}
722
723#[test]
724fn add_field() {
725 let appid = "appid";
726 let de = DesktopEntry::from_appid(appid.to_string());
727
728 assert_eq!(de.appid, appid);
729 assert_eq!(de.name(&[] as &[&str]).unwrap(), appid);
730
731 let s = get_languages_from_env();
732
733 println!("{:?}", s);
734}
735
736#[test]
737fn env_with_locale() {
738 let locales = &["fr_FR"];
739
740 let de = DesktopEntry::from_path(
741 PathBuf::from("tests_entries/org.mozilla.firefox.desktop"),
742 Some(locales),
743 )
744 .unwrap();
745
746 assert_eq!(de.generic_name(locales).unwrap(), "Navigateur Web");
747
748 let locales = &["nb"];
749
750 assert_eq!(de.generic_name(locales).unwrap(), "Web Browser");
751}