diff --git a/src/icon.rs b/src/icon.rs index 0f9b551..d6ae2ea 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct IconFile { pub path: PathBuf, pub file_type: FileType, @@ -17,7 +17,7 @@ impl IconFile { } } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum FileType { Png, Xmp, @@ -39,4 +39,17 @@ 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/theme.rs b/src/theme.rs index 2cebc5f..171f196 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,9 +1,9 @@ -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::OsString; +use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -20,6 +20,33 @@ impl Icons { pub fn new() -> Self { IconSearch::default().search().icons() } + + pub fn theme(&self, theme_name: &str) -> Option> { + let theme_name: &OsStr = theme_name.as_ref(); + self.themes.get(theme_name).cloned() + } + + pub fn find_default_icon(&self, icon_name: &str, size: u32, scale: u32) -> Option { + self.find_icon(icon_name, size, scale, "hicolor") + } + + pub fn find_icon( + &self, + icon_name: &str, + size: u32, + scale: u32, + theme: &str, + ) -> Option { + let theme = self.theme(theme)?; + theme.find_icon(icon_name, size, scale) + } + + pub fn find_standalone_icon(&self, icon_name: &str) -> Option { + self.standalone_icons + .iter() + .find(|ico| ico.path.file_stem() == Some(icon_name.as_ref())) + .cloned() + } } pub struct Theme { @@ -27,6 +54,70 @@ pub struct Theme { pub inherits_from: Vec>, } +impl Theme { + pub fn find_icon_unscaled(&self, icon_name: &str, size: u32) -> Option { + self.find_icon(icon_name, size, 1) + } + + pub fn find_icon(&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}")); + + let base_dirs = &self.info.base_dirs; + + let sub_dirs = &self.info.index.directories; + // first, try to find an exact icon size match: + let exact_sub_dirs = sub_dirs + .into_iter() + .filter(|sub_dir| sub_dir.matches_size(size, scale)); + + for base_dir in base_dirs { + for sub_dir in exact_sub_dirs.clone() { + for file_name in &file_names { + let path = base_dir + .join(sub_dir.directory_name.as_str()) + .join(file_name); + + if path.exists() { + if let Some(file) = IconFile::from_path(&path) { + // exact match! + return Some(file); + } + } + } + } + } + + drop(exact_sub_dirs); + + // no exact match: try to find a match as close as possible instead. + let mut min_dist = u32::MAX; + let mut best_icon = None; + + for base_dir in base_dirs { + for sub_dir in sub_dirs { + let distance = sub_dir.size_distance(size, scale); + + if distance < min_dist { + for file_name in &file_names { + let path = base_dir + .join(sub_dir.directory_name.as_str()) + .join(file_name); + if path.exists() { + if let Some(file) = IconFile::from_path(&path) { + min_dist = distance; + best_icon = Some(file); + } + } + } + } + } + } + + best_icon + } +} + pub struct ThemeInfo { pub internal_name: String, pub base_dirs: Vec, @@ -96,9 +187,9 @@ impl ThemeIndex { entry.next().ok_or(ThemeParseError::NotAnIconTheme)??; let name: &str = find_attr_req(&icon_theme_section, "Name")?; - // SPEC: `Comment` is required, but most icon theme developers can't actually be arsed to + // SPEC: `Comment` is required, but most icon theme developers can't 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_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")? @@ -129,7 +220,7 @@ impl ThemeIndex { .unwrap_or(false); if !directories.contains(&title) && !is_scaled_dir { - // section isn't a listed directory! ignore! + // this section isn't a listed directory! ignore! return None; } @@ -303,9 +394,26 @@ fn find_attr_req<'a>( #[cfg(test)] mod test { + use crate::icon::{FileType, IconFile}; use crate::theme::{DirectoryType, ThemeIndex}; + use crate::Icons; use std::error::Error; + #[test] + fn test_find_icon() { + let icons = Icons::new(); + + let option = icons.find_default_icon("firefox", 128, 1); + + assert_eq!( + option, + Some(IconFile { + path: "/usr/share/icons/hicolor/128x128/apps/firefox.png".into(), + file_type: FileType::Png + }) + ) + } + #[test] fn test_parse_example_theme() -> Result<(), Box> { static EXAMPLE: &'static str = include_str!("../resources/example.index.theme");