Files
feresaria/shell/ferese-shell/src/application_icons.rs
T

134 lines
4.3 KiB
Rust

//! Application artwork is resolved off the UI thread, then cached by app ID.
use std::path::{Path, PathBuf};
use cosmic::widget::icon;
use freedesktop_desktop_entry as fde;
pub(super) fn fallback() -> icon::Handle {
icon::from_svg_bytes(ferese_theme::icons::WINDOW).symbolic(true)
}
pub(super) fn resolve(entries: &[fde::DesktopEntry], app_id: &str) -> icon::Handle {
path(entries, app_id, |name| {
icon::from_name(name.to_owned())
.prefer_svg(true)
.size(32)
// A shortened name could identify a different application.
.fallback(None)
.path()
})
.map(icon::from_path)
.unwrap_or_else(fallback)
}
fn path(
entries: &[fde::DesktopEntry],
app_id: &str,
mut lookup: impl FnMut(&str) -> Option<PathBuf>,
) -> Option<PathBuf> {
let entry = fde::find_app_by_id(entries, fde::unicase::Ascii::new(app_id));
let source = entry
.and_then(fde::DesktopEntry::icon)
.filter(|s| !s.is_empty());
let declared = source.and_then(|source| {
if Path::new(source).is_absolute() {
Some(PathBuf::from(source))
} else {
lookup(source)
}
});
declared.filter(|path| path.is_file()).or_else(|| {
// Some applications install an app-ID icon without an Icon= entry.
// Also try the canonical desktop ID for StartupWMClass aliases.
[Some(app_id), entry.map(|entry| entry.appid.as_str())]
.into_iter()
.flatten()
.filter(|id| !id.is_empty() && !id.contains('/'))
.find_map(|id| lookup(id).filter(|path| path.is_file()))
})
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(directory: &Path, id: &str, fields: &str) -> fde::DesktopEntry {
let path = directory.join(format!("{id}.desktop"));
std::fs::write(
&path,
format!("[Desktop Entry]\nType=Application\nName=Test\n{fields}\n"),
)
.unwrap();
fde::DesktopEntry::from_path(path, Some(&[] as &[&str])).unwrap()
}
#[test]
fn declared_artwork_wins_and_absolute_raster_icons_work() {
let dir = tempfile::tempdir().unwrap();
let png = dir.path().join("own.png");
std::fs::write(&png, b"fixture").unwrap();
let entries = [entry(
dir.path(),
"test.app",
&format!("Icon={}", png.display()),
)];
assert_eq!(
path(&entries, "test.app", |_| panic!(
"absolute icon needs no lookup"
)),
Some(png)
);
}
#[test]
fn missing_or_broken_declarations_use_the_app_id_and_wm_class_alias() {
let dir = tempfile::tempdir().unwrap();
let svg = dir.path().join("own.svg");
std::fs::write(&svg, b"fixture").unwrap();
for fields in ["", "Icon=missing-icon", "Icon=/missing/icon.png"] {
let entries = [entry(
dir.path(),
"test.app",
&format!("StartupWMClass=Alias\n{fields}"),
)];
let mut names = Vec::new();
assert_eq!(
path(&entries, "Alias", |name| {
names.push(name.to_owned());
(name == "test.app").then(|| svg.clone())
}),
Some(svg.clone())
);
assert!(names.iter().any(|name| name == "Alias"));
}
}
#[test]
fn unknown_applications_never_use_the_ferese_brand() {
assert!(path(&[], "unknown", |_| None).is_none());
let icon = fallback();
assert!(icon.symbolic);
match icon.data {
icon::Data::Svg(handle) => {
assert_ne!(
handle,
cosmic::widget::svg::Handle::from_memory(ferese_theme::icons::APPLICATION)
);
}
_ => panic!("fallback must be the bundled window glyph"),
}
}
#[test]
#[ignore = "inspect desktop/icon lookup using the caller's XDG environment"]
fn inspect_installed_application_icons() {
let entries: Vec<_> = fde::Iter::new(fde::default_paths())
.entries::<&str>(Some(&[]))
.collect();
let ids = std::env::var("FERESE_TEST_APP_IDS").unwrap();
for id in ids.split(',') {
eprintln!("APP_ICON {id}: {:?}", resolve(&entries, id));
}
}
}