Docs, style, lints.

This commit is contained in:
Ridan Vandenbergh 2025-06-21 13:16:38 +02:00
parent 06c0eec062
commit 0b3e9a9987
No known key found for this signature in database
3 changed files with 65 additions and 32 deletions

View file

@ -23,29 +23,25 @@
//! //!
//! 1. *Finding standalone icons and themes*: //! 1. *Finding standalone icons and themes*:
//! //!
//! Icons are found either in icon themes, or 'standalone' (outside a theme) in XDG base directories. //! Icons are found either in icon themes or 'standalone' (outside a theme) in XDG base directories.
//! While a number of directories should always be scanned for icons, the user or application is //! While a number of directories should always be scanned for icons, the user or application is
//! allowed to search additional directories as it sees fit. //! allowed to search additional directories as it sees fit.
//! //!
//! [IconSearch] handles this part, and is also the main entrypoint for `icon`. //! [IconSearch] handles this part, and is also the main entrypoint for `icon`.
//! //!
//! 2. *Parsing icon themes*: //! 2. *Parsing and resolving icon themes*:
//! //!
//! Each icon theme lives in a directory in the root of one or more of the "search directories". //! Each icon theme lives in a directory in the root of one or more of the "search directories".
//! The name of its directory is called the theme's _internal name_, and in it lies the theme's //! The name of its directory is called the theme's _internal name_, and in it lies the theme's
//! definition, `index.theme`. //! definition, `index.theme`.
//! //!
//! To find icons in a theme, its `index.theme` file must be parsed to understand the directory //! Icon themes also can declare other icon themes from which they inherit. For that reason,
//! structure within the theme itself. //! we also need to do the additional work of figuring out the inheritance tree of each theme,
//! This is handled by [theme::ThemeInfo]. //! removing (transitive) duplicates from the inheritance tree, etc.
//! //!
//! 3a. *Find just one icon* (oneshot): //! 3. *Finding icons*:
//! //!
//! // TODO //! Once a full picture of icons and theme 'graphs' is obtained, we can start looking up icons.
//!
//! 3b. *Find many icons*:
//!
//! // TODO
//! //!
//! # Alternative crates //! # Alternative crates
//! //!

View file

