use std::fs::read_to_string; use std::path::PathBuf; use freedesktop_desktop_entry::{DesktopEntry, Iter}; use once_cell::sync::Lazy; use regex::Regex; use rmenu_plugin::{Action, Entry, Method}; mod icons; static XDG_DATA_ENV: &'static str = "XDG_DATA_DIRS"; static XDG_CONFIG_ENV: &'static str = "XDG_CONFIG_HOME"; static XDG_DATA_DEFAULT: &'static str = "/usr/share:/usr/local/share"; static XDG_CONFIG_DEFAULT: &'static str = "~/.config"; static EXEC_RGX: Lazy = Lazy::new(|| Regex::new(r"%\w").expect("Failed to Build Exec Regex")); /// Retrieve XDG-CONFIG-HOME Directory #[inline] fn config_dir() -> PathBuf { let path = std::env::var(XDG_CONFIG_ENV).unwrap_or_else(|_| XDG_CONFIG_DEFAULT.to_string()); PathBuf::from(shellexpand::tilde(&path).to_string()) } /// Retrieve XDG-DATA Directories fn data_dirs(dir: &str) -> Vec { std::env::var(XDG_DATA_ENV) .unwrap_or_else(|_| XDG_DATA_DEFAULT.to_string()) .split(":") .map(|p| shellexpand::tilde(p).to_string()) .map(PathBuf::from) .map(|p| p.join(dir.to_owned())) .filter(|p| p.exists()) .collect() } /// Modify Exec Statements to Remove %u/%f/etc... #[inline(always)] fn fix_exec(exec: &str) -> String { EXEC_RGX.replace_all(exec, "").trim().to_string() } /// Parse XDG Desktop Entry into RMenu Entry fn parse_desktop(path: &PathBuf, locale: Option<&str>) -> Option { let bytes = read_to_string(path).ok()?; let entry = DesktopEntry::decode(&path, &bytes).ok()?; let name = entry.name(locale)?.to_string(); let icon = entry.icon().map(|i| i.to_string()); let comment = entry.comment(locale).map(|s| s.to_string()); let terminal = entry.terminal(); let mut actions = match entry.exec() { Some(exec) => vec![Action { name: "main".to_string(), exec: Method::new(fix_exec(exec), terminal), comment: None, }], None => vec![], }; actions.extend( entry .actions() .unwrap_or("") .split(";") .into_iter() .filter(|a| a.len() > 0) .filter_map(|a| { let name = entry.action_name(a, locale)?; let exec = entry.action_exec(a)?; Some(Action { name: name.to_string(), exec: Method::new(fix_exec(exec), terminal), comment: None, }) }), ); Some(Entry { name, actions, comment, icon, }) } /// Assign XDG Icon based on Desktop-Entry fn assign_icon(icon: String, map: &icons::IconMap) -> Option { if !icon.contains("/") { if let Some(icon) = map.get(&icon) { if let Some(path) = icon.to_str() { return Some(path.to_owned()); } } } Some(icon) } fn main() { let locale = Some("en"); let sizes = vec![64, 32, 96, 22, 128]; // collect icons let cfg = config_dir(); let spec = icons::IconSpec::standard(&cfg, sizes); let icons = icons::collect_icons(spec); // collect applications let app_paths = data_dirs("applications"); let mut desktops: Vec = Iter::new(app_paths) .into_iter() .filter_map(|f| parse_desktop(&f, locale)) .map(|mut e| { e.icon = e.icon.and_then(|s| assign_icon(s, &icons)); e }) .collect(); desktops.sort_by_cached_key(|e| e.name.to_owned()); desktops .into_iter() .filter_map(|e| serde_json::to_string(&e).ok()) .map(|s| println!("{}", s)) .last(); }