Reorders imports and reflows a long binding so the new CI's format-check job passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
2.6 KiB
Rust
84 lines
2.6 KiB
Rust
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
|
|
}
|