diff --git a/Cargo.lock b/Cargo.lock index 557cc10..1a644ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2907,12 +2907,14 @@ dependencies = [ name = "worf-hyprswitch" version = "0.1.0" dependencies = [ + "dirs 6.0.0", "env_logger", "freedesktop-icons", "hyprland", "log", "rayon", "sysinfo", + "toml", "worf", ] diff --git a/examples/worf-hyprswitch/Cargo.toml b/examples/worf-hyprswitch/Cargo.toml index d2eae3d..c065309 100644 --- a/examples/worf-hyprswitch/Cargo.toml +++ b/examples/worf-hyprswitch/Cargo.toml @@ -6,8 +6,10 @@ edition = "2024" [dependencies] worf = {path = "../../worf"} env_logger = "0.11.8" -log = "0.4.27" hyprland = "0.4.0-beta.2" sysinfo = "0.34.2" freedesktop-icons = "0.4.0" rayon = "1.10.0" +toml = "0.8.22" +log = "0.4.27" +dirs = "6.0.0" diff --git a/examples/worf-hyprswitch/src/main.rs b/examples/worf-hyprswitch/src/main.rs index 6325723..29e818d 100644 --- a/examples/worf-hyprswitch/src/main.rs +++ b/examples/worf-hyprswitch/src/main.rs @@ -1,35 +1,44 @@ -use std::{env, sync::Arc}; - use hyprland::{ dispatch::{DispatchType, WindowIdentifier}, prelude::HyprData, shared::Address, }; use rayon::prelude::*; +use std::collections::HashMap; +use std::{env, fs, sync::Arc, thread}; use sysinfo::{Pid, System}; use worf::{ + Error, config::{self, Config}, + desktop, desktop::EntryType, gui::{self, ItemProvider, MenuItem}, }; +#[derive(Clone)] +struct Window { + process: String, + address: Address, + icon: Option, +} + #[derive(Clone)] struct WindowProvider { - windows: Vec>, + windows: Vec>, } impl WindowProvider { - fn new(cfg: &Config) -> Result { + fn new(cfg: &Config, cache: &HashMap) -> Result { let clients = hyprland::data::Clients::get().map_err(|e| e.to_string())?; let clients: Vec<_> = clients.iter().cloned().collect(); - let desktop_files = Arc::new(worf::desktop::find_desktop_files()); + let desktop_files = Arc::new(desktop::find_desktop_files()); let mut sys = System::new_all(); sys.refresh_all(); let sys = Arc::new(sys); - let menu_items: Vec> = clients + let menu_items: Vec> = clients .par_iter() .filter_map(|c| { let sys = Arc::clone(&sys); @@ -40,40 +49,51 @@ impl WindowProvider { .map(|x| x.name().to_string_lossy().into_owned()); process_name.map(|process_name| { - let icon = freedesktop_icons::lookup(&process_name) - .with_size(cfg.image_size()) - .with_scale(1) - .find() - .map(|icon| icon.to_string_lossy().to_string()) - .or_else(|| { - desktop_files - .iter() - .find_map(|d| match &d.entry.entry_type { - EntryType::Application(app) => { - if app.startup_wm_class.as_ref().is_some_and(|wm_class| { - *wm_class.to_lowercase() - == c.initial_class.to_lowercase() - }) { - d.entry.icon.as_ref().map(|icon| icon.content.clone()) - } else { - None + let icon = cache.get(&process_name).cloned().or_else(|| { + freedesktop_icons::lookup(&process_name) + .with_size(cfg.image_size()) + .with_scale(1) + .find() + .map(|icon| icon.to_string_lossy().to_string()) + .or_else(|| { + desktop_files + .iter() + .find_map(|d| match &d.entry.entry_type { + EntryType::Application(app) => { + if app.startup_wm_class.as_ref().is_some_and( + |wm_class| { + *wm_class.to_lowercase() + == c.initial_class.to_lowercase() + }, + ) { + d.entry + .icon + .as_ref() + .map(|icon| icon.content.clone()) + } else { + None + } } - } - _ => None, - }) - }); + _ => None, + }) + }) + }); MenuItem::new( format!( "[{}] \t {} \t {}", c.workspace.name, c.initial_class, c.title ), - icon, + icon.clone(), None, vec![].into_iter().collect(), None, 0.0, - Some(c.address.to_string()), + Some(Window { + process: process_name, + address: c.address.clone(), + icon, + }), ) }) }) @@ -85,16 +105,33 @@ impl WindowProvider { } } -impl ItemProvider for WindowProvider { - fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { +impl ItemProvider for WindowProvider { + fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec>) { (false, self.windows.clone()) } - fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { + fn get_sub_elements(&mut self, _: &MenuItem) -> (bool, Option>>) { (false, None) } } +fn load_icon_cache(cache_path: &String) -> Result, Error> { + let toml_content = + fs::read_to_string(cache_path).map_err(|e| Error::UpdateCacheError(format!("{e}")))?; + let cache: HashMap = toml::from_str(&toml_content) + .map_err(|_| Error::ParsingError("failed to parse cache".to_owned()))?; + Ok(cache) +} + +fn cache_path() -> Result { + let path = dirs::cache_dir() + .map(|x| x.join("worf-hyprswitch")) + .ok_or_else(|| Error::UpdateCacheError("cannot read cache file".to_owned()))?; + + desktop::create_file_if_not_exists(&path)?; + Ok(path.to_string_lossy().into_owned()) +} + fn main() -> Result<(), String> { env_logger::Builder::new() .parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) @@ -104,15 +141,35 @@ fn main() -> Result<(), String> { let args = config::parse_args(); let config = config::load_config(Some(&args)).unwrap_or(args); - let provider = WindowProvider::new(&config)?; + let cache_path = cache_path().map_err(|err| err.to_string())?; + let mut cache = load_icon_cache(&cache_path).map_err(|e| e.to_string())?; + + let provider = WindowProvider::new(&config, &cache)?; + let windows = provider.windows.clone(); + let update_cache = thread::spawn(move || { + windows.iter().for_each(|item| { + if let Some(window) = &item.data { + if let Some(icon) = &window.icon { + cache.insert(window.process.clone(), icon.clone()); + } + } + }); + let updated_toml = toml::to_string(&cache); + match updated_toml { + Ok(toml) => { + fs::write(cache_path, toml).map_err(|e| Error::UpdateCacheError(e.to_string())) + } + Err(e) => Err(Error::UpdateCacheError(e.to_string())), + } + }); let result = gui::show(config, provider, false, None, None).map_err(|e| e.to_string())?; - if let Some(window_id) = result.menu.data { - Ok( - hyprland::dispatch::Dispatch::call(DispatchType::FocusWindow( - WindowIdentifier::Address(Address::new(window_id)), - )) - .map_err(|e| e.to_string())?, - ) + + if let Some(window) = result.menu.data { + hyprland::dispatch::Dispatch::call(DispatchType::FocusWindow(WindowIdentifier::Address( + window.address, + ))) + .map_err(|e| e.to_string())?; + Ok(update_cache.join().unwrap().map_err(|e| e.to_string())?) } else { Err("No window data found".to_owned()) }