diff options
| author | cashmere <cashmere@autistici.org> | 2026-07-21 14:07:42 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-07-21 14:07:42 +0200 |
| commit | 198f374a18b5c8a0e21f7d41125393aa89523f5c (patch) | |
| tree | ad8d32cc498b1b2715a5e71e5f438bf4bacfca53 /src/icon.rs | |
| parent | 85f0a9e875eee768766d7bdd24272d348f6b1e33 (diff) | |
| download | i3bar-mango-198f374a18b5c8a0e21f7d41125393aa89523f5c.tar.gz | |
mango: show focused window next to tags, fix mmsg lookup, 0.15.x compat
- new focused-window block ({icon} {app_id} - {title}) driven by the
active_client field of mmsg watch all-monitors (mangowm >= 0.14)
- resolve program icons via freedesktop icon themes/.desktop files
(new icon module, cairo png feature, cached per app_id)
- new show_focused_window config option (default true)
- run mmsg from PATH instead of hardcoded /usr/bin/mmsg (broken on NixOS)
- handle null active_client fields (no focused client) without failing
to parse the whole event
- drop stale TODO comment and unused variable in mango provider
- add .justfile run recipe exporting MANGO_INSTANCE_SIGNATURE
Diffstat (limited to 'src/icon.rs')
| -rw-r--r-- | src/icon.rs | 137 |
1 files changed, 137 insertions, 0 deletions
diff --git a/src/icon.rs b/src/icon.rs new file mode 100644 index 0000000..4dae1a9 --- /dev/null +++ b/src/icon.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use pangocairo::cairo; + +#[derive(Default)] +pub struct IconCache { + map: HashMap<String, Option<cairo::ImageSurface>>, +} + +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() + } +} + +fn resolve_icon(app_id: &str, bar_height: u32) -> Option<cairo::ImageSurface> { + 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 `<app_id>.desktop` in the applications +/// directories of $XDG_DATA_HOME and $XDG_DATA_DIRS. +fn desktop_file_icon(app_id: &str) -> Option<String> { + 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 `<name>.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<PathBuf> { + 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::<u32>() 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<PathBuf> { + 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) +} |
