From 1ac1003d315d94c882a5b9af27ebabb8015b5342 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Fri, 20 Jun 2025 23:35:04 +0200 Subject: [PATCH] Fix icon finding and add extensive test --- Cargo.lock | 199 +++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/icon.rs | 5 +- src/search.rs | 24 ++---- src/theme.rs | 91 +++++++++++++++++++++-- 5 files changed, 291 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72dcbf6..6fbe50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 2aa4007..d34d7f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ log = { version = "0.4.27", optional = true } [features] "log" = ["dep:log"] + +[dev-dependencies] +freedesktop-desktop-entry = "0.7.13" diff --git a/src/icon.rs b/src/icon.rs index d6ae2ea..862ed25 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -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] } diff --git a/src/search.rs b/src/search.rs index 1868670..5b338cb 100644 --- a/src/search.rs +++ b/src/search.rs @@ -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. - } } diff --git a/src/theme.rs b/src/theme.rs index 171f196..ae01b38 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -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 { - 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 { @@ -60,6 +66,15 @@ impl Theme { } pub fn find_icon(&self, icon_name: &str, size: u32, scale: u32) -> Option { + 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 { 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]