notif-picker/src/icon.rs
Austin Schaefer fe2039c05e Initial commit: mako notification picker for wofi
Reads notification history via makoctl, formats entries as pango markup
with resolved app icons, and pipes them to wofi in dmenu mode for
selection and invocation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 11:50:35 +02:00

83 lines
2.6 KiB
Rust

use freedesktop_icons::lookup;
use std::path::PathBuf;
const ICON_SIZE: u16 = 32;
/// Resolve the best icon path for a notification, preferring the app's own
/// identity (desktop entry, then app name) over `app_icon`, which is often
/// just a hint for the specific notification's content (e.g. blueman sends
/// "battery" for a battery-level alert, not its own icon).
pub fn resolve(
app_icon: Option<&str>,
desktop_entry: Option<&str>,
app_name: Option<&str>,
) -> Option<PathBuf> {
let theme = freedesktop_icons::default_theme_gtk();
let candidates = [
desktop_entry.and_then(desktop_file_icon_name),
app_name.map(str::to_lowercase),
app_icon.map(str::to_owned),
];
candidates
.into_iter()
.flatten()
.find_map(|name| resolve_name(&name, theme.as_deref()))
}
fn resolve_name(name: &str, theme: Option<&str>) -> Option<PathBuf> {
if name.starts_with('/') {
let path = PathBuf::from(name);
return path.is_file().then_some(path);
}
let mut query = lookup(name).with_size(ICON_SIZE);
if let Some(theme) = theme {
query = query.with_theme(theme);
}
query.find()
}
/// Look up the `Icon=` value from a `.desktop` file identified by its
/// desktop-entry id, searching XDG_DATA_DIRS/applications the same way a
/// desktop environment would.
fn desktop_file_icon_name(desktop_entry: &str) -> Option<String> {
for dir in data_dirs() {
let path = dir
.join("applications")
.join(format!("{desktop_entry}.desktop"));
if let Ok(contents) = std::fs::read_to_string(&path)
&& let Some(icon) = parse_icon_key(&contents)
{
return Some(icon);
}
}
None
}
fn data_dirs() -> Vec<PathBuf> {
let mut dirs = vec![];
if let Some(home) = std::env::var_os("HOME") {
dirs.push(PathBuf::from(home).join(".local/share"));
}
let xdg_data_dirs = std::env::var("XDG_DATA_DIRS")
.unwrap_or_else(|_| "/usr/local/share:/usr/share".to_string());
dirs.extend(xdg_data_dirs.split(':').map(PathBuf::from));
dirs
}
fn parse_icon_key(desktop_file_contents: &str) -> Option<String> {
let mut in_desktop_entry_section = false;
for line in desktop_file_contents.lines() {
let line = line.trim();
if line.starts_with('[') {
in_desktop_entry_section = line == "[Desktop Entry]";
continue;
}
if in_desktop_entry_section && let Some(value) = line.strip_prefix("Icon=") {
return Some(value.trim().to_string());
}
}
None
}