@ -64,7 +64,7 @@ pub mod states {
/// ``` /// ```
/// use icon::IconSearch; /// use icon::IconSearch;
/// ///
/// let icons = IconSearch::default() /// let icons = IconSearch::new()
/// // (optional) add directories to search /// // (optional) add directories to search
/// .add_directories(["/some/additional/directory/"]) /// .add_directories(["/some/additional/directory/"])
/// // find icons and folders /// // find icons and folders
@ -92,7 +92,7 @@ impl IconSearch<Initial> {
/// - `/usr/share/pixmaps` /// - `/usr/share/pixmaps`
/// ///
/// If you wish to add directories to those, use this function and then [`add_directories`](Self::add_directories). /// If you wish to add directories to those, use this function and then [`add_directories`](Self::add_directories).
pub fn default() -> Self { pub fn new() -> Self {
<Self as Default>::default() <Self as Default>::default()
} }
@ -118,7 +118,7 @@ impl IconSearch<Initial> {
/// ``` /// ```
/// use icon::IconSearch; /// use icon::IconSearch;
/// ///
/// let dirs = IconSearch::default() /// let dirs = IconSearch::new()
/// .add_directories(["/home/root/.icons"]) /// .add_directories(["/home/root/.icons"])
/// .search() /// .search()
/// .icons(); /// .icons();
@ -202,8 +202,7 @@ impl IconSearch<LocationsFound> {
/// ///
/// Contained search directories are lost. /// Contained search directories are lost.
pub fn into_icon_locations(self) -> IconLocations { pub fn into_icon_locations(self) -> IconLocations {
let icons = self.icon_locations.expect("guaranteed by type-state"); self.icon_locations.expect("guaranteed by type-state")
icons
} }
// -- STAGE 3: We have icon theme candidates, so it's time to resolve them. // -- STAGE 3: We have icon theme candidates, so it's time to resolve them.
@ -249,7 +248,7 @@ impl IconLocations {
/// Prefer following the normal flow instead: /// Prefer following the normal flow instead:
/// ```rust /// ```rust
/// use icon::IconSearch; /// use icon::IconSearch;
/// let search = IconSearch::default() /// let search = IconSearch::new()
/// .search(); /// .search();
/// ///
/// let locations = search.into_icon_locations(); /// let locations = search.into_icon_locations();
@ -296,6 +295,7 @@ impl IconLocations {
return; return;
} }
#[allow(clippy::manual_ok_err)] // clippy doesn't see the #[cfg]
let info = match locations.load_single_theme(name) { let info = match locations.load_single_theme(name) {
Ok(d) => Some(d), Ok(d) => Some(d),
Err(_e) => { Err(_e) => {
@ -325,12 +325,12 @@ impl IconLocations {
// collect all required themes: // collect all required themes:
for theme_name in theme_names { for theme_name in theme_names {
let theme_name = theme_name.as_ref(); let theme_name = theme_name.as_ref();
collect_themes(theme_name.as_ref(), &self, &mut themes); collect_themes(theme_name, self, &mut themes);
} }
// make 100% sure we have `hicolor`, for the half-impossible edge-case of only collecting // 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 // themes that does not have hicolor in their inheritance tree
collect_themes("hicolor".as_ref(), &self, &mut themes); collect_themes("hicolor".as_ref(), self, &mut themes);
// of course, the user might be cursed and not have `hicolor` installed at all! // 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. // that is troubling, but we'll see that it is handled correctly below.
@ -419,7 +419,7 @@ impl IconLocations {
let parents = &theme_chains[theme_idx]; let parents = &theme_chains[theme_idx];
let parents = parents let parents = parents
.into_iter() .iter()
.skip(1) // the first in the chain is the theme itself, which we'll ignore—it's not a parent. .skip(1) // the first in the chain is the theme itself, which we'll ignore—it's not a parent.
.copied() .copied()
// unwrap OK because, by the topological order, all of these parents // unwrap OK because, by the topological order, all of these parents
@ -514,7 +514,7 @@ impl Default for IconSearch {
xdg.data_home xdg.data_home
.into_iter() .into_iter()
.chain(xdg.data_dirs.into_iter()) .chain(xdg.data_dirs)
.map(|data_dir| data_dir.join("icons")) .map(|data_dir| data_dir.join("icons"))
.for_each(|dir| directories.push(dir)); .for_each(|dir| directories.push(dir));
@ -532,7 +532,7 @@ mod test {
#[test] #[test]
fn test_standard_usage() { fn test_standard_usage() {
let _icons = IconSearch::default() let _icons = IconSearch::new()
.add_directories(["/this/path/probably/doesnt/exist/but/who/cares/"]) .add_directories(["/this/path/probably/doesnt/exist/but/who/cares/"])
.search() .search()
.icons(); .icons();
@ -542,7 +542,7 @@ mod test {
#[test] #[test]
fn test_find_standard_theme_and_icon() { fn test_find_standard_theme_and_icon() {
let dirs = IconSearch::default(); let dirs = IconSearch::new();
let locations = dirs.find_icon_locations(); let locations = dirs.find_icon_locations();

View file

@ -7,6 +7,18 @@ use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
/// Main struct to locate icon files.
///
/// Create this using [`Icons::new`] for the standard configuration, or use [`IconSearch`] if you
/// wish to tune where icons can be found.
///
/// # Example
///
/// ```rust
/// use icon::Icons;
///
/// Icons::new().find_icon("firefox", 32, 1, "hicolor");
/// ```
pub struct Icons { pub struct Icons {
pub standalone_icons: Vec<IconFile>, pub standalone_icons: Vec<IconFile>,
pub themes: HashMap<OsString, Arc<Theme>>, pub themes: HashMap<OsString, Arc<Theme>>,
@ -18,22 +30,34 @@ impl Icons {
/// This function collects all standalone icons and icon themes on the system. /// This function collects all standalone icons and icon themes on the system.
/// To configure what directories are searched, use [`IconSearch`] instead. /// To configure what directories are searched, use [`IconSearch`] instead.
pub fn new() -> Self { pub fn new() -> Self {
IconSearch::default().search().icons() IconSearch::new().search().icons()
} }
/// Access a known icon theme by name
pub fn theme(&self, theme_name: &str) -> Option<Arc<Theme>> { pub fn theme(&self, theme_name: &str) -> Option<Arc<Theme>> {
let theme_name: &OsStr = theme_name.as_ref(); let theme_name: &OsStr = theme_name.as_ref();
self.themes.get(theme_name).cloned() self.themes.get(theme_name).cloned()
} }
/// Like [`find_icon`], with `theme` being `"hicolor"`, which is the default icon theme.
pub fn find_default_icon(&self, icon_name: &str, size: u32, scale: u32) -> Option<IconFile> { pub fn find_default_icon(&self, icon_name: &str, size: u32, scale: u32) -> Option<IconFile> {
self.find_icon(icon_name, size, scale, "hicolor") self.find_icon(icon_name, size, scale, "hicolor")
} }
/// Look up an icon by name, size, scale and theme. /// 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 used instead.
/// If no theme by the given name exists, the `"hicolor"` theme (default theme) is checked. /// - If the icon is not found in the provided theme, its parents are checked.
/// - If the icon is not found in any of the themes, the standalone icon list is checked.
///
/// # Icon matching
///
/// This function will return an icon matching the specified size and scale exactly if it exists.
/// Otherwise, an icon with the smallest "distance" (in icon size) is returned.
///
/// This will only return `None` if no icon by the specified name exists in the specified theme
/// and its parents, and no standalone icon by the same name exists either.
///
pub fn find_icon( pub fn find_icon(
&self, &self,
icon_name: &str, icon_name: &str,
@ -47,6 +71,12 @@ impl Icons {
.or_else(|| self.find_standalone_icon(icon_name)) .or_else(|| self.find_standalone_icon(icon_name))
} }
/// Look up a standalone icon by name.
///
/// "Standalone" icons are icons that live outside icon themes, residing at the root in the
/// search directories instead.
///
/// These icons do not have any size or scalability information attached to them.
pub fn find_standalone_icon(&self, icon_name: &str) -> Option<IconFile> { pub fn find_standalone_icon(&self, icon_name: &str) -> Option<IconFile> {
self.standalone_icons self.standalone_icons
.iter() .iter()
@ -55,6 +85,12 @@ impl Icons {
} }
} }
impl Default for Icons {
fn default() -> Self {
Self::new()
}
}
pub struct Theme { pub struct Theme {
pub info: ThemeInfo, pub info: ThemeInfo,
pub inherits_from: Vec<Arc<Theme>>, pub inherits_from: Vec<Arc<Theme>>,
@ -74,8 +110,9 @@ impl Theme {
}) })
} }
// find an icon in this theme only, not checking parents.
fn find_icon_here(&self, icon_name: &str, size: u32, scale: u32) -> Option<IconFile> { fn find_icon_here(&self, icon_name: &str, size: u32, scale: u32) -> Option<IconFile> {
const EXTENSIONS: [&'static str; 3] = ["png", "xmp", "svg"]; const EXTENSIONS: [&str; 3] = ["png", "xmp", "svg"];
let file_names = EXTENSIONS.map(|ext| format!("{icon_name}.{ext}")); let file_names = EXTENSIONS.map(|ext| format!("{icon_name}.{ext}"));
let base_dirs = &self.info.base_dirs; let base_dirs = &self.info.base_dirs;
@ -83,7 +120,7 @@ impl Theme {
let sub_dirs = &self.info.index.directories; let sub_dirs = &self.info.index.directories;
// first, try to find an exact icon size match: // first, try to find an exact icon size match:
let exact_sub_dirs = sub_dirs let exact_sub_dirs = sub_dirs
.into_iter() .iter()
.filter(|sub_dir| sub_dir.matches_size(size, scale)); .filter(|sub_dir| sub_dir.matches_size(size, scale));
for base_dir in base_dirs { for base_dir in base_dirs {