gdritter repos xdg-desktop / master src / lib.rs
master

Tree @master (Download .tar.gz)

lib.rs @masterraw · history · blame

#[macro_use] extern crate failure;
#[macro_use] extern crate lazy_static;
extern crate regex;

use failure::Error;

use std::collections::HashMap;
use std::io::{Read};

#[derive(Debug)]
pub struct DesktopEntry {
    pub info: DesktopEntryInfo,
    pub typ: EntryType,
}

#[derive(Debug)]
pub enum EntryType {
    Application(ApplicationInfo),
    Link(String),
    Directory,
}

#[derive(Debug)]
pub struct ApplicationInfo {
    pub dbus_activatable: bool,
    pub try_exec: Option<String>,
    pub exec: Option<String>,
    pub path: Option<String>,
    pub terminal: bool,
    pub actions: Vec<Action>,
    pub mime_type: Vec<String>,
    pub categories: Vec<String>,
    pub implements: Vec<String>,
    pub keywords: Vec<String>,
    pub startup_notify: bool,
    pub startup_wm_class: Option<String>,
}

#[derive(Debug)]
pub struct Action {
    pub name: String,
    pub icon: Option<String>,
    pub exec: Option<String>,
}

impl Action {
    fn from_hashmap(bag: &mut HashMap<String, String>) -> Result<Action, Error> {
        let name = bag.remove("Name")
            .ok_or(format_err!("No Name!"))?;
        let icon = bag.remove("Icon");
        let exec = bag.remove("Exec");

        Ok(Action { name, icon, exec })
    }
}

impl ApplicationInfo {
    fn from_hashmap(
        bag: &mut HashMap<String, String>,
        rest: &mut parser::Bag,
    ) ->
        Result<ApplicationInfo, Error>
    {
        let exec = bag.remove("Exec");
        let path = bag.remove("Path");
        let try_exec = bag.remove("TryExec");


        let actions = bag.remove("Actions")
            .map_or_else(|| Vec::new(), parser::list_of_strings);
        let actions: Result<Vec<Action>, Error> =
            actions.into_iter().map(|a: String| {
            let key = format!("Desktop Action {}", a);
            let mut b = rest.remove(&key)
                .ok_or(format_err!("No action named {}", key))?;
            let r: Result<Action, Error> = Action::from_hashmap(&mut b);
            r
            }).collect();
        let actions = actions?;

        let mime_type = bag.remove("MimeType")
            .map_or_else(|| Vec::new(), parser::list_of_strings);
        let categories = bag.remove("Categories")
            .map_or_else(|| Vec::new(), parser::list_of_strings);
        let implements = bag.remove("Implements")
            .map_or_else(|| Vec::new(), parser::list_of_strings);
        let keywords = bag.remove("Keywords")
            .map_or_else(|| Vec::new(), parser::list_of_strings);

        let terminal = bag.remove("Terminal")
            .map_or_else(|| Ok(false), parser::to_bool)?;
        let dbus_activatable = bag.remove("DbusActivatable")
            .map_or_else(|| Ok(false), parser::to_bool)?;

        let startup_notify = bag.remove("StartupNotify")
            .map_or_else(|| Ok(false), parser::to_bool)?;
        let startup_wm_class = None;

        Ok(ApplicationInfo {
            dbus_activatable,
            try_exec,
            exec,
            path,
            terminal,
            actions,
            mime_type,
            categories,
            implements,
            keywords,
            startup_notify,
            startup_wm_class,
        })
    }
}

#[derive(Debug)]
pub struct DesktopEntryInfo {
    /// Version of the Desktop Entry Specification that the desktop
    /// entry conforms with. Entries that confirm with this version of
    /// the specification should use `1.1`. Note that the version
    /// field is not required to be present.
    pub version: Option<String>,
    /// Specific name of the application, for example "Mozilla".
    pub name: String,
    /// Generic name of the application, for example "Web Browser".
    pub generic_name: Option<String>,
    /// `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).
    pub no_display: bool,
    /// Tooltip for the entry, for example "View sites on the
    /// Internet". The value should not be redundant with the values
    /// of `Name` and `GenericName`.
    pub comment: Option<String>,
    /// 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 will be used to locate the icon.
    pub icon: Option<String>,
    pub hidden: bool,
    pub only_show_in: Vec<String>,
    pub not_show_in: Vec<String>,
}


mod parser {
    use std::collections::HashMap;
    use std::io::{BufRead, BufReader, Read};
    use std::mem;

    use failure::Error;
    use regex::Regex;

    pub type Bag = HashMap<String, HashMap<String, String>>;

