notif-picker/src/icon.rs

85 lines
2.6 KiB
Rust
Raw Normal View History

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<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) = 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) = 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<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
}