#[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(),
);
}
}