use std::collections::HashMap; use std::path::PathBuf; use pangocairo::cairo; #[derive(Default)] pub struct IconCache { map: HashMap>, } impl IconCache { /// Get the icon for an app_id, best suited for the given bar height. /// Results (including misses) are cached per app_id. pub fn get(&mut self, app_id: &str, bar_height: u32) -> Option<&cairo::ImageSurface> { self.map .entry(app_id.to_string()) .or_insert_with(|| resolve_icon(app_id, bar_height)) .as_ref() } /// Get a tray icon by name, preferring the given SNI IconThemePath. pub fn get_tray_icon( &mut self, name: &str, theme_path: &str, bar_height: u32, ) -> Option<&cairo::ImageSurface> { if name.is_empty() { return None; } self.map .entry(format!("{theme_path}:{name}")) .or_insert_with(|| resolve_tray_icon(name, theme_path, bar_height)) .as_ref() } } fn resolve_tray_icon(name: &str, theme_path: &str, bar_height: u32) -> Option { if !theme_path.is_empty() { let path = PathBuf::from(theme_path).join(format!("{name}.png")); if let Ok(mut file) = std::fs::File::open(path) && let Ok(surface) = cairo::ImageSurface::create_from_png(&mut file) { return Some(surface); } } resolve_icon(name, bar_height) } fn resolve_icon(app_id: &str, bar_height: u32) -> Option { let mut candidates = vec![app_id.to_string()]; if let Some(desktop_icon) = desktop_file_icon(app_id) && !candidates.contains(&desktop_icon) { candidates.push(desktop_icon); } for name in candidates { if let Some(path) = find_icon_png(&name, bar_height) && let Ok(mut file) = std::fs::File::open(path) && let Ok(surface) = cairo::ImageSurface::create_from_png(&mut file) { return Some(surface); } } None } /// Look up the `Icon=` value of `.desktop` in the applications /// directories of $XDG_DATA_HOME and $XDG_DATA_DIRS. fn desktop_file_icon(app_id: &str) -> Option { let file_name = format!("{app_id}.desktop"); for base in std::iter::once(data_home()).chain(data_dirs()) { let path = base.join("applications").join(&file_name); if let Ok(contents) = std::fs::read_to_string(path) { let mut in_desktop_entry = false; for line in contents.lines() { let line = line.trim(); if line.starts_with('[') { in_desktop_entry = line == "[Desktop Entry]"; } else if in_desktop_entry && let Some(icon) = line.strip_prefix("Icon=") { return Some(icon.trim().to_string()); } } } } None } /// Search for `.png` in the icon themes and pixmaps directories. /// Picks the smallest icon >= bar_height, else the largest available. fn find_icon_png(name: &str, bar_height: u32) -> Option { let file_name = format!("{name}.png"); let mut best: Option<(u32, PathBuf)> = None; let base_dirs = std::iter::once(data_home()) .chain(std::iter::once(home_dir().join(".icons"))) .chain(data_dirs()); for base in base_dirs { let pixmap = base.join("pixmaps").join(&file_name); if pixmap.is_file() { consider(&mut best, 0, pixmap, bar_height); } let Ok(themes) = std::fs::read_dir(base.join("icons")) else { continue; }; for theme in themes.flatten() { let Ok(size_dirs) = std::fs::read_dir(theme.path()) else { continue; }; for size_dir in size_dirs.flatten() { let dir_name = size_dir.file_name(); let Some(dir_name) = dir_name.to_str() else { continue; }; // "48x48" or "48x48@2x"; skip "scalable" (cairo can't load SVG) let Ok(size) = dir_name.split('x').next().unwrap().parse::() else { continue; }; let path = size_dir.path().join("apps").join(&file_name); if path.is_file() { consider(&mut best, size, path, bar_height); } } } } best.map(|(_, path)| path) } /// Keep the candidate that is the smallest >= bar_height, else the largest. fn consider(best: &mut Option<(u32, PathBuf)>, size: u32, path: PathBuf, bar_height: u32) { let better = match best { None => true, Some((best_size, _)) => match (size >= bar_height, *best_size >= bar_height) { (true, true) => size < *best_size, (true, false) => true, (false, true) => false, (false, false) => size > *best_size, }, }; if better { *best = Some((size, path)); } } fn data_home() -> PathBuf { match std::env::var_os("XDG_DATA_HOME") { Some(dir) if !dir.is_empty() => PathBuf::from(dir), _ => home_dir().join(".local/share"), } } fn data_dirs() -> Vec { match std::env::var_os("XDG_DATA_DIRS") { Some(dirs) if !dirs.is_empty() => std::env::split_paths(&dirs).collect(), _ => vec!["/usr/local/share".into(), "/usr/share".into()], } } fn home_dir() -> PathBuf { std::env::var_os("HOME").map_or_else(|| PathBuf::from("/"), PathBuf::from) }