diff --git a/Cargo.lock b/Cargo.lock index 34524b2..72dcbf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,77 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "cc" -version = "1.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "file-lock" -version = "2.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "040b48f80a749da50292d0f47a1e2d5bf1d772f52836c07f64bfccc62ba6e664" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "freedesktop_entry_parser" version = "1.3.0" @@ -83,62 +12,21 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi", -] - -[[package]] -name = "hashbrown" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" - [[package]] name = "icon" version = "0.1.0" dependencies = [ "freedesktop_entry_parser", - "icon-cache", - "once_map", + "log", "thiserror 2.0.12", "xdg", ] [[package]] -name = "icon-cache" -version = "0.1.1" +name = "log" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b7947ecb08aeac6a28bf829b1324d24e3f340d124531e2a90f7a5862a7c367" -dependencies = [ - "derive_more", - "file-lock", - "memmap2", - "zerocopy", -] - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" @@ -146,15 +34,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -171,47 +50,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_map" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd2cae3bec3936bbed1ccc5a3343b3738858182419f9c0522c7260c80c430b0" -dependencies = [ - "ahash", - "hashbrown", - "parking_lot", - "stable_deref_trait", -] - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "proc-macro2" version = "1.0.95" @@ -230,45 +68,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" - -[[package]] -name = "redox_syscall" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" -dependencies = [ - "bitflags", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "syn" version = "2.0.101" @@ -326,122 +125,8 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - [[package]] name = "xdg" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" - -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 975a738..2aa4007 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,9 @@ edition = "2024" [dependencies] freedesktop_entry_parser = "1.3.0" -icon-cache = "0.1.1" -once_map = "0.4.21" thiserror = "2.0.12" xdg = "3.0.0" +log = { version = "0.4.27", optional = true } + +[features] +"log" = ["dep:log"] diff --git a/src/search_dir.rs b/src/search_dir.rs index e03b4ba..7d3ec01 100644 --- a/src/search_dir.rs +++ b/src/search_dir.rs @@ -1,8 +1,9 @@ use crate::icon::IconFile; -use crate::theme::{ThemeDescriptor, ThemeParseError}; +use crate::theme::{OwnedThemeDescriptor, Theme, ThemeDescriptor, ThemeParseError}; use std::collections::HashMap; use std::ffi::{OsStr, OsString}; use std::path::PathBuf; +use std::sync::Arc; /// Icons and icon themes are looked for in a set of directories. /// @@ -99,7 +100,191 @@ pub struct IconLocations { } impl IconLocations { - pub fn theme(&self, internal_name: S) -> std::io::Result> + pub fn resolve(self) -> Vec>> { + let names = self.themes_directories.keys().cloned().collect::>(); + + self.resolve_only(names) + } + + pub fn resolve_only(self, theme_names: I) -> Vec>> + where + I: IntoIterator, + S: AsRef, + { + // Icon themes may transitively depend on the same icon theme many times. + // This is a bit of an issue, as when an exhaustive icon lookup would be implemented naively, + // users may end up searching the same icon theme multiple times. + // To accommodate this, either one has to keep a list of visited icon themes every time they + // perform a lookup, or avoid the issue altogether by removing redundant parents up-front. + + // That second option is what this function does, paying a (rather small) one-time cost to + // make the rest of the API cleaner and smaller by guaranteeing that the returned icon themes + // have dependencies that form a direct acyclic graph without redundant paths. + + fn collect_themes( + name: &OsStr, + locations: &IconLocations, + themes: &mut HashMap>, + ) { + // Skip if we already have this theme. + if themes.contains_key(name) { + return; + } + + let descriptor = match locations.theme_description(name) { + Ok(d) => Some(d), + Err(_e) => { + #[cfg(feature = "log")] + log::debug!("skipping theme candidate {name:?} because {_e}"); + + None + } + }; + let descriptor = themes.entry(name.to_os_string()).insert_entry(descriptor); + + let Some(descriptor) = descriptor.get() else { + return; + }; + + let parents = descriptor.index.inherits.clone(); + + // Collect all parents of this theme: + for parent in parents { + collect_themes(parent.as_ref().as_ref(), locations, themes); + } + } + + // Map from theme names to their descriptor: + let mut themes = HashMap::new(); + + // collect all required themes: + for theme_name in theme_names { + let theme_name = theme_name.as_ref(); + collect_themes(theme_name.as_ref(), &self, &mut themes); + } + + // make 100% sure we have `hicolor`, for the half-impossible edge-case of only collecting + // themes that does not have hicolor in their inheritance tree + collect_themes("hicolor".as_ref(), &self, &mut themes); + // of course, the user might be cursed and not have `hicolor` installed at all! + // that is troubling, but we'll see that it is handled correctly below. + + // let's prune theme candidates that have no description (meaning they weren't themes, or + // were invalid) + // we'll also split them up, as `theme_chains` borrows names from `theme_names`, + // but we need to mutate theme_descriptions later (during the borrow) to avoid + // cloning the descriptions + let (theme_names, mut theme_descriptions): (Vec<_>, Vec<_>) = themes + .into_iter() + .flat_map(|(key, value)| value.map(|v| (key, Some(v)))) + .unzip(); + + // the Options are there just so we can take descriptions out of the vec without messing up the order. + debug_assert!(theme_descriptions.iter().all(Option::is_some)); + + // do we even have hicolor? + // if not, there's no use in inserting hicolor into the inheritance tree later + let hicolor_idx = theme_names.iter().position(|name| name == "hicolor"); + + // Time to find the optimal ancestry for each theme. + // as hicolor _should_ have all icons by default, and all themes depend on hicolor at some depth, + // DFS would de facto end up in hicolor before ever trying the second theme in an Inherits set. + // therefore BFS is the only sensible option, but the spec doesn't define this. + + // indexed by the position in our theme_names/theme_descriptions vecs + let number_of_themes = theme_names.len(); + let mut theme_chains = Vec::>::with_capacity(number_of_themes); + + for theme_idx in 0..number_of_themes { + let mut chain = Vec::from([theme_idx]); + + let mut cursor = 0; + while let Some(node_idx) = chain.get(cursor).copied() { + cursor += 1; + + let Some(Some(description)) = theme_descriptions.get(node_idx) else { + continue; + }; + + for parent in &description.index.inherits { + let Some(parent_idx) = theme_names + .iter() + .position(|name| *name.as_os_str() == **parent) + else { + // this parent was invalid + continue; + }; + + if !chain.contains(&parent_idx) { + chain.push(parent_idx); + } + } + } + + // From the spec: "If no theme is specified, implementations are required to add the + // "hicolor" theme to the inheritance tree." + if let Some(hicolor_idx) = hicolor_idx { + if !chain.contains(&hicolor_idx) { + chain.push(hicolor_idx); + } + } + + theme_chains.push(chain); + } + + // at this point `theme_chains` contains a _topological order_ for each theme's parents, + // meaning we can easily iterate over it, constructing `Theme`s, assuming at every point + // that each parent already has a `Theme` created for it :) + + // again indexed by theme indices, None values mean the theme hasn't been processed yet. + // the goal is that, by the end of the for loop, that this only contains `Some`s. + // we rely on the topological order of chains to always have all the prerequisite themes + // present already in this map! + let mut full_themes = vec![None::>; number_of_themes]; + + for chain in &theme_chains { + // go from last theme to first, as all dependencies are "forward" in the chain: + for theme_idx in chain.iter().copied().rev() { + let theme_desc = theme_descriptions[theme_idx].take(); + + let Some(theme_desc) = theme_desc else { + // the option was None, meaning this theme was processed already :-) + continue; + }; + + let parents = &theme_chains[theme_idx]; + let parents = parents + .into_iter() + .skip(1) // the first in the chain is the theme itself, which we'll ignore—it's not a parent. + .copied() + // unwrap OK because, by the topological order, all of these parents + // should already be present in the array: + .map(|parent_idx| Arc::clone(full_themes[parent_idx].as_ref().unwrap())) + .collect(); + + let theme = Theme { + description: theme_desc, + parents, + }; + + full_themes[theme_idx] = Some(Arc::new(theme)); + } + } + + debug_assert!(full_themes.iter().all(Option::is_some)); + + let full_themes: Vec<_> = full_themes.into_iter().map(Option::unwrap).collect(); + + // and so, we have reached the end of the Big Beautiful Function. + // `full_themes` is a list of + // - All themes requested, + // - all themes required by the inheritance tree of those themes, without duplicates, + // - and an optimal chain (inheritance tree search order) for each theme. + + full_themes + } + + pub fn theme_description(&self, internal_name: S) -> std::io::Result where S: AsRef, { @@ -121,8 +306,9 @@ impl IconLocations { S: AsRef, { let name = icon_name.as_ref(); - - self.standalone_icons.iter() + + self.standalone_icons + .iter() .find(|icon| icon.path.file_stem() == Some(name)) } } @@ -176,11 +362,28 @@ mod test { let dirs = SearchDirectories::default(); let locations = dirs.find_icon_locations(); - - let descriptor = locations.theme("Adwaita").unwrap(); + + let descriptor = locations.theme_description("Adwaita").unwrap(); assert_eq!(descriptor.index.name, "Adwaita"); let icon = locations.standalone_icon("htop").unwrap(); assert_eq!(icon.path.file_name(), Some("htop.png".as_ref())) } + + #[test] + fn test_2() { + let result = SearchDirectories::default() + .find_icon_locations() + .theme_description("breeze") + .unwrap(); + + println!("{:?}", result.index.inherits); + } + + #[test] + fn test() { + let _dirs = SearchDirectories::default().find_icon_locations().resolve(); + + // it didn't panic. + } } diff --git a/src/theme.rs b/src/theme.rs index 68e659d..fc4241b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,12 +1,27 @@ +use crate::icon::IconFile; use crate::theme::ThemeParseError::MissingRequiredAttribute; use freedesktop_entry_parser::low_level::{EntryIter, SectionBytes}; use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::OsString; use std::path::{Path, PathBuf}; +use std::sync::Arc; +pub type OwnedIcons = Icons<'static>; pub type OwnedThemeDescriptor = ThemeDescriptor<'static>; pub type OwnedThemeIndex = ThemeIndex<'static>; pub type OwnedDirectoryIndex = DirectoryIndex<'static>; +pub struct Icons<'a> { + pub standalone_icons: Vec, + pub themes: HashMap>>, +} + +pub struct Theme<'a> { + pub description: ThemeDescriptor<'a>, + pub parents: Vec>>, +} + pub struct ThemeDescriptor<'a> { pub internal_name: String, pub base_dirs: Vec, @@ -50,7 +65,7 @@ impl ThemeDescriptor<'_> { index, }) } - + pub fn into_owned(self) -> OwnedThemeDescriptor { theme_into_owned(self) } @@ -90,11 +105,16 @@ impl<'a> ThemeIndex<'a> { let icon_theme_section: SectionBytes<'a> = entry.next().ok_or(ThemeParseError::NotAnIconTheme)??; let name: &'a str = find_attr_req(&icon_theme_section, "Name")?; - let comment = find_attr_req(&icon_theme_section, "Comment")?; + + // SPEC: `Comment` is required, but most icon theme developers can't actually be arsed to + // include it! To make `icon` practical, we choose a default of an empty string instead. + // let comment = find_attr_req(&icon_theme_section, "Comment")?; + let comment = find_attr(&icon_theme_section, "Comment")? + .unwrap_or(""); // If no theme is specified, implementations are required to add the "hicolor" theme to the inheritance tree. let inherits = find_attr(&icon_theme_section, "Inherits")? - .unwrap_or("hicolor") - .split(',') // `inherits` is a comma-separated string list + .iter() + .flat_map(|s| s.split(',')) // `inherits` is a comma-separated string list .map(Into::into) .collect::>(); let directories = find_attr_req(&icon_theme_section, "Directories")?