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