Fix icon finding and add extensive test

This commit is contained in:
Ridan Vandenbergh 2025-06-20 23:35:04 +02:00
parent b1c9668dfe
commit 1ac1003d31
No known key found for this signature in database
5 changed files with 291 additions and 31 deletions

199
Cargo.lock generated
View file

@ -2,6 +2,44 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "cc"
version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [
"shlex",
]
[[package]]
name = "freedesktop-desktop-entry"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb78ccb4eb670a9c659f1c61e709d41fd6401cddf562f14cac1a47077918d3"
dependencies = [
"gettext-rs",
"log",
"memchr",
"thiserror 2.0.12",
"unicase",
"xdg 2.5.2",
]
[[package]]
name = "freedesktop_entry_parser"
version = "1.3.0"
@ -12,14 +50,60 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "gettext-rs"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44e92f7dc08430aca7ed55de161253a22276dfd69c5526e5c5e95d1f7cf338a"
dependencies = [
"gettext-sys",
"locale_config",
]
[[package]]
name = "gettext-sys"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb45773f5b8945f12aecd04558f545964f943dacda1b1155b3d738f5469ef661"
dependencies = [
"cc",
"temp-dir",
]
[[package]]
name = "icon"
version = "0.1.0"
dependencies = [
"freedesktop-desktop-entry",
"freedesktop_entry_parser",
"log",
"thiserror 2.0.12",
"xdg",
"xdg 3.0.0",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "locale_config"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
dependencies = [
"lazy_static",
"objc",
"objc-foundation",
"regex",
"winapi",
]
[[package]]
@ -28,6 +112,15 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "memchr"
version = "2.7.4"
@ -50,6 +143,35 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@ -68,6 +190,41 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.101"
@ -79,6 +236,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "temp-dir"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964"
[[package]]
name = "thiserror"
version = "1.0.69"
@ -119,12 +282,46 @@ dependencies = [
"syn",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xdg"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
[[package]]
name = "xdg"
version = "3.0.0"

View file

@ -11,3 +11,6 @@ log = { version = "0.4.27", optional = true }
[features]
"log" = ["dep:log"]
[dev-dependencies]
freedesktop-desktop-entry = "0.7.13"

View file

@ -39,16 +39,15 @@ impl FileType {
None
}
}
pub fn ext(&self) -> &str {
match self {
FileType::Png => "png",
FileType::Xmp => "xmp",
FileType::Svg => "svg",
}
}
pub const fn types() -> [FileType; 3] {
[FileType::Png, FileType::Xmp, FileType::Svg]
}

View file

@ -51,7 +51,7 @@ pub mod states {
/// Icons and icon themes are looked for in a set of directories.
///
/// By default, that is `$HOME/.icons`, `$XDG_DATA_DIRS/icons` and `/usr/share/pixmaps`.
/// By default, that is `$HOME/.icons`, `$XDG_DATA_HOME/icons`, `$XDG_DATA_DIRS/icons` and `/usr/share/pixmaps`.
/// Applications may further add their own icon directories to this list, and users may extend or change the list.
/// The default list may be obtained using the `Default` implementation on `IconSearch` or its `default` method.
///
@ -512,8 +512,9 @@ impl Default for IconSearch {
directories.push(home.join(".icons"));
}
xdg.data_dirs
xdg.data_home
.into_iter()
.chain(xdg.data_dirs.into_iter())
.map(|data_dir| data_dir.join("icons"))
.for_each(|dir| directories.push(dir));
@ -535,6 +536,8 @@ mod test {
.add_directories(["/this/path/probably/doesnt/exist/but/who/cares/"])
.search()
.icons();
// no panic
}
#[test]
@ -549,21 +552,4 @@ mod test {
let icon = locations.standalone_icon("htop").unwrap();
assert_eq!(icon.path.file_name(), Some("htop.png".as_ref()))
}
#[test]
fn test_2() {
let result = IconSearch::default()
.find_icon_locations()
.load_single_theme("breeze")
.unwrap();
println!("{:?}", result.index.inherits);
}
#[test]
fn test() {
let _dirs = IconSearch::default().find_icon_locations().resolve();
// it didn't panic.
}
}

View file

@ -1,6 +1,6 @@
use crate::IconSearch;
use crate::icon::IconFile;
use crate::theme::ThemeParseError::MissingRequiredAttribute;
use crate::IconSearch;
use freedesktop_entry_parser::low_level::{EntryIter, SectionBytes};
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
@ -30,6 +30,10 @@ impl Icons {
self.find_icon(icon_name, size, scale, "hicolor")
}
/// Look up an icon by name, size, scale and theme.
///
/// If the icon is not found in the theme, its parents are checked.
/// If no theme by the given name exists, the `"hicolor"` theme (default theme) is checked.
pub fn find_icon(
&self,
icon_name: &str,
@ -37,8 +41,10 @@ impl Icons {
scale: u32,
theme: &str,
) -> Option<IconFile> {
let theme = self.theme(theme)?;
theme.find_icon(icon_name, size, scale)
let theme = self.theme(theme).or_else(|| self.theme("hicolor"))?;
theme
.find_icon(icon_name, size, scale)
.or_else(|| self.find_standalone_icon(icon_name))
}
pub fn find_standalone_icon(&self, icon_name: &str) -> Option<IconFile> {
@ -60,6 +66,15 @@ impl Theme {
}
pub fn find_icon(&self, icon_name: &str, size: u32, scale: u32) -> Option<IconFile> {
self.find_icon_here(icon_name, size, scale).or_else(|| {
// or find it in one of our parents
self.inherits_from
.iter()
.find_map(|theme| theme.find_icon_here(icon_name, size, scale))
})
}
fn find_icon_here(&self, icon_name: &str, size: u32, scale: u32) -> Option<IconFile> {
const EXTENSIONS: [&'static str; 3] = ["png", "xmp", "svg"];
let file_names = EXTENSIONS.map(|ext| format!("{icon_name}.{ext}"));
@ -394,24 +409,84 @@ fn find_attr_req<'a>(
#[cfg(test)]
mod test {
use crate::Icons;
use crate::icon::{FileType, IconFile};
use crate::theme::{DirectoryType, ThemeIndex};
use crate::Icons;
use std::error::Error;
use std::path::Path;
#[test]
fn test_find_icon() {
fn test_find_firefox() {
let icons = Icons::new();
let option = icons.find_default_icon("firefox", 128, 1);
let ico = icons.find_default_icon("firefox", 128, 1);
assert_eq!(
option,
ico,
Some(IconFile {
path: "/usr/share/icons/hicolor/128x128/apps/firefox.png".into(),
file_type: FileType::Png
})
)
);
// we should be able to find an icon for a bunch of different sizes
for size in (16u32..=64).step_by(8) {
assert!(icons.find_default_icon("firefox", size, 1).is_some());
}
assert!(icons.find_default_icon("firefox", 64, 2).is_some());
}
#[test]
fn find_all_desktop_entry_icons() {
let icons = Icons::new();
// some desktop files are just packaged poorly.
// if a test fails here, and you are certain that the icon just straight up doesn't exist,
// or is in an unfindable place by normal means,
// disallow it in this list.
static DISALLOW_LIST: &[&str] = &[
"imv-dir",
"imv",
"io.elementary.granite.demo",
"java-java-openjdk",
"jconsole-java-openjdk",
"jshell-java-openjdk",
"lstopo",
"signon-ui",
];
for entry in
freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
.entries(None::<&[&str]>)
{
let Some(icon_name) = entry.icon() else {
continue;
};
if Path::new(icon_name).exists() {
continue; // absolute URLs to icons are OK
}
if DISALLOW_LIST
.iter()
.any(|x| Some(x.as_ref()) == entry.path.file_stem())
{
continue;
}
// TODO: perhaps our system should expose a way to construct a "composed theme" filter,
// for cases where you want to search a multitude (or all) themes
let icon = icons
.find_icon(icon_name, 32, 1, "gnome")
.or_else(|| icons.find_icon(icon_name, 32, 1, "breeze"));
assert!(
icon.is_some(),
"Icon {icon_name} from desktop entry {:?} missing!!",
entry.path
)
}
}
#[test]