use std::{ cmp::Ordering, collections::HashSet, error::Error, fs::{File, read_to_string}, hash::{Hash, Hasher}, io::{Error as io_error, ErrorKind}, path::Path, }; use serde::{self, Deserialize, Serialize}; use simd_json; use walkdir::WalkDir; use crate::utils::{inish, xdg}; #[derive(Serialize, Deserialize, Clone, Debug, Eq)] #[serde(rename_all = "snake_case")] pub struct Desktop { pub entry_type: String, pub name: String, pub no_display: bool, pub hidden: bool, pub exec: Option, pub url: Option, pub term: bool, pub keywords: Vec, } impl Desktop { fn parse(f: &Path) -> Result> { let s = read_to_string(f)?; let config = inish::parse(&s)?; match config.get("Desktop Entry") { Some(section) => Ok(Desktop { entry_type: section.get("Type").unwrap_or(&"").to_string(), name: section.get("Name").unwrap_or(&"").to_string(), term: section.get("Terminal").unwrap_or(&"") == &"true", no_display: section.get("NoDisplay").unwrap_or(&"") == &"true", hidden: section.get("Hidden").unwrap_or(&"") == &"true", exec: section.get("Exec").map(|x| x.to_string()), url: section.get("URL").map(|x| x.to_string()), keywords: section .get("Keywords") .map(|x| { x.split(';') .map(|y| y.trim().to_string()) .filter(|z| z != "") .collect() }) .unwrap_or_else(|| vec![]), }), None => Err(Box::new(io_error::new( ErrorKind::NotFound, "no desktop entry in file", ))), } } } impl Ord for Desktop { fn cmp(&self, other: &Self) -> Ordering { self.name.cmp(&other.name) } } impl PartialOrd for Desktop { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for Desktop { fn eq(&self, other: &Self) -> bool { self.name == other.name } } // Must agree with PartialEq: equal entries (same name) must hash equally. impl Hash for Desktop { fn hash(&self, state: &mut H) { self.name.hash(state) } } pub fn load_desktop_cache() -> Result, Box> { match File::open(format!("{}/wldash/desktop.json", xdg::cache_folder())) { Ok(f) => simd_json::from_reader(f).map_err(|e| e.into()), Err(e) => Err(e.into()), } } pub fn write_desktop_cache(v: &Vec) -> Result<(), Box> { match File::create(format!("{}/wldash/desktop.json", xdg::cache_folder())) { Ok(f) => simd_json::to_writer(f, v).map_err(|e| e.into()), Err(e) => Err(e.into()), } } pub fn load_desktop_files() -> Vec { let dirs = xdg::data_folders(); let mut desktop: Vec = Vec::new(); let mut seen: HashSet = HashSet::new(); for dir in dirs { for entry in WalkDir::new(format!("{}/applications", dir)) .into_iter() .flatten() { // Entries are often symlinks, so only rule out directories. if entry.file_type().is_dir() || entry.path().extension().and_then(|e| e.to_str()) != Some("desktop") { continue; } { if let Ok(d) = Desktop::parse(entry.path()) { if d.hidden || d.no_display || (d.entry_type != "Application" && d.entry_type != "Link") { continue; } if !seen.contains(&d.name) { seen.insert(d.name.to_string()); desktop.push(d); } } } } } desktop }