summaryrefslogtreecommitdiff
path: root/src/icon.rs
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-21 14:07:42 +0200
committercashmere <cashmere@autistici.org>2026-07-21 14:07:42 +0200
commit198f374a18b5c8a0e21f7d41125393aa89523f5c (patch)
treead8d32cc498b1b2715a5e71e5f438bf4bacfca53 /src/icon.rs
parent85f0a9e875eee768766d7bdd24272d348f6b1e33 (diff)
downloadi3bar-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.rs137
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)
+}