1mod decoder;
5mod exec;
6mod iter;
7
8pub use self::iter::Iter;
9pub use decoder::DecodeError;
10pub use exec::ExecError;
11use std::borrow::Cow;
12use std::collections::BTreeMap;
13use std::fmt::{self, Display, Formatter};
14use std::hash::{Hash, Hasher};
15use std::path::{Path, PathBuf};
16pub use unicase;
17use unicase::Ascii;
18use xdg::BaseDirectories;
19
20pub fn desktop_entries(locales: &[String]) -> Vec<DesktopEntry> {
22 Iter::new(default_paths())
23 .filter_map(|p| DesktopEntry::from_path(p, Some(&locales)).ok())
24 .collect::<Vec<_>>()
25}
26
27pub fn find_app_by_id<'a>(
33 entries: &'a [DesktopEntry],
34 app_id: Ascii<&str>,
35) -> Option<&'a DesktopEntry> {
36 let match_by_wm_class = entries
40 .iter()
41 .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().is_some_and(|wm_class| wm_class == id)
189 }
190
191 #[inline]
193 pub fn matches_id(&self, id: Ascii<&str>) -> bool {
194 id == self.id()
196 || self.path.file_stem()
198 .and_then(|os_str| os_str.to_str())
199 .is_some_and(|name| {
200 name == id
201 || id.split('.').rev().next().is_some_and(|id| id == name)
203 })
204 }
205
206 #[inline]
208 pub fn matches_name(&self, name: Ascii<&str>) -> bool {
209 self.name::<&str>(&[])
210 .map(|n| n.as_ref() == name)
211 .unwrap_or_default()
212 }
213}
214
215impl DesktopEntry {
216 #[inline]
217 pub fn id(&self) -> &str {
218 self.appid.as_ref()
219 }
220
221 #[inline]
223 pub fn desktop_entry(&self, key: &str) -> Option<&str> {
224 self.groups.desktop_entry()?.entry(key)
225 }
226
227 #[inline]
228 pub fn desktop_entry_localized<'a, L: AsRef<str>>(
229 &'a self,
230 key: &str,
231 locales: &[L],
232 ) -> Option<Cow<'a, str>> {
233 Self::localized_entry(
234 self.ubuntu_gettext_domain.as_deref(),
235 self.groups.desktop_entry(),
236 key,
237 &mut locales.iter().map(AsRef::as_ref),
238 )
239 }
240
241 pub fn add_desktop_entry(&mut self, key: String, value: String) {
244 let action_key = "Desktop Entry";
245 let value = (value, LocaleMap::default());
246
247 match self.groups.0.get_mut(action_key) {
248 Some(keymap) => {
249 keymap.0.insert(key, value);
250 }
251 None => {
252 let mut keymap = Group::default();
253 keymap.0.insert(key, value);
254 self.groups.0.insert(action_key.to_string(), keymap);
255 }
256 }
257 }
258
259 #[inline]
260 pub fn name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
261 self.desktop_entry_localized("Name", locales)
262 }
263
264 #[inline]
265 pub fn generic_name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
266 self.desktop_entry_localized("GenericName", locales)
267 }
268
269 #[inline]
271 pub fn full_name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
272 self.desktop_entry_localized("X-GNOME-FullName", locales)
273 .filter(|name| !name.as_ref().is_empty())
274 .or_else(|| self.name(locales))
275 }
276
277 #[inline]
278 pub fn icon(&self) -> Option<&str> {
279 self.desktop_entry("Icon")
280 }
281
282 #[inline]
284 pub fn comment<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<str>> {
285 self.desktop_entry_localized("Comment", locales)
286 }
287
288 #[inline]
289 pub fn exec(&self) -> Option<&str> {
290 self.desktop_entry("Exec")
291 }
292
293 #[inline]
295 pub fn categories(&self) -> Option<Vec<&str>> {
296 self.desktop_entry("Categories")
297 .map(|e| e.split(';').collect())
298 }
299
300 #[inline]
302 pub fn keywords<L: AsRef<str>>(&self, locales: &[L]) -> Option<Vec<Cow<str>>> {
303 self.localized_entry_splitted(self.groups.desktop_entry(), "Keywords", locales)
304 }
305
306 #[inline]
308 pub fn mime_type(&self) -> Option<Vec<&str>> {
309 self.desktop_entry("MimeType")
310 .map(|e| e.split(';').collect())
311 }
312
313 #[inline]
314 pub fn no_display(&self) -> bool {
315 self.desktop_entry_bool("NoDisplay")
316 }
317
318 #[inline]
319 pub fn only_show_in(&self) -> Option<Vec<&str>> {
320 self.desktop_entry("OnlyShowIn")
321 .map(|e| e.split(';').collect())
322 }
323
324 #[inline]
325 pub fn not_show_in(&self) -> Option<Vec<&str>> {
326 self.desktop_entry("NotShowIn")
327 .map(|e| e.split(';').collect())
328 }
329
330 #[inline]
331 pub fn flatpak(&self) -> Option<&str> {
332 self.desktop_entry("X-Flatpak")
333 }
334
335 #[inline]
336 pub fn prefers_non_default_gpu(&self) -> bool {
337 self.desktop_entry_bool("PrefersNonDefaultGPU")
338 }
339
340 #[inline]
341 pub fn startup_notify(&self) -> bool {
342 self.desktop_entry_bool("StartupNotify")
343 }
344
345 #[inline]
346 pub fn startup_wm_class(&self) -> Option<&str> {
347 self.desktop_entry("StartupWMClass")
348 }
349
350 #[inline]
351 pub fn terminal(&self) -> bool {
352 self.desktop_entry_bool("Terminal")
353 }
354
355 #[inline]
356 pub fn type_(&self) -> Option<&str> {
357 self.desktop_entry("Type")
358 }
359
360 #[inline]
361 pub fn actions(&self) -> Option<Vec<&str>> {
362 self.desktop_entry("Actions")
363 .map(|e| e.split(';').collect())
364 }
365
366 #[inline]
378 pub fn action_entry(&self, action: &str, key: &str) -> Option<&str> {
379 self.groups
380 .group(["Desktop Action ", action].concat().as_str())?
381 .entry(key)
382 }
383
384 pub fn action_entry_localized<L: AsRef<str>>(
385 &self,
386 action: &str,
387 key: &str,
388 locales: &[L],
389 ) -> Option<Cow<'_, str>> {
390 #[inline(never)]
391 fn inner<'a>(
392 this: &'a DesktopEntry,
393 action: &str,
394 key: &str,
395 locales: &mut dyn Iterator<Item = &str>,
396 ) -> Option<Cow<'a, str>> {
397 let group = this
398 .groups
399 .group(["Desktop Action ", action].concat().as_str());
400
401 DesktopEntry::localized_entry(
402 this.ubuntu_gettext_domain.as_deref(),
403 group,
404 key,
405 locales,
406 )
407 }
408
409 inner(self, action, key, &mut locales.iter().map(AsRef::as_ref))
410 }
411
412 #[inline]
413 pub fn action_name<L: AsRef<str>>(&self, action: &str, locales: &[L]) -> Option<Cow<str>> {
414 self.action_entry_localized(action, "Name", locales)
415 }
416
417 #[inline]
418 pub fn action_exec(&self, action: &str) -> Option<&str> {
419 self.action_entry(action, "Exec")
420 }
421
422 #[inline]
423 fn desktop_entry_bool(&self, key: &str) -> bool {
424 self.desktop_entry(key).map_or(false, |v| v == "true")
425 }
426
427 #[inline(never)]
428 pub(crate) fn localized_entry<'a>(
429 ubuntu_gettext_domain: Option<&str>,
430 group: Option<&'a Group>,
431 key: &str,
432 locales: &mut dyn Iterator<Item = &str>,
433 ) -> Option<Cow<'a, str>> {
434 let (default_value, locale_map) = group?.0.get(key)?;
435
436 for locale in locales {
437 match locale_map.get(locale) {
438 Some(value) => return Some(Cow::Borrowed(value)),
439 None => {
440 if let Some(pos) = memchr::memchr(b'_', locale.as_bytes()) {
441 if let Some(value) = locale_map.get(&locale[..pos]) {
442 return Some(Cow::Borrowed(value));
443 }
444 }
445 }
446 }
447 }
448 if let Some(domain) = ubuntu_gettext_domain {
449 return Some(Cow::Owned(dgettext(domain, default_value)));
450 }
451 Some(Cow::Borrowed(default_value))
452 }
453
454 #[inline(never)]
455 pub fn localized_entry_splitted<'a, L: AsRef<str>>(
456 &'a self,
457 group: Option<&'a Group>,
458 key: &str,
459 locales: &[L],
460 ) -> Option<Vec<Cow<'a, str>>> {
461 #[inline(never)]
462 fn inner<'a>(
463 this: &'a DesktopEntry,
464 group: Option<&'a Group>,
465 key: &str,
466 locales: &mut dyn Iterator<Item = &str>,
467 ) -> Option<Vec<Cow<'a, str>>> {
468 let (default_value, locale_map) = group?.0.get(key)?;
469
470 for locale in locales {
471 match locale_map.get(locale) {
472 Some(value) => {
473 return Some(value.split(';').map(Cow::Borrowed).collect());
474 }
475 None => {
476 if let Some(pos) = memchr::memchr(b'_', locale.as_bytes()) {
477 if let Some(value) = locale_map.get(&locale[..pos]) {
478 return Some(value.split(';').map(Cow::Borrowed).collect());
479 }
480 }
481 }
482 }
483 }
484 if let Some(domain) = &this.ubuntu_gettext_domain {
485 return Some(
486 dgettext(domain, default_value)
487 .split(';')
488 .map(|e| Cow::Owned(e.to_string()))
489 .collect(),
490 );
491 }
492
493 Some(default_value.split(';').map(Cow::Borrowed).collect())
494 }
495
496 inner(self, group, key, &mut locales.iter().map(AsRef::as_ref))
497 }
498}
499
500impl Display for DesktopEntry {
501 fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
502 for (group_name, group) in &self.groups.0 {
503 let _ = writeln!(formatter, "[{}]", group_name);
504
505 for (key, (value, localizations)) in &group.0 {
506 let _ = writeln!(formatter, "{}={}", key, value);
507 for (locale, localized) in localizations {
508 let _ = writeln!(formatter, "{}[{}]={}", key, locale, localized);
509 }
510 }
511 writeln!(formatter)?;
512 }
513
514 Ok(())
515 }
516}
517
518#[derive(Debug, Clone, PartialEq, Eq)]
519pub enum IconSource {
520 Name(String),
521 Path(PathBuf),
522}
523
524impl IconSource {
525 pub fn from_unknown(icon: &str) -> Self {
526 let icon_path = Path::new(icon);
527 if icon_path.is_absolute() && icon_path.exists() {
528 Self::Path(icon_path.into())
529 } else {
530 Self::Name(icon.into())
531 }
532 }
533}
534
535impl Default for IconSource {
536 #[inline]
537 fn default() -> Self {
538 Self::Name("application-default".to_string())
539 }
540}
541
542#[derive(Debug, Clone, Hash, PartialEq, Eq)]
543pub enum PathSource {
544 Local,
545 LocalDesktop,
546 LocalFlatpak,
547 LocalNix,
548 Nix,
549 System,
550 SystemLocal,
551 SystemFlatpak,
552 SystemSnap,
553 Other(String),
554}
555
556impl PathSource {
557 pub fn guess_from(path: &Path) -> PathSource {
561 let base_dirs = BaseDirectories::new().unwrap();
562 let data_home = base_dirs.get_data_home();
563
564 if path.starts_with("/usr/share") {
565 PathSource::System
566 } else if path.starts_with("/usr/local/share") {
567 PathSource::SystemLocal
568 } else if path.starts_with("/var/lib/flatpak") {
569 PathSource::SystemFlatpak
570 } else if path.starts_with("/var/lib/snapd") {
571 PathSource::SystemSnap
572 } else if path.starts_with("/nix/var/nix/profiles/default")
573 || path.starts_with("/nix/store")
574 {
575 PathSource::Nix
576 } else if path.to_string_lossy().contains("/flatpak/") {
577 PathSource::LocalFlatpak
578 } else if path.starts_with(data_home.as_path()) {
579 PathSource::Local
580 } else if path.starts_with("/nix/var/nix/profiles/per-user")
581 || path.to_string_lossy().contains(".nix")
582 {
583 PathSource::LocalNix
584 } else {
585 PathSource::Other(String::from("unknown"))
586 }
587 }
588}
589
590#[cold]
596pub fn default_paths() -> impl Iterator<Item = PathBuf> {
597 let base_dirs = BaseDirectories::new().unwrap();
598 let mut data_dirs: Vec<PathBuf> = vec![];
599 data_dirs.push(base_dirs.get_data_home());
600 data_dirs.append(&mut base_dirs.get_data_dirs());
601
602 data_dirs.into_iter().map(|d| d.join("applications"))
603}
604
605#[inline]
606pub(crate) fn dgettext(domain: &str, message: &str) -> String {
607 use gettextrs::{setlocale, LocaleCategory};
608 setlocale(LocaleCategory::LcAll, "");
609 gettextrs::dgettext(domain, message)
610}
611
612#[cold]
615pub fn get_languages_from_env() -> Vec<String> {
616 let mut l = Vec::new();
617
618 if let Ok(lang) = std::env::var("LANG") {
619 l.push(lang);
620 }
621
622 if let Ok(lang) = std::env::var("LANGUAGES") {
623 lang.split(':').for_each(|lang| {
624 l.push(lang.to_owned());
625 })
626 }
627
628 l
629}
630
631pub fn current_desktop() -> Option<Vec<String>> {
632 std::env::var("XDG_CURRENT_DESKTOP").ok().map(|x| {
633 let x = x.to_ascii_lowercase();
634 if x == "unity" {
635 vec!["gnome".to_string()]
636 } else {
637 x.split(':').map(|e| e.to_string()).collect()
638 }
639 })
640}
641
642#[test]
643fn add_field() {
644 let appid = "appid";
645 let de = DesktopEntry::from_appid(appid.to_string());
646
647 assert_eq!(de.appid, appid);
648 assert_eq!(de.name(&[] as &[&str]).unwrap(), appid);
649
650 let s = get_languages_from_env();
651
652 println!("{:?}", s);
653}
654
655#[test]
656fn env_with_locale() {
657 let locales = &["fr_FR"];
658
659 let de = DesktopEntry::from_path(
660 PathBuf::from("tests_entries/org.mozilla.firefox.desktop"),
661 Some(locales),
662 )
663 .unwrap();
664
665 assert_eq!(de.generic_name(locales).unwrap(), "Navigateur Web");
666
667 let locales = &["nb"];
668
669 assert_eq!(de.generic_name(locales).unwrap(), "Web Browser");
670}