use freedesktop_icons::lookup; use std::path::PathBuf; use std::{env, fs}; 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 { 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 { 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 { for dir in data_dirs() { let path = dir .join("applications") .join(format!("{desktop_entry}.desktop")); if let Ok(contents) = fs::read_to_string(&path) && let Some(icon) = parse_icon_key(&contents) { return Some(icon); } } None } fn data_dirs() -> Vec { let mut dirs = vec![]; if let Some(home) = env::var_os("HOME") { dirs.push(PathBuf::from(home).join(".local/share")); } let xdg_data_dirs = 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 { 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 }