use crate::icon::IconFile; use crate::theme::{Icons, Theme, ThemeInfo, 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. /// /// By default, that is `$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. /// /// To add directories to the instance, use [IconSearch::add_directories]. /// /// To construct a new `IconSearch` from a list, use the `From` implementation or construct it by hand. /// /// # Example /// /// ``` /// use icon::IconSearch; /// /// let dirs = IconSearch::default(); /// // TODO /// ``` #[derive(Debug, Clone)] pub struct IconSearch { pub dirs: Vec, } impl IconSearch { pub const fn new_empty() -> Self { Self { dirs: Vec::new() } } pub fn default() -> Self { ::default() } /// Add a list of directories to this `IconSearch` /// /// # Example /// /// ``` /// use icon::IconSearch; /// /// let dirs = IconSearch::default().add_directories(["/home/root/.icons"]); /// ``` pub fn add_directories(mut self, directories: I) -> Self where I: IntoIterator, P: Into, { let mut extra_dirs = directories.into_iter().map(Into::into).collect(); self.dirs.append(&mut extra_dirs); extra_dirs.into() } pub fn find_icon_locations(&self) -> IconLocations { // "Each theme is stored as subdirectories of the base directories" let (files, dirs) = self .dirs .iter() .flat_map(|base_dir| base_dir.read_dir()) // read the entries in each base dir .flatten() // merge all the iterators .flatten() // remove Err entries .filter_map(|entry| Some((entry.file_type().ok()?, entry))) // get file type for each entry and skip if fail .partition::, _>(|(ft, _)| ft.is_file()); // icons at the top-level in a base_dir don't belong to a theme, but must still be able to be found! let files = files .into_iter() .flat_map(|(_, entry)| IconFile::from_path(&entry.path())) .collect::>(); // "In at least one of the theme directories there must be a file called // index.theme that describes the theme. The first index.theme found while // searching the base directories in order is used" // For each theme name, create a list of directories where it may be found: let mut themes_directories: HashMap> = HashMap::new(); for (_, dir) in dirs { let theme_name = dir.file_name(); themes_directories .entry(theme_name) .or_default() .push(dir.path()); } IconLocations { standalone_icons: files, themes_directories, } } } #[derive(Debug)] pub struct IconLocations { pub standalone_icons: Vec, pub themes_directories: HashMap>, } impl IconLocations { pub fn icons(self) -> Icons { let themes = self.resolve(); Icons { standalone_icons: self.standalone_icons, themes, } } pub fn resolve(&self) -> HashMap> { self.resolve_only(self.themes_directories.keys()) } pub fn resolve_only(&self, theme_names: I) -> HashMap> 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, being to pay a (rather small) one-time cost to // make the rest of the API cleaner and smaller. It guarantees 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(), 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, inherits_from: 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. // and to wrap things up, let's zip the themes back up with their names theme_names .into_iter() .zip(full_themes) .collect::>() } pub fn theme_description(&self, internal_name: S) -> std::io::Result where S: AsRef, { let internal_name = internal_name.as_ref(); let theme = self .themes_directories .get(internal_name) .ok_or_else(|| std::io::Error::other(ThemeParseError::NotAnIconTheme))?; ThemeInfo::new_from_folders(internal_name.to_string_lossy().into_owned(), theme.clone()) } pub fn standalone_icon(&self, icon_name: S) -> Option<&IconFile> where S: AsRef, { let name = icon_name.as_ref(); self.standalone_icons .iter() .find(|icon| icon.path.file_stem() == Some(name)) } } /// Anything that turns into an iterator of things that can become paths, can be turned into a `IconSearch`. impl From for IconSearch where I: IntoIterator, P: Into, { fn from(value: I) -> Self { let dirs = value.into_iter().map(Into::into).collect(); IconSearch { dirs } } } impl Default for IconSearch { fn default() -> Self { // "By default, apps should look in $HOME/.icons (for backwards compatibility), // in $XDG_DATA_DIRS/icons // and in /usr/share/pixmaps (in that order)." let xdg = xdg::BaseDirectories::new(); let mut directories = vec![]; if let Some(home) = std::env::home_dir() { directories.push(home.join(".icons")); } xdg.data_dirs .into_iter() .map(|data_dir| data_dir.join("icons")) .for_each(|dir| directories.push(dir)); directories.push("/usr/share/pixmaps".into()); directories.into() } } #[cfg(test)] mod test { use crate::search::IconSearch; // these tests assume certain applications are installed on the system they are ran on. #[test] fn test_find_standard_theme_and_icon() { let dirs = IconSearch::default(); let locations = dirs.find_icon_locations(); 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 = IconSearch::default() .find_icon_locations() .theme_description("breeze") .unwrap(); println!("{:?}", result.index.inherits); } #[test] fn test() { let _dirs = IconSearch::default().find_icon_locations().resolve(); // it didn't panic. } }