freedesktop_desktop_entry/
lib.rs1mod decoder;
5mod iter;
6
7mod exec;
8use cached::proc_macro::cached;
9pub use exec::ExecError;
10
11pub mod matching;
12pub use decoder::DecodeError;
13
14pub use self::iter::Iter;
15use std::borrow::Cow;
16use std::collections::BTreeMap;
17use std::hash::{Hash, Hasher};
18
19use std::path::{Path, PathBuf};
20use xdg::BaseDirectories;
21
22pub type GroupName = String;
23#[derive(Debug, Clone, Default)]
24pub struct Groups(pub BTreeMap<GroupName, Group>);
25
26impl Groups {
27 pub fn desktop_entry(&self) -> Option<&Group> {
28 self.0.get("Desktop Entry")
29 }
30
31 pub fn group(&self, key: &str) -> Option<&Group> {
32 self.0.get(key)
33 }
34}
35
36pub type Key = String;
37#[derive(Debug, Clone, Default)]
38pub struct Group(pub BTreeMap<Key, (Value, LocaleMap)>);
39
40impl Group {
41 pub fn localized_entry<L: AsRef<str>>(&self, key: &str, locales: &[L]) -> Option<&str> {
42 let (default_value, locale_map) = self.0.get(key)?;
43
44 for locale in locales {
45 match locale_map.get(locale.as_ref()) {
46 Some(value) => return Some(value),
47 None => {
48 if let Some(pos) = memchr::memchr(b'_', locale.as_ref().as_bytes()) {
49 if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) {
50 return Some(value);
51 }
52 }
53 }
54 }
55 }
56
57 Some(default_value)
58 }
59
60 pub fn entry(&self, key: &str) -> Option<&str> {
61 self.0.get(key).map(|key| key.0.as_ref())
62 }
63
64 pub fn entry_bool(&self, key: &str) -> Option<bool> {
65 match self.entry(key)? {
66 "true" => Some(true),
67 "false" => Some(false),
68 _ => None,
69 }
70 }
71}
72
73pub type Locale = String;
74pub type LocaleMap = BTreeMap<Locale, Value>;
75pub type Value = String;
76
77#[derive(Debug, Clone)]
78pub struct DesktopEntry {
79 pub appid: String,
80 pub groups: Groups,
81 pub path: PathBuf,
82 pub ubuntu_gettext_domain: Option<String>,
83}
84
85impl Eq for DesktopEntry {}
86
87impl Hash for DesktopEntry {
88 fn hash<H: Hasher>(&self, state: &mut H) {
89 self.appid.hash(state);
90 }
91}
92
93impl PartialEq for DesktopEntry {
94 fn eq(&self, other: &Self) -> bool {
95 self.appid == other.appid
96 }
97}
98
99impl DesktopEntry {
100 pub fn from_appid(appid: String) -> DesktopEntry {
103 let name = appid.split('.').next_back().unwrap_or(&appid).to_string();
104
105 let mut de = DesktopEntry {
106 appid,
107 groups: Groups::default(),
108 path: PathBuf::from(""),
109 ubuntu_gettext_domain: None,
110 };
111 de.add_desktop_entry("Name".to_string(), name);
112 de
113 }
114}
115
116impl DesktopEntry {
117 pub fn id(&self) -> &str {
118 self.appid.as_ref()
119 }
120
121 pub fn desktop_entry(&self, key: &str) -> Option<&str> {
123 self.groups.desktop_entry()?.entry(key)
124 }
125
126 pub fn desktop_entry_localized<'a, L: AsRef<str>>(
127 &'a self,
128 key: &str,
129 locales: &[L],
130 ) -> Option<Cow<'a, str>> {
131 Self::localized_entry(
132 self.ubuntu_gettext_domain.as_deref(),
133 self.groups.desktop_entry(),
134 key,
135 locales,
136 )
137 }
138
139 pub fn add_desktop_entry(&mut self, key: String, value: String) {
142 let action_key = "Desktop Entry";
143 let value = (value, LocaleMap::default());
144
145 match self.groups.0.get_mut(action_key) {
146 Some(keymap) => {
147 keymap.0.insert(key, value);
148 }
149 None => {
150 let mut keymap = Group::default();
151 keymap.0.insert(key, value);
152 self.groups.0.insert(action_key.to_string(), keymap);
153 }
154 }
155 }
156
157 pub fn name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
158 self.desktop_entry_localized("Name", locales)
159 }
160
161 pub fn generic_name<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<'_, str>> {
162 self.desktop_entry_localized("GenericName", locales)
163 }
164
165 pub fn icon(&self) -> Option<&str> {
166 self.desktop_entry("Icon")
167 }
168
169 pub fn comment<L: AsRef<str>>(&self, locales: &[L]) -> Option<Cow<str>> {
171 self.desktop_entry_localized("Comment", locales)
172 }
173
174 pub fn exec(&self) -> Option<&str> {
175 self.desktop_entry("Exec")
176 }
177
178 pub fn categories(&self) -> Option<Vec<&str>> {
180 self.desktop_entry("Categories")
181 .map(|e| e.split(';').collect())
182 }
183
184 pub fn keywords<L: AsRef<str>>(&self, locales: &[L]) -> Option<Vec<Cow<str>>> {
186 self.localized_entry_splitted(self.groups.desktop_entry(), "Keywords", locales)
187 }
188
189 pub fn mime_type(&self) -> Option<Vec<&str>> {
191 self.desktop_entry("MimeType")
192 .map(|e| e.split(';').collect())
193 }
194
195 pub fn no_display(&self) -> bool {
196 self.desktop_entry_bool("NoDisplay")
197 }
198
199 pub fn only_show_in(&self) -> Option<Vec<&str>> {
200 self.desktop_entry("OnlyShowIn")
201 .map(|e| e.split(';').collect())
202 }
203
204 pub fn not_show_in(&self) -> Option<Vec<&str>> {
205 self.desktop_entry("NotShowIn")
206 .map(|e| e.split(';').collect())
207 }
208
209 pub fn flatpak(&self) -> Option<&str> {
210 self.desktop_entry("X-Flatpak")
211 }
212
213 pub fn prefers_non_default_gpu(&self) -> bool {
214 self.desktop_entry_bool("PrefersNonDefaultGPU")
215 }
216
217 pub fn startup_notify(&self) -> bool {
218 self.desktop_entry_bool("StartupNotify")
219 }
220
221 pub fn startup_wm_class(&self) -> Option<&str> {
222 self.desktop_entry("StartupWMClass")
223 }
224
225 pub fn terminal(&self) -> bool {
226 self.desktop_entry_bool("Terminal")
227 }
228
229 pub fn type_(&self) -> Option<&str> {
230 self.desktop_entry("Type")
231 }
232
233 pub fn actions(&self) -> Option<Vec<&str>> {
234 self.desktop_entry("Actions")
235 .map(|e| e.split(';').collect())
236 }
237
238 pub fn action_entry(&self, action: &str, key: &str) -> Option<&str> {
250 self.groups
251 .group(["Desktop Action ", action].concat().as_str())?
252 .entry(key)
253 }
254
255 pub fn action_entry_localized<L: AsRef<str>>(
256 &self,
257 action: &str,
258 key: &str,
259 locales: &[L],
260 ) -> Option<Cow<'_, str>> {
261 let group = self
262 .groups
263 .group(["Desktop Action ", action].concat().as_str());
264
265 Self::localized_entry(self.ubuntu_gettext_domain.as_deref(), group, key, locales)
266 }
267
268 pub fn action_name<L: AsRef<str>>(&self, action: &str, locales: &[L]) -> Option<Cow<str>> {
269 self.action_entry_localized(action, "Name", locales)
270 }
271
272 pub fn action_exec(&self, action: &str) -> Option<&str> {
273 self.action_entry(action, "Exec")
274 }
275
276 fn desktop_entry_bool(&self, key: &str) -> bool {
277 self.desktop_entry(key).map_or(false, |v| v == "true")
278 }
279
280 pub(crate) fn localized_entry<'a, L: AsRef<str>>(
281 ubuntu_gettext_domain: Option<&str>,
282 group: Option<&'a Group>,
283 key: &str,
284 locales: &[L],
285 ) -> Option<Cow<'a, str>> {
286 let (default_value, locale_map) = group?.0.get(key)?;
287
288 for locale in locales {
289 match locale_map.get(locale.as_ref()) {
290 Some(value) => return Some(Cow::Borrowed(value)),
291 None => {
292 if let Some(pos) = memchr::memchr(b'_', locale.as_ref().as_bytes()) {
293 if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) {
294 return Some(Cow::Borrowed(value));
295 }
296 }
297 }
298 }
299 }
300 if let Some(domain) = ubuntu_gettext_domain {
301 return Some(Cow::Owned(dgettext(domain, default_value)));
302 }
303 Some(Cow::Borrowed(default_value))
304 }
305
306 pub fn localized_entry_splitted<'a, L: AsRef<str>>(
307 &self,
308 group: Option<&'a Group>,
309 key: &str,
310 locales: &[L],
311 ) -> Option<Vec<Cow<'a, str>>> {
312 let (default_value, locale_map) = group?.0.get(key)?;
313
314 for locale in locales {
315 match locale_map.get(locale.as_ref()) {
316 Some(value) => {
317 return Some(value.split(';').map(Cow::Borrowed).collect());
318 }
319 None => {
320 if let Some(pos) = memchr::memchr(b'_', locale.as_ref().as_bytes()) {
321 if let Some(value) = locale_map.get(&locale.as_ref()[..pos]) {
322 return Some(value.split(';').map(Cow::Borrowed).collect());
323 }
324 }
325 }
326 }
327 }
328 if let Some(domain) = &self.ubuntu_gettext_domain {
329 return Some(
330 dgettext(domain, default_value)
331 .split(';')
332 .map(|e| Cow::Owned(e.to_string()))
333 .collect(),
334 );
335 }
336
337 Some(default_value.split(';').map(Cow::Borrowed).collect())
338 }
339}
340
341use std::fmt::{self, Display, Formatter};
342
343impl Display for DesktopEntry {
344 fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
345 for (group_name, group) in &self.groups.0 {
346 let _ = writeln!(formatter, "[{}]", group_name);
347
348 for (key, (value, localizations)) in &group.0 {
349 let _ = writeln!(formatter, "{}={}", key, value);
350 for (locale, localized) in localizations {
351 let _ = writeln!(formatter, "{}[{}]={}", key, locale, localized);
352 }
353 }
354 writeln!(formatter)?;
355 }
356
357 Ok(())
358 }
359}
360
361#[derive(Debug, Clone, Hash, PartialEq, Eq)]
362pub enum PathSource {
363 Local,
364 LocalDesktop,
365 LocalFlatpak,
366 LocalNix,
367 Nix,
368 System,
369 SystemLocal,
370 SystemFlatpak,
371 SystemSnap,
372 Other(String),
373}
374
375impl PathSource {
376 pub fn guess_from(path: &Path) -> PathSource {
380 let base_dirs = BaseDirectories::new().unwrap();
381 let data_home = base_dirs.get_data_home();
382
383 if path.starts_with("/usr/share") {
384 PathSource::System
385 } else if path.starts_with("/usr/local/share") {
386 PathSource::SystemLocal
387 } else if path.starts_with("/var/lib/flatpak") {
388 PathSource::SystemFlatpak
389 } else if path.starts_with("/var/lib/snapd") {
390 PathSource::SystemSnap
391 } else if path.starts_with("/nix/var/nix/profiles/default")
392 || path.starts_with("/nix/store")
393 {
394 PathSource::Nix
395 } else if path.to_string_lossy().contains("/flatpak/") {
396 PathSource::LocalFlatpak
397 } else if path.starts_with(data_home.as_path()) {
398 PathSource::Local
399 } else if path.starts_with("/nix/var/nix/profiles/per-user")
400 || path.to_string_lossy().contains(".nix")
401 {
402 PathSource::LocalNix
403 } else {
404 PathSource::Other(String::from("unknown"))
405 }
406 }
407}
408
409pub fn default_paths() -> impl Iterator<Item = PathBuf> {
415 let base_dirs = BaseDirectories::new().unwrap();
416 let mut data_dirs: Vec<PathBuf> = vec![];
417 data_dirs.push(base_dirs.get_data_home());
418 data_dirs.append(&mut base_dirs.get_data_dirs());
419
420 data_dirs.into_iter().map(|d| d.join("applications"))
421}
422
423pub(crate) fn dgettext(domain: &str, message: &str) -> String {
424 use gettextrs::{setlocale, LocaleCategory};
425 setlocale(LocaleCategory::LcAll, "");
426 gettextrs::dgettext(domain, message)
427}
428
429#[cached]
432pub fn get_languages_from_env() -> Vec<String> {
433 let mut l = Vec::new();
434
435 if let Ok(lang) = std::env::var("LANG") {
436 l.push(lang);
437 }
438
439 if let Ok(lang) = std::env::var("LANGUAGES") {
440 lang.split(':').for_each(|lang| {
441 l.push(lang.to_owned());
442 })
443 }
444
445 l
446}
447
448pub fn current_desktop() -> Option<Vec<String>> {
449 std::env::var("XDG_CURRENT_DESKTOP").ok().map(|x| {
450 let x = x.to_ascii_lowercase();
451 if x == "unity" {
452 vec!["gnome".to_string()]
453 } else {
454 x.split(':').map(|e| e.to_string()).collect()
455 }
456 })
457}
458
459#[test]
460fn add_field() {
461 let appid = "appid";
462 let de = DesktopEntry::from_appid(appid.to_string());
463
464 assert_eq!(de.appid, appid);
465 assert_eq!(de.name(&[] as &[&str]).unwrap(), appid);
466
467 let s = get_languages_from_env();
468
469 println!("{:?}", s);
470}
471
472#[test]
473fn env_with_locale() {
474 let locales = &["fr_FR"];
475
476 let de = DesktopEntry::from_path(
477 PathBuf::from("tests_entries/org.mozilla.firefox.desktop"),
478 Some(locales),
479 )
480 .unwrap();
481
482 assert_eq!(de.generic_name(locales).unwrap(), "Navigateur Web");
483
484 let locales = &["nb"];
485
486 assert_eq!(de.generic_name(locales).unwrap(), "Web Browser");
487}