    pub fn bags_from_file<R: Read>(reader: &mut R) -> Result<Bag, Error> {
        lazy_static! {
            static ref SECTION: Regex =
                Regex::new(r"\[([A-Za-z0-9 -]*)\]").unwrap();
            static ref KV: Regex =
                Regex::new(r"([A-Za-z0-9-]*(\[[A-Za-z@_]*\])?)=(.*)").unwrap();
        }

        let reader = BufReader::new(reader);
        let mut bags = HashMap::new();
        let mut current_section = None;
        let mut current_bag = HashMap::new();

        for ln in reader.lines() {
            let ln = ln?;

            if let Some(kv) = KV.captures(&ln) {
                current_bag.insert(kv[1].to_owned(), kv[3].to_owned());
            } else if let Some(s) = SECTION.captures(&ln) {
                if let Some(name) = current_section.take() {
                    let bag = mem::replace(&mut current_bag, HashMap::new());
                    bags.insert(name, bag);
                }
                current_section = Some(s[1].to_owned());
            }
        }

        if let Some(name) = current_section.take() {
            bags.insert(name, current_bag);
        }

        Ok(bags)
    }

    pub fn list_of_strings(str: String) -> Vec<String> {
        str.split(";").filter(|s| s.len() > 0).map(|s| s.to_owned()).collect()
    }

    pub fn to_bool(str: String) -> Result<bool, Error> {
        match str.as_ref() {
            "true" => Ok(true),
            "false" => Ok(false),
            _ => Err(format_err!("Invalid value for boolean field: {}", str)),
        }
    }
}

impl DesktopEntryInfo {
    pub fn from_bag(bags: &mut parser::Bag) -> Result<DesktopEntryInfo, Error> {
        let bag = bags.get_mut("Desktop Entry")
            .ok_or(format_err!("No Desktop Entry"))?;

        let version = bag.remove("Version");

        let name = bag.remove("Name")
            .ok_or(format_err!("No name"))?
            .to_owned();
        let generic_name = bag.remove("Generic Name");
        let comment = bag.remove("Comment");
        let icon = bag.remove("Icon");

        let no_display = bag.remove("No Display")
            .map_or_else(|| Ok(false), parser::to_bool)?;
        let hidden = bag.remove("Hidden")
            .map_or_else(|| Ok(false), parser::to_bool)?;

        let only_show_in = bag.remove("OnlyShowIn")
            .map_or_else(|| Vec::new(), parser::list_of_strings);
        let not_show_in = bag.remove("NotShowIn")
            .map_or_else(|| Vec::new(), parser::list_of_strings);

        let info = DesktopEntryInfo {
            version,
            name,
            generic_name,
            no_display,
            comment,
            icon,
            hidden,
            only_show_in,
            not_show_in,
        };

        Ok(info)
    }
}

impl DesktopEntry {
    pub fn is_application(&self) -> bool {
        match self.typ {
            EntryType::Application(_) => true,
            _ => false,
        }
    }

    pub fn is_directory(&self) -> bool {
        match self.typ {
            EntryType::Directory => true,
            _ => false,
        }
    }

    pub fn is_link(&self) -> bool {
        match self.typ {
            EntryType::Link(_) => true,
            _ => false,
        }
    }

    pub fn from_file<R: Read>(reader: &mut R) -> Result<DesktopEntry, Error> {
        let mut bags = parser::bags_from_file(reader)?;
        DesktopEntry::from_bags(&mut bags)
    }

    fn from_bags(bags: &mut parser::Bag) -> Result<DesktopEntry, Error> {
        let info = DesktopEntryInfo::from_bag(bags)?;
        let mut bag = bags.remove("Desktop Entry")
            .ok_or(format_err!("No Desktop Entry"))?;
        let typ = bag.remove("Type")
            .ok_or(format_err!("No Type"))?;
        let typ = match typ.as_ref() {
            "Application" => {
                let app = ApplicationInfo::from_hashmap(&mut bag, bags)?;
                EntryType::Application(app)
            }
            "Link" => {
                let url = bag.remove("URL")
                    .ok_or(format_err!("No URL"))?;
                EntryType::Link(url)
            }
            "Directory" => EntryType::Directory,
            r => Err(format_err!("Bad type: {}", r))?,
        };
        Ok(DesktopEntry { info, typ })
    }
}


#[cfg(test)]
mod tests {
    #[test]
    fn firefox_example() {
        let firefox_example = include_str!("../test_cases/firefox.desktop");
        let mut f = ::std::io::Cursor::new(firefox_example);
        let mut entry = ::parser::bags_from_file(&mut f).unwrap();
        assert_eq!(
            entry["Desktop Entry"]["Name"],
            "Firefox".to_owned(),
        );

        let info = ::DesktopEntryInfo::from_bag(&mut entry).unwrap();
        println!("Got: {:#?}", info);
        assert_eq!(
            info.name,
            "Firefox".to_owned(),
        );
    }

    #[test]
    fn steam_example() {
        let steam_example = include_str!("../test_cases/steam.desktop");
        let mut f = ::std::io::Cursor::new(steam_example);
        let entry = ::DesktopEntry::from_file(&mut f).unwrap();
        println!("got {:#?}", entry);
        assert_eq!(
            entry.info.name,
            "Steam".to_owned(),
        );
    }
}