Implement resolve and resolve_only to collect theme candidates into theme graphs

This commit is contained in:
Ridan Vandenbergh 2025-06-20 18:12:05 +02:00
parent ee9af9c9aa
commit 20b552cebd
No known key found for this signature in database
4 changed files with 241 additions and 331 deletions

323
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"]

View file

@ -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<S>(&self, internal_name: S) -> std::io::Result<ThemeDescriptor<'_>>
pub fn resolve(self) -> Vec<Arc<Theme<'static>>> {
let names = self.themes_directories.keys().cloned().collect::<Vec<_>>();
self.resolve_only(names)
}
pub fn resolve_only<I, S>(self, theme_names: I) -> Vec<Arc<Theme<'static>>>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
// 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<OsString, Option<ThemeDescriptor>>,
) {
// 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::<Vec<usize>>::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::<Arc<Theme>>; 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<S>(&self, internal_name: S) -> std::io::Result<OwnedThemeDescriptor>
where
S: AsRef<OsStr>,
{
@ -121,8 +306,9 @@ impl IconLocations {
S: AsRef<OsStr>,
{
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.
}
}

View file

@ -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<IconFile>,
pub themes: HashMap<OsString, Arc<Theme<'a>>>,
}
pub struct Theme<'a> {
pub description: ThemeDescriptor<'a>,
pub parents: Vec<Arc<Theme<'a>>>,
}
pub struct ThemeDescriptor<'a> {
pub internal_name: String,
pub base_dirs: Vec<PathBuf>,
@ -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::<Vec<_>>();
let directories = find_attr_req(&icon_theme_section, "Directories")?