Skip to main content

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}