hyprswitch cache images to improve startup times

This commit is contained in:
Alexander Mohr 2025-06-08 16:01:42 +02:00
parent beb9b9d1c3
commit d9ca8e62d8
3 changed files with 102 additions and 41 deletions

2
Cargo.lock generated
View file

@ -2907,12 +2907,14 @@ dependencies = [
name = "worf-hyprswitch" name = "worf-hyprswitch"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dirs 6.0.0",
"env_logger", "env_logger",
"freedesktop-icons", "freedesktop-icons",
"hyprland", "hyprland",
"log", "log",
"rayon", "rayon",
"sysinfo", "sysinfo",
"toml",
"worf", "worf",
] ]

View file

@ -6,8 +6,10 @@ edition = "2024"
[dependencies] [dependencies]
worf = {path = "../../worf"} worf = {path = "../../worf"}
env_logger = "0.11.8" env_logger = "0.11.8"
log = "0.4.27"
hyprland = "0.4.0-beta.2" hyprland = "0.4.0-beta.2"
sysinfo = "0.34.2" sysinfo = "0.34.2"
freedesktop-icons = "0.4.0" freedesktop-icons = "0.4.0"
rayon = "1.10.0" rayon = "1.10.0"
toml = "0.8.22"
log = "0.4.27"
dirs = "6.0.0"

View file

@ -1,35 +1,44 @@
use std::{env, sync::Arc};
use hyprland::{ use hyprland::{
dispatch::{DispatchType, WindowIdentifier}, dispatch::{DispatchType, WindowIdentifier},
prelude::HyprData, prelude::HyprData,
shared::Address, shared::Address,
}; };
use rayon::prelude::*; use rayon::prelude::*;
use std::collections::HashMap;
use std::{env, fs, sync::Arc, thread};
use sysinfo::{Pid, System}; use sysinfo::{Pid, System};
use worf::{ use worf::{
Error,
config::{self, Config}, config::{self, Config},
desktop,
desktop::EntryType, desktop::EntryType,
gui::{self, ItemProvider, MenuItem}, gui::{self, ItemProvider, MenuItem},
}; };
#[derive(Clone)]
struct Window {
process: String,
address: Address,
icon: Option<String>,
}
#[derive(Clone)] #[derive(Clone)]
struct WindowProvider { struct WindowProvider {
windows: Vec<MenuItem<String>>, windows: Vec<MenuItem<Window>>,
} }
impl WindowProvider { impl WindowProvider {
fn new(cfg: &Config) -> Result<Self, String> { fn new(cfg: &Config, cache: &HashMap<String, String>) -> Result<Self, String> {
let clients = hyprland::data::Clients::get().map_err(|e| e.to_string())?; let clients = hyprland::data::Clients::get().map_err(|e| e.to_string())?;
let clients: Vec<_> = clients.iter().cloned().collect(); 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(); let mut sys = System::new_all();
sys.refresh_all(); sys.refresh_all();
let sys = Arc::new(sys); let sys = Arc::new(sys);
let menu_items: Vec<MenuItem<String>> = clients let menu_items: Vec<MenuItem<_>> = clients
.par_iter() .par_iter()
.filter_map(|c| { .filter_map(|c| {
let sys = Arc::clone(&sys); let sys = Arc::clone(&sys);
@ -40,40 +49,51 @@ impl WindowProvider {
.map(|x| x.name().to_string_lossy().into_owned()); .map(|x| x.name().to_string_lossy().into_owned());
process_name.map(|process_name| { process_name.map(|process_name| {
let icon = freedesktop_icons::lookup(&process_name) let icon = cache.get(&process_name).cloned().or_else(|| {
.with_size(cfg.image_size()) freedesktop_icons::lookup(&process_name)
.with_scale(1) .with_size(cfg.image_size())
.find() .with_scale(1)
.map(|icon| icon.to_string_lossy().to_string()) .find()
.or_else(|| { .map(|icon| icon.to_string_lossy().to_string())
desktop_files .or_else(|| {
.iter() desktop_files
.find_map(|d| match &d.entry.entry_type { .iter()
EntryType::Application(app) => { .find_map(|d| match &d.entry.entry_type {
if app.startup_wm_class.as_ref().is_some_and(|wm_class| { EntryType::Application(app) => {
*wm_class.to_lowercase() if app.startup_wm_class.as_ref().is_some_and(
== c.initial_class.to_lowercase() |wm_class| {
}) { *wm_class.to_lowercase()
d.entry.icon.as_ref().map(|icon| icon.content.clone()) == c.initial_class.to_lowercase()
} else { },
None ) {
d.entry
.icon
.as_ref()
.map(|icon| icon.content.clone())
} else {
None
}
} }
} _ => None,
_ => None, })
}) })
}); });
MenuItem::new( MenuItem::new(
format!( format!(
"[{}] \t {} \t {}", "[{}] \t {} \t {}",
c.workspace.name, c.initial_class, c.title c.workspace.name, c.initial_class, c.title
), ),
icon, icon.clone(),
None, None,
vec![].into_iter().collect(), vec![].into_iter().collect(),
None, None,
0.0, 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<String> for WindowProvider { impl ItemProvider<Window> for WindowProvider {
fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<String>>) { fn get_elements(&mut self, _: Option<&str>) -> (bool, Vec<MenuItem<Window>>) {
(false, self.windows.clone()) (false, self.windows.clone())
} }
fn get_sub_elements(&mut self, _: &MenuItem<String>) -> (bool, Option<Vec<MenuItem<String>>>) { fn get_sub_elements(&mut self, _: &MenuItem<Window>) -> (bool, Option<Vec<MenuItem<Window>>>) {
(false, None) (false, None)
} }
} }
fn load_icon_cache(cache_path: &String) -> Result<HashMap<String, String>, Error> {
let toml_content =
fs::read_to_string(cache_path).map_err(|e| Error::UpdateCacheError(format!("{e}")))?;
let cache: HashMap<String, String> = toml::from_str(&toml_content)
.map_err(|_| Error::ParsingError("failed to parse cache".to_owned()))?;
Ok(cache)
}
fn cache_path() -> Result<String, Error> {
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> { fn main() -> Result<(), String> {
env_logger::Builder::new() env_logger::Builder::new()
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned())) .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 args = config::parse_args();
let config = config::load_config(Some(&args)).unwrap_or(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())?; let result = gui::show(config, provider, false, None, None).map_err(|e| e.to_string())?;
if let Some(window_id) = result.menu.data {
Ok( if let Some(window) = result.menu.data {
hyprland::dispatch::Dispatch::call(DispatchType::FocusWindow( hyprland::dispatch::Dispatch::call(DispatchType::FocusWindow(WindowIdentifier::Address(
WindowIdentifier::Address(Address::new(window_id)), window.address,
)) )))
.map_err(|e| e.to_string())?, .map_err(|e| e.to_string())?;
) Ok(update_cache.join().unwrap().map_err(|e| e.to_string())?)
} else { } else {
Err("No window data found".to_owned()) Err("No window data found".to_owned())
} }