use std::{ any::type_name, fs::read_dir, path::PathBuf, sync::{ Arc, OnceLock, RwLock, atomic::{ AtomicBool, Ordering, }, }, }; use freedesktop_entry_parser as fd; use mlua::prelude::*; use notify::RecommendedWatcher; use rayon::prelude::*; use url::Url; use crate::lenses::{ Cache, Entries, Entry, Lense, }; static APPS_DIR: &'static str = "/usr/share/applications"; static WATCHER: OnceLock = OnceLock::new(); #[derive(Default)] pub struct Application { cache: RwLock, should_interrupt: AtomicBool, } impl Lense for Application { const NAME: &str = "Application"; const PREFIX: Option<&'static str> = None; fn init() -> Arc { let this = Arc::new(Application::default()); let watcher_this = this.clone(); WATCHER .set( notify::recommended_watcher(move |event| { match event { Ok(_) => { // We don't care what specifically changed, just that *it did* watcher_this.set_cache(Cache::Stale); } Err(err) => { eprintln!("Watch error: {:?}", err) } } }) .expect("Failed to instantiate a watcher"), ) .expect("Failed to set a watcher"); this } #[inline] fn set_cache(&self, cache: Cache) { if let Err(err) = self.cache.write().map(|mut place| *place = cache) { eprintln!( "Failed to write cache value for {}: {:?}", type_name::(), err ) } } #[inline] fn get_cache(&self) -> Cache { match self.cache.read() { Ok(ok) => ok.clone(), Err(err) => { eprintln!( "Failed to read cache value for {}: {:?}", type_name::(), err ); Cache::Stale } } } #[inline] fn set_interrupt(&self, interrupt: bool) { // self.should_interrupt.store(interrupt, Ordering::Relaxed) } #[inline] fn get_interrupt(&self) -> bool { false // self.should_interrupt.load(Ordering::Relaxed) } fn entries(&self, _: &Lua, _: &str) -> Result { let entries = read_dir(APPS_DIR)? .map(|result| result.map(|e| e.path())) .collect::, std::io::Error>>()?; let parsed_entries: Entries = entries .into_par_iter() .filter(|path| path.extension().is_some_and(|ext| ext == "desktop")) .filter_map(|path| parse_entry(path).ok().flatten()) .collect::>() .into(); Ok(parsed_entries) } } fn parse_entry(path: PathBuf) -> Result, ()> { let Ok(entry) = fd::parse_entry(&path) else { return Err(()); }; let section = entry.section("Desktop Entry"); let name = section.attr("Name").ok_or(())?.to_string(); if section.attr("Type").ok_or(())? != "Application" { return Err(()); } if section.attr("OnlyShowIn").is_some() || section.attr("Hidden").is_some() || section.attr("NoDisplay").is_some() { return Err(()); } let exec = section.attr("Exec").ok_or(())?.to_string(); let mut new_exec = exec.clone(); for (index, _) in exec.match_indices('%') { match exec.chars().nth(index + 1).unwrap().to_ascii_lowercase() { 'i' => { if let Some(icon) = section.attr("Icon") { new_exec.replace_range(index..index + 2, &format!("--icon {icon}")); } } 'c' => new_exec.replace_range(index..index + 2, &name), 'k' => new_exec.replace_range(index..index + 2, Url::from_file_path(&path)?.as_str()), 'f' | 'u' | 'v' | 'm' | 'd' | 'n' => new_exec.replace_range(index..index + 2, ""), _ => continue, } } Ok(Some(Entry { message: name, exec: Some(( new_exec, section .attr("Terminal") .unwrap_or("false") .parse() .map_err(drop)?, )), })) }