xdgkit/desktop_entry.rs
1/*!
2# Desktop Entry
3
4This is the rustification of the desktop entry specifications!
5*/
6
7// desktop_entry.rs
8// Rusified in 2021 Copyright Israel Dahl. All rights reserved.
9//
10// /VVVV\
11// /V V\
12// /V V\
13// / 0 0 \
14// \|\|\</\/\>/|/|/
15// \_/\_/
16//
17// This program is free software; you can redistribute it and/or modify
18// it under the terms of the GNU General Public License version 2 as
19// published by the Free Software Foundation.
20//
21// This program is distributed in the hope that it will be useful,
22// but WITHOUT ANY WARRANTY; without even the implied warranty of
23// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24// GNU General Public License for more details.
25//
26// You should have received a copy of the GNU General Public License
27// along with this program; if not, write to the Free Software
28// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29
30extern crate tini;
31use tini::Ini;
32use std::fmt;
33// could just say use crate::utils::*
34use crate::utils::get_language;
35use crate::utils::to_bool;
36
37use crate::categories::*;
38use self::DesktopEnvironment::*;
39use std::slice::Iter;
40#[allow(dead_code)]
41#[derive(Debug, Clone, Copy)]
42/// Registered DesktopEnvironment Environments
43pub enum DesktopEnvironment {
44 /// Gnome Desktop
45 Gnome,
46 /// Gnome Classic Desktop
47 GnomeClassic,
48 /// Gnome Flashback Desktop
49 GnomeFlashback,
50 /// Kde Desktop
51 Kde,
52 /// Lxde Desktop
53 Lxde,
54 /// Lxqt Desktop
55 Lxqt,
56 /// MATÉ Desktop
57 Mate,
58 /// Razor-qt Desktop
59 Razor,
60 /// Rox Desktop
61 Rox,
62 /// Trinity Desktop
63 Tde,
64 /// Unity Shell
65 Unity,
66 /// Xfce Desktop
67 Xfce,
68 /// Ede Desktop
69 Ede,
70 /// Cinnamon Desktop
71 Cinnamon,
72 /// Pantheon Desktop
73 Pantheon,
74 /// Legacy menu systems
75 Old,
76 /// This is for random people making whatever they want... `Unknown` is similar to Desktop Entry's `type`
77 Unknown,
78}
79impl Default for DesktopEnvironment {
80 fn default() -> Self {
81 Self::Unknown
82 }
83}
84impl DesktopEnvironment {
85 #[allow(dead_code)]
86 /// This is to allow people to iterate over the `enum` nicely
87 pub fn iter() -> Iter<'static, DesktopEnvironment> {
88 static ONLYSHOWIN:[DesktopEnvironment; 17] = [Gnome, GnomeClassic, GnomeFlashback, Kde, Lxde, Lxqt, Mate, Razor, Rox, Tde, Unity, Xfce, Ede, Cinnamon, Pantheon, Old, Unknown];
89 ONLYSHOWIN.iter()
90 }
91 #[allow(dead_code)]
92 /// Take a String and return a `DesktopEnvironment`
93 pub fn from_string(item:String) -> DesktopEnvironment {
94 if item == "GNOME" {
95 return DesktopEnvironment::Gnome
96 } else if item == "GNOMEClassic" {
97 return DesktopEnvironment::GnomeClassic
98 } else if item == "GNOMEFlashback" {
99 return DesktopEnvironment::GnomeFlashback
100 } else if item == "KDE" {
101 return DesktopEnvironment::Kde
102 } else if item == "LXDE" {
103 return DesktopEnvironment::Lxde
104 } else if item == "LXQT" {
105 return DesktopEnvironment::Lxqt
106 } else if item == "MATE" {
107 return DesktopEnvironment::Mate
108 } else if item == "Razor" {
109 return DesktopEnvironment::Razor
110 } else if item == "ROX" {
111 return DesktopEnvironment::Rox
112 } else if item == "TDE" {
113 return DesktopEnvironment::Tde
114 } else if item == "Unity" {
115 return DesktopEnvironment::Unity
116 } else if item == "XFCE" {
117 return DesktopEnvironment::Xfce
118 } else if item == "EDE" {
119 return DesktopEnvironment::Ede
120 } else if item == "Cinnamon" {
121 return DesktopEnvironment::Cinnamon
122 } else if item == "Pantheon" {
123 return DesktopEnvironment::Pantheon
124 } else if item == "Old" {
125 return DesktopEnvironment::Old
126 }
127 DesktopEnvironment::Unknown
128 }
129}
130impl fmt::Display for DesktopEnvironment {
131 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132 let v:String = match *self {
133 DesktopEnvironment::Gnome => "GNOME".to_string(),
134 DesktopEnvironment::GnomeClassic => "GNOMEClassic".to_string(),
135 DesktopEnvironment::GnomeFlashback => "GNOMEFlashback".to_string(),
136 DesktopEnvironment::Kde => "KDE".to_string(),
137 DesktopEnvironment::Lxde => "LXDE".to_string(),
138 DesktopEnvironment::Lxqt => "LXQT".to_string(),
139 DesktopEnvironment::Mate => "MATE".to_string(),
140 DesktopEnvironment::Razor => "Razor".to_string(),
141 DesktopEnvironment::Rox => "ROX".to_string(),
142 DesktopEnvironment::Tde => "TDE".to_string(),
143 DesktopEnvironment::Unity => "Unity".to_string(),
144 DesktopEnvironment::Xfce => "XFCE".to_string(),
145 DesktopEnvironment::Ede => "EDE".to_string(),
146 DesktopEnvironment::Cinnamon => "Cinnamon".to_string(),
147 DesktopEnvironment::Pantheon => "Pantheon".to_string(),
148 DesktopEnvironment::Old => "Old".to_string(),
149 DesktopEnvironment::Unknown => "Unknown".to_string(),
150 };
151 write!(f, "{:?}", v)
152 }
153}
154
155/// This specification defines 3 types of desktop entries:
156/// * Application (type 1),
157/// * Link (type 2)
158/// * Directory (type 3)
159///
160/// To allow the addition of new types in the future, implementations should ignore desktop entries with an "unknown" type.
161#[derive(Debug, Clone, Copy)]
162pub enum DesktopType {
163 /// A program
164 Application,
165 /// A website
166 Link,
167 /// A file system directory
168 Directory,
169 /// Ignore 'Unknown' type in your program, it really just means it is anything other than the above known types
170 Unknown,
171}
172impl DesktopType {
173 /// Convert a `String` into a `DesktopType`
174 pub fn from_string(dt:String)->DesktopType {
175 if dt == "Application" {
176 return DesktopType::Application
177 } else if dt == "Link" {
178 return DesktopType::Link
179 } else if dt == "Directory" {
180 return DesktopType::Directory
181 }
182 DesktopType::Unknown
183 }
184 /// Convert a `DesktopType` into a `String`
185 pub fn to_string(dt:DesktopType)->String {
186 match dt {
187 DesktopType::Application => String::from("Application"),
188 DesktopType::Link => String::from("Link"),
189 DesktopType::Directory => String::from("Directory"),
190 _=> String::from(""),
191 }
192}
193}
194impl fmt::Display for DesktopType {
195 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
196 write!(f, "{:?}", self)
197 }
198}
199
200/// This function converts a Option<String> to a DesktopType
201#[allow(dead_code)]
202pub fn convert_xdg_type(xdg_type_option:Option<String>)->DesktopType {
203 if let Some(dt) = xdg_type_option {
204 if dt == "Application" {
205 return DesktopType::Application
206 }
207 else if dt == "Link" {
208 return DesktopType::Link
209 }
210 else if dt == "Directory" {
211 return DesktopType::Directory
212 }
213 }
214 DesktopType::Unknown
215}
216
217/// This function will return a Option<String> from a DesktopType
218#[allow(dead_code)]
219pub fn string_xdg_type(dt:DesktopType)->Option<String> {
220 match dt {
221 DesktopType::Application => Some(String::from("Application")),
222 DesktopType::Link => Some(String::from("Link")),
223 DesktopType::Directory => Some(String::from("Directory")),
224 _=> None,
225 }
226}
227
228/// # Desktop Entry Files
229///
230///
231/// `Option<String>` will be used for all localeString/String
232///
233/// `Option<Vec<String>>` for all localeString(s)/String(s)
234///
235/// `Option<bool>` will be used for all boolean
236
237///
238/// `f32` for version number
239///
240/// `DesktopType` enumerates known "Type" fields and the "Unknown" reserved "Type"
241///
242/// `type` is a **reserved** word in rust, so it is called 'xdg_type'
243///
244/// I copied and pasted the website here and made things rustified
245///
246/// No camelcase inside structs, all lowercase/underscore version of CamelCase
247///
248/// The text following is from the XDG webpage:
249///
250/// # Recognized desktop entry keys
251///
252/// Keys are either OPTIONAL or REQUIRED. If a key is OPTIONAL it may or may not be present in the file. However, if it isn't, the implementation of the standard should not blow up, it must provide some sane defaults.
253///
254/// Some keys only make sense in the context when another particular key is also present and set to a specific value. Those keys should not be used if the particular key is not present or not set to the specific value. For example, the Terminal key can only be used when the value of the Type key is Application.
255///
256/// If a REQUIRED key is only valid in the context of another key set to a specific value, then it has to be present only if the other key is set to the specific value. For example, the URL key has to be present when and only when when the value of the Type key is Link.
257#[derive(Debug, Clone)]
258pub struct DesktopEntry {
259//type is a reserved word in rust and other languages so we will use xdg_type
260 /// This specification defines 3 types of desktop entries: Application (type 1), Link (type 2) and Directory (type 3). To allow the addition of new types in the future, implementations should ignore desktop entries with an unknown type.
261 pub xdg_type:DesktopType,
262 /// Version of the Desktop Entry Specification that the desktop entry conforms with. Entries that confirm with this version of the specification should use 1.5. Note that the version field is not required to be present.
263 pub version:String,
264 /// Specific name of the application, for example "Mozilla".
265 pub name:Option<String>,
266 /// Generic name of the application, for example "Web Browser".
267 pub generic_name:Option<String>,
268 /// NoDisplay means "this application exists, but don't display it in the menus". This can be useful to e.g. associate this application with MIME types, so that it gets launched from a file manager (or other apps), without having a menu entry for it (there are tons of good reasons for this, including e.g. the netscape -remote, or kfmclient openURL kind of stuff).
269 pub no_display:Option<bool>,
270 /// Tooltip for the entry, for example "View sites on the Internet". The value should not be redundant with the values of Name and GenericName.
271 pub comment:Option<String>,
272 /// Icon to display in file manager, menus, etc. If the name is an absolute path, the given file will be used. If the name is not an absolute path, the algorithm described in the [Icon Theme Specification](https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup) will be used to locate the icon.
273 pub icon:Option<String>,
274 /// Hidden should have been called Deleted. It means the user deleted (at his level) something that was present (at an upper level, e.g. in the system dirs). It's Stringictly equivalent to the .desktop file not existing at all, as far as that user is concerned. This can also be used to "uninstall" existing files (e.g. due to a renaming) - by letting make install install a file with Hidden=true in it.
275 pub hidden:Option<bool>,
276 /// A list of Strings identifying the desktop environments that should display/not display a given desktop entry.
277/// By default, a desktop file should be shown, unless an OnlyShowIn key is present, in which case, the default is for the file not to be shown.
278/// If $XDG_CURRENT_DESKTOP is set then it contains a colon-separated list of Strings. In order, each Stringing is considered. If a matching entry is found in OnlyShowIn then the desktop file is shown. If an entry is found in NotShowIn then the desktop file is not shown. If none of the Strings match then the default action is taken (as above).
279/// $XDG_CURRENT_DESKTOP should have been set by the login manager, according to the value of the DesktopNames found in the session file. The entry in the session file has multiple values separated in the usual way: with a semicolon.
280/// The same desktop name may not appear in both OnlyShowIn and NotShowIn of a group.
281 pub only_show_in:Option<Vec<DesktopEnvironment>>,
282 pub not_show_in:Option<Vec<DesktopEnvironment>>,
283 /// A boolean value specifying if D-Bus activation is supported for this application. If this key is missing, the default value is false. If the value is true then implementations should ignore the Exec key and send a D-Bus message to launch the application. See [D-Bus Activation](https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s08.html) for more information on how this works. Applications should still include Exec= lines in their desktop files for compatibility with implementations that do not understand the DBusActivatable key.
284 pub dbus_activatable:Option<bool>,
285 /// Path to an executable file on disk used to determine if the program is actually installed. If the path is not an absolute path, the file is looked up in the $PATH environment variable. If the file is not present or if it is not executable, the entry may be ignored (not be used in menus, for example).
286 pub try_exec:Option<String>,
287 /// Program to execute, possibly with arguments. See the [Exec](https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html) key for details on how this key works. The Exec key is required if DBusActivatable is not set to true. Even if DBusActivatable is true, Exec should be specified for compatibility with implementations that do not understand DBusActivatable.
288 pub exec:Option<String>,
289 /// If entry is of type Application, the working directory to run the program in.
290 pub path:Option<String>,
291 /// Whether the program runs in a terminal window.
292 pub terminal:Option<bool>,
293 /// Identifiers for application actions. This can be used to tell the application to make a specific action, different from the default behavior. The [Application actions section](https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s11.html) describes how actions work.
294 pub actions:Option<Vec<String>>,
295 /// The MIME type(s) supported by this application.
296 pub mime_type:Option<Vec<String>>,
297 /// Categories in which the entry should be shown in a menu for possible values see the [Desktop Menu Specification](http://www.freedesktop.org/Standards/menu-spec).
298 pub categories:Option<Vec<Categories>>,
299 /// A list of interfaces that this application implements. By default, a desktop file implements no interfaces. See [Interface]([https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s09.html) for more information on how this works.
300 pub implements:Option<Vec<String>>,
301 /// A list of Strings which may be used in addition to other metadata to describe this entry. This can be useful e.g. to facilitate searching through entries. The values are not meant for display, and should not be redundant with the values of Name or GenericName.
302 pub keywords:Option<Vec<String>>,
303 /// If true, it is KNOWN that the application will send a "remove" message when started with the DESKTOP_STARTUP_ID environment variable set. If false, it is KNOWN that the application does not work with startup notification at all (does not shown any window, breaks even when using StartupWMClass, etc.). If absent, a reasonable handling is up to implementations (assuming false, using StartupWMClass, etc.). See the [Startup Notification Protocol Specification](http://www.freedesktop.org/Standards/startup-notification-spec) for more details.
304 pub startup_notify:Option<bool>,
305 /// If specified, it is known that the application will map at least one window with the given Stringing as its WM class or WM name hint see the [Startup Notification Protocol Specification](http://www.freedesktop.org/Standards/startup-notification-spec) for more details.
306 pub startup_wm_class:Option<String>,
307 /// If entry is Link type, the URL to access.
308 pub url:Option<String>,
309 /// If true, the application prefers to be run on a more powerful discrete GPU if available, which we describe as “a GPU other than the default one” in this spec to avoid the need to define what a discrete GPU is and in which cases it might be considered more powerful than the default GPU. This key is only a hint and support might not be present depending on the implementation.
310 pub prefers_non_default_gpu:Option<bool>,
311}
312
313/// Implementations
314impl DesktopEntry {
315 /// Creates a blank entry with a specified type
316 pub fn empty(initial_type:DesktopType)->Self where Self:Sized {
317 // nothing to see here
318 DesktopEntry {
319 xdg_type:initial_type,
320 version:"1.0".into(),
321 name:None,
322 generic_name:None,
323 no_display:None,
324 comment:None,
325 icon:None,
326 hidden:None,
327 only_show_in:None,
328 not_show_in:None,
329 dbus_activatable:None,
330 try_exec:None,
331 exec:None,
332 path:None,
333 terminal:None,
334 actions:None,
335 mime_type:None,
336 categories:None,
337 implements:None,
338 keywords:None,
339 startup_notify:None,
340 startup_wm_class:None,
341 url:None,
342 prefers_non_default_gpu:None,
343 }
344 }
345 /// Creates a 'new' DesktopEntry from reading a file
346 /// note: xdgkit does not check the file extention, you *may* want to
347 /// This reads not only `.desktop` files but also `.directory`
348 pub fn new(file_name:String)->Self where Self:Sized {
349 if let Ok(file_string) = std::fs::read_to_string(file_name.as_str()) {
350 return Self::read(file_string)
351 }
352 Self::default()
353 }
354 /// Creates a 'new' DesktopEntry from reading a file
355 /// note: xdgkit does not check the file extention, you *may* want to
356 /// This reads not only `.desktop` files but also `.directory`
357 pub fn read(file_string:String)->Self where Self:Sized {
358 let test_ini = Ini::from_string(file_string.as_str());
359 if test_ini.is_err() {
360 println!("ERROR!!! {:?} in {}",test_ini,file_string);
361 return Self::empty(DesktopType::Application)
362 }
363 let conf = test_ini.unwrap();
364
365 let section = "Desktop Entry";
366 let mut locale:bool = false;
367 let lang_var: Option<String> = get_language();
368 if lang_var.is_some() {
369 locale = true;
370 }
371 let lang:String = lang_var.unwrap_or_else(||String::from(""));
372 //TODO
373 let mut lang_two:String = lang.to_owned();
374 let mut use_two_lang:bool = false;
375 let pos = lang_two.chars().position(|c| c == '_');
376 if let Some(posi) = pos {
377 use_two_lang = true;
378 if posi < lang_two.len() {
379 // throw away variable for excess of trim
380 let _junk = lang_two.split_off(posi);
381 }
382 }
383
384 //Populate our struct
385 let dt:Option<String> = conf.get(section, "Type");
386 let mut ver:Option<String> = conf.get(section, "Version");
387 let nd:Option<String> = conf.get(section, "NoDisplay");
388 let com:Option<String> = conf.get(section,"Comment");
389 let ic:Option<String> = conf.get(section, "Icon");
390 let hid:Option<String> = conf.get(section, "Hidden");
391 let only:Option<Vec<String>> = conf.get_vec_with_sep(section, "OnlyShowIn",";");
392 let not:Option<Vec<String>> = conf.get_vec_with_sep(section, "NotShowIn",";");
393 let dbus:Option<String> = conf.get(section, "DBusActivatable");
394 let tex:Option<String> = conf.get(section, "TryExec");
395 let ex:Option<String> = conf.get(section, "Exec");
396 let pth:Option<String> = conf.get(section, "Path");
397 let term:Option<String> = conf.get(section, "Terminal");
398 // TODO get_vec_with_sep does not work.... :(
399 let act:Option<Vec<String>> = conf.get_vec_with_sep(section, "Actions",";");
400 let mime:Option<Vec<String>> = conf.get_vec_with_sep(section, "MimeType",";");
401 let cat:Option<Vec<String>> = conf.get_vec_with_sep(section, "Categories",";");
402 //println!("cats:{:?}", cat.clone());
403 let imp:Option<Vec<String>> = conf.get_vec_with_sep(section, "Implements",";");
404 let start:Option<String> = conf.get(section,"StartupNotify");
405 let wm:Option<String> = conf.get(section,"StartupWMClass");
406 let ur:Option<String> = conf.get(section,"URL");
407 let gpu:Option<String> = conf.get(section,"PrefersNonDefaultGPU");
408 //LOCALE
409 let nom:Option<String> = conf.get(section,"Name"); //why was is 'ref' again?
410 let gen_nom:Option<String> = conf.get(section, "GenericName");
411 let keyw:Option<Vec<String>> = conf.get_vec_with_sep(section,"Keywords",";");
412
413// these are the 'return' variables for the struct
414 let mut local_name:Option<String> = None;
415 let mut local_gen:Option<String> = None;
416 let mut local_key:Option<Vec<String>> = None;
417
418 // Need to parse for locale specific strings
419 if locale {
420
421 // NAME
422 let item:String = format!("{}{}{}{}","Name","[",lang,"]");
423 let attempt_n: Option<String> = conf.get(section,item.as_str());
424 if attempt_n.is_none() {
425 if use_two_lang {
426 let itm2:String = format!("{}{}{}{}","Name","[",lang_two,"]");
427 let attmpt2: Option<String> = conf.get(section, itm2.as_str());
428 if attmpt2.is_some() {
429 local_name = attmpt2;
430 }
431 }
432 }
433 else {
434 local_name = attempt_n;
435 }
436 if local_name.is_none() {
437 local_name = nom;
438 }
439
440 // GENERIC NAME
441 let item1:String = format!("{}{}{}{}","GenericName","[",lang,"]");
442 let attempt_gn: Option<String> = conf.get(section,item1.as_str());
443 if attempt_gn.is_none() {
444 if use_two_lang {
445 let itm:String = format!("{}{}{}{}","GenericName","[",lang_two,"]");
446 let attmpt3: Option<String> = conf.get(section, itm.as_str());
447 if attmpt3.is_some() {
448 local_gen = attmpt3;
449 }
450 }
451 }
452 else {
453 local_gen = attempt_gn;
454 }
455 if local_gen.is_none() {
456 local_gen = gen_nom;
457 }
458
459 // KEYWORDS
460 let itm1:String = format!("{}{}{}{}","Keywords","[",lang,"]");
461 let attempt_k: Option<Vec<String>> = conf.get_vec_with_sep(section,itm1.as_str(),";");
462 if attempt_k.is_none() {
463 if use_two_lang {
464 let it3m:String = format!("{}{}{}{}","Keywords","[",lang_two,"]");
465 let attm3pt: Option<Vec<String>> = conf.get_vec_with_sep(section, it3m.as_str(),";");
466 if attm3pt.is_some() {
467 local_key = attm3pt;
468 }
469 }
470 }
471 else {
472 local_key = attempt_k;
473 }
474 if local_key.is_none() {
475 local_key = keyw;
476 }
477 }
478 if ver.is_none() {
479 ver = Some(String::from("1.0"));// 1.5/1.0 ???
480 }
481 // make Category enums
482 let mut cats:Vec<Categories> =vec![];
483 if cat.is_some() {
484 for item in cat.unwrap() {
485 cats.push(Categories::from_string(item));
486 }
487 }
488 //make only show in enums
489 let mut onlyshow:Vec<DesktopEnvironment> =vec![];
490 if let Some(onlys) = only {
491 for item in onlys {
492 onlyshow.push(DesktopEnvironment::from_string(item));
493 }
494 }
495 //make not show in enums
496 let mut notshow:Vec<DesktopEnvironment> =vec![];
497 if let Some(nots) = not {
498 for item in nots {
499 notshow.push(DesktopEnvironment::from_string(item));
500 }
501 }
502 // check emptiness, before trying anying XD
503 let mut categories:Option<Vec<Categories>> = None;
504 if !cats.is_empty() {
505 categories = Some(cats);
506 }
507 let mut only_show:Option<Vec<DesktopEnvironment>> = None;
508 if !onlyshow.is_empty() {
509 only_show = Some(onlyshow);
510 }
511 let mut not_show:Option<Vec<DesktopEnvironment>> = None;
512 if !notshow.is_empty() {
513 not_show = Some(notshow);
514 }
515 // blast off!
516 DesktopEntry {
517 xdg_type:convert_xdg_type(dt),
518 version:ver.unwrap(),
519 name:local_name,
520 generic_name:local_gen,
521 no_display:to_bool(nd),
522 comment:com,
523 icon:ic,
524 hidden:to_bool(hid),
525 only_show_in:only_show,
526 not_show_in:not_show,
527 dbus_activatable:to_bool(dbus),
528 try_exec:tex,
529 exec:ex,
530 path:pth,
531 terminal:to_bool(term),
532 actions:act,
533 mime_type:mime,
534 categories:categories,
535 implements:imp,
536 keywords:local_key,
537 startup_notify:to_bool(start),
538 startup_wm_class:wm,
539 url:ur,
540 prefers_non_default_gpu:to_bool(gpu),
541 }
542 }
543}
544impl Default for DesktopEntry {
545 fn default() -> Self {
546 Self::empty(DesktopType::Application)
547 }
548}