134 lines
4.3 KiB
Rust
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));
|
|
}
|
|
}
|
|
}
|