diff --git a/Cargo.toml b/Cargo.toml index 298d939..e14a113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,16 +10,17 @@ readme = "README.md" keywords = ["icons", "gui", "freedesktop"] [dependencies] -bstr = "1.12.1" -btoi = "0.5.0" -memchr = "2.7.6" -memmap2 = "0.9" +dirs = "6.0" thiserror = "2.0" -tracing = { version = "0.1.41", default-features = false } xdg = "3.0" +tracing = "0.1.41" +ini_core = "0.2.0" +memmap2 = "0.9" [dev-dependencies] speculoos = "0.13.0" +linicon = "2.3.0" +gtk4 = "0.10" criterion = "0.7" [features] diff --git a/benches/simple_lookup.rs b/benches/simple_lookup.rs index cbfb0b1..8fd6c32 100644 --- a/benches/simple_lookup.rs +++ b/benches/simple_lookup.rs @@ -1,8 +1,9 @@ -use cosmic_freedesktop_icons::lookup; use criterion::{ - AxisScale, BenchmarkId, Criterion, PlotConfiguration, criterion_group, criterion_main, + AxisScale, BenchmarkId, Criterion, PlotConfiguration, black_box, criterion_group, + criterion_main, }; -use std::hint::black_box; +use freedesktop_icons::lookup; +use gtk4::{IconLookupFlags, IconTheme, TextDirection}; pub fn bench_lookups(c: &mut Criterion) { let mut group = c.benchmark_group("ComparisonsLookups"); @@ -10,13 +11,21 @@ pub fn bench_lookups(c: &mut Criterion) { group.plot_config(plot_config); let args = [ - "user-home", // (Best case) An icon that can be found in the current theme - "firefox", // An icon that can be found in the hicolor default theme - "com.valvesoftware.Steam", // An icon that resides in /usr/share/pixmaps - "not-found", // (Worst case) An icon that does not exist + "user-home", // (Best case) An icon that can be found in the current theme + "firefox", // An icon that can be found in the hicolor default theme + "archlinux-logo", // An icon that resides in /usr/share/pixmaps + "not-found", // (Worst case) An icon that does not exist ]; for arg in args { + group.bench_with_input(BenchmarkId::new("freedesktop-icons", arg), arg, |b, arg| { + b.iter(|| { + lookup(black_box(arg)) + .with_theme(black_box("Adwaita")) + .find() + }); + }); + group.bench_with_input( BenchmarkId::new("freedesktop-icons-cache", arg), arg, @@ -31,6 +40,33 @@ pub fn bench_lookups(c: &mut Criterion) { }); }, ); + + group.bench_with_input(BenchmarkId::new("linicon", arg), arg, |b, arg| { + b.iter(|| { + linicon::lookup_icon(black_box(arg)) + .from_theme(black_box("Adwaita")) + .with_scale(black_box(1)) + .with_size(black_box(24)) + .next() + }); + }); + + group.bench_with_input(BenchmarkId::new("gtk", arg), arg, |b, arg| { + gtk4::init().unwrap(); + let theme = IconTheme::new(); + b.iter(|| { + theme + .lookup_icon( + black_box(arg), + black_box(&[]), + black_box(24), + black_box(1), + black_box(TextDirection::None), + black_box(IconLookupFlags::empty()), + ) + .icon_name() + }); + }); } group.finish(); diff --git a/benches/tests.rs b/benches/tests.rs new file mode 100644 index 0000000..9c31812 --- /dev/null +++ b/benches/tests.rs @@ -0,0 +1,105 @@ +use freedesktop_icons::lookup; +use gtk4::{IconLookupFlags, IconTheme, TextDirection}; +use speculoos::prelude::*; +use std::path::PathBuf; + +#[test] +fn gtk_lookup() { + gtk4::init().unwrap(); + let theme = IconTheme::new(); + + let x = theme.lookup_icon( + "firefox", + &[], + 24, + 1, + TextDirection::None, + IconLookupFlags::empty(), + ); + + assert!(x.icon_name().is_some()) +} + +// Linicon sometimes fails with theme that have unknown parents +// This test only ensure we are running the correct function in the benchmarks +// And results are identical. +#[test] +fn linicon() { + // Current theme + let lin_user_home = linicon::lookup_icon("user-home") + .from_theme("Adwaita") + .with_size(24) + .with_scale(1) + .next(); + + let user_home = lookup("user-home") + .with_theme("Adwaita") + .with_size(24) + .with_scale(1) + .find(); + + asserting!("Linicon return some icon") + .that(&lin_user_home.unwrap()) + .is_ok() + .map(|icon| &icon.path) + .is_equal_to(PathBuf::from( + "/usr/share/icons/Adwaita/24x24/places/user-home.png", + )); + + asserting!("Our implementation should return the same result as linicon") + .that(&user_home) + .is_some() + .is_equal_to(PathBuf::from( + "/usr/share/icons/Adwaita/24x24/places/user-home.png", + )); + + // Fallback to hicolor + let lin_firefox = linicon::lookup_icon("firefox") + .from_theme("Adwaita") + .with_size(24) + .with_scale(1) + .next(); + + let firefox = lookup("firefox") + .with_theme("Adwaita") + .with_size(24) + .with_scale(1) + .find(); + + asserting!("Linicon return some icon") + .that(&lin_firefox.unwrap()) + .is_ok() + .map(|icon| &icon.path) + .is_equal_to(PathBuf::from( + "/usr/share/icons/hicolor/22x22/apps/firefox.png", + )); + + asserting!("Our implementation should return the same result as linicon") + .that(&firefox) + .is_some() + .is_equal_to(PathBuf::from( + "/usr/share/icons/hicolor/22x22/apps/firefox.png", + )); + + // pixmaps + let lin_archlinux = linicon::lookup_icon("archlinux-logo") + .from_theme("Adwaita") + .with_size(24) + .with_scale(1) + .next(); + + let archlinux = lookup("archlinux-logo") + .with_theme("Adwaita") + .with_size(24) + .with_scale(1) + .find(); + + asserting!("Linicon fails to fallback to pixmaps") + .that(&lin_archlinux) + .is_none(); + + asserting!("But we succeed") + .that(&archlinux) + .is_some() + .is_equal_to(PathBuf::from("/usr/share/pixmaps/archlinux-logo.png")); +} diff --git a/src/cache.rs b/src/cache.rs index cadf666..f5d7d19 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,17 +1,15 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, RwLock}; +use std::sync::LazyLock; +use std::sync::Mutex; use std::time::Instant; pub(crate) static CACHE: LazyLock = LazyLock::new(Cache::default); -type Theme = Box; -type Icon = Box; -type SizedMap = BTreeMap<(u16, u16), CacheEntry>; -type IconMap = BTreeMap; -type ThemeMap = BTreeMap; +type IconMap = BTreeMap<(String, u16, u16), CacheEntry>; +type ThemeMap = BTreeMap; #[derive(Default)] -pub(crate) struct Cache(RwLock); +pub(crate) struct Cache(Mutex); #[derive(Debug, Clone, PartialEq)] pub enum CacheEntry { @@ -25,7 +23,7 @@ pub enum CacheEntry { impl Cache { pub fn clear(&self) { - self.0.write().unwrap().clear(); + self.0.lock().unwrap().clear(); } pub fn insert>( @@ -36,38 +34,41 @@ impl Cache { icon_name: &str, icon_path: &Option

, ) { - let mut inner = self.0.write().unwrap(); + let mut theme_map = self.0.lock().unwrap(); let entry = icon_path .as_ref() .map(|path| CacheEntry::Found(path.as_ref().to_path_buf())) .unwrap_or(CacheEntry::NotFound(Instant::now())); - inner - .entry(theme.into()) - .or_insert_with(IconMap::default) - .entry(icon_name.into()) - .or_insert_with(BTreeMap::default) - .insert((size, scale), entry); + match theme_map.get_mut(theme) { + Some(icon_map) => { + icon_map.insert((icon_name.to_string(), size, scale), entry); + } + None => { + let mut icon_map = BTreeMap::new(); + icon_map.insert((icon_name.to_string(), size, scale), entry); + theme_map.insert(theme.to_string(), icon_map); + } + } } pub fn get(&self, theme: &str, size: u16, scale: u16, icon_name: &str) -> CacheEntry { - let inner = self.0.read().unwrap(); + let theme_map = self.0.lock().unwrap(); - inner + theme_map .get(theme) - .and_then(|icon_map| icon_map.get(icon_name)) - .and_then(|icon_map| icon_map.get(&(size, scale)).cloned()) + .map(|icon_map| icon_map.get(&(icon_name.to_string(), size, scale))) + .and_then(|path| path.cloned()) .unwrap_or(CacheEntry::Unknown) } pub fn reset_none(&self) { - let mut inner = self.0.write().unwrap(); - for (_theme_name, theme) in inner.iter_mut() { - for (_, cached_icons) in theme.iter_mut() { - for (_, cached_icon) in cached_icons.iter_mut() { - if matches!(cached_icon, CacheEntry::NotFound(_)) { - *cached_icon = CacheEntry::Unknown; - } + let mut theme_map = self.0.lock().unwrap(); + + for (_theme_name, theme) in theme_map.iter_mut() { + for (_icon_data, cached_icon) in theme.iter_mut() { + if matches!(cached_icon, CacheEntry::NotFound(_)) { + *cached_icon = CacheEntry::Unknown; } } } diff --git a/src/lib.rs b/src/lib.rs index 5353ad9..2b642e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,14 +51,11 @@ //! .find(); //! # } //! ``` -use memmap2::Mmap; use theme::BASE_PATHS; use crate::cache::{CACHE, CacheEntry}; -use crate::theme::{THEMES, Theme, try_build_icon_path}; -use std::hash::{Hash, Hasher}; +use crate::theme::{THEMES, try_build_icon_path}; use std::io::BufRead; -use std::ops::ControlFlow; use std::path::PathBuf; use std::time::Instant; @@ -85,10 +82,8 @@ pub fn list_themes() -> Vec { .flatten() .map(|path| &path.index) .filter_map(|index| { - let file = std::fs::File::open(index) - .and_then(|file| unsafe { Mmap::map(&file) }) - .ok()?; - let mut reader = std::io::Cursor::new(file.as_ref()); + let file = std::fs::File::open(index).ok()?; + let mut reader = std::io::BufReader::new(file); let mut line = String::new(); while let Ok(read) = reader.read_line(&mut line) { @@ -110,6 +105,56 @@ pub fn list_themes() -> Vec { themes } +/// Return the default GTK theme if set. +/// +/// ## Example +/// ```rust, no_run +/// use cosmic_freedesktop_icons::default_theme_gtk; +/// +/// let theme = default_theme_gtk(); +/// +/// assert_eq!(Some("Adwaita"), theme.as_deref()); +/// ``` +pub fn default_theme_gtk() -> Option { + // Calling gsettings is the simplest way to retrieve the default icon theme without adding + // GTK as a dependency. There seems to be several ways to set the default GTK theme + // including a file in XDG_CONFIG_HOME as well as an env var. Gsettings is the most + // straightforward method. + let gsettings = std::process::Command::new("gsettings") + .args(["get", "org.gnome.desktop.interface", "icon-theme"]) + .output() + .ok()?; + + // Only return the theme if it's in the cache. + if gsettings.status.success() { + let name = String::from_utf8(gsettings.stdout).ok()?; + let name = name.trim().trim_matches('\''); + THEMES.get(name).and_then(|themes| { + themes.first().and_then(|path| { + let file = std::fs::File::open(&path.index).ok()?; + let mut reader = std::io::BufReader::new(file); + + let mut line = String::new(); + while let Ok(read) = reader.read_line(&mut line) { + if read == 0 { + break; + } + + if let Some(name) = line.strip_prefix("Name=") { + return Some(name.trim().to_owned()); + } + + line.clear(); + } + + None + }) + }) + } else { + None + } +} + /// The lookup builder struct, holding all the lookup query parameters. pub struct LookupBuilder<'a> { name: &'a str, @@ -118,7 +163,6 @@ pub struct LookupBuilder<'a> { scale: u16, size: u16, theme: &'a str, - extra_paths: &'a [PathBuf], } /// Build an icon lookup for the given icon name. @@ -225,14 +269,6 @@ impl<'a> LookupBuilder<'a> { self } - /// Search additional directories for the icon as flat paths (no theme hierarchy). - /// These paths are searched before the theme chain. - #[inline] - pub fn with_extra_paths<'b: 'a>(mut self, paths: &'b [PathBuf]) -> Self { - self.extra_paths = paths; - self - } - /// Execute the current lookup /// if no icon is found in the current theme fallback to /// `/usr/share/icons/hicolor` theme and then to `/usr/share/pixmaps`. @@ -254,7 +290,6 @@ impl<'a> LookupBuilder<'a> { scale: 1, size: 24, theme: "hicolor", - extra_paths: &[], } } @@ -266,91 +301,80 @@ impl<'a> LookupBuilder<'a> { if self.cache { match self.cache_lookup(self.theme) { CacheEntry::Found(icon) => return Some(icon), - CacheEntry::NotFound(last_check) if last_check.elapsed().as_secs() < 5 => { + CacheEntry::NotFound(last_check) + if last_check.duration_since(Instant::now()).as_secs() < 5 => + { return None; } _ => (), } } - if !self.extra_paths.is_empty() { - let extensions = if self.force_svg { - [".svg", ".png", ".xpm"] - } else { - [".png", ".svg", ".xpm"] - }; - let mut name_buf = String::new(); - - let result = extensions - .into_iter() - .try_for_each(|ext| { - self.extra_paths.iter().try_for_each(|dir| { - let mut path = dir.clone(); - if try_build_icon_path(&mut path, &mut name_buf, self.name, ext) { - return ControlFlow::Break(path); - } - name_buf.clear(); - ControlFlow::Continue(()) - }) - }) - .break_value(); - - if result.is_some() { - return result; - } - } - - // Records theme paths that have already been searched. - let searched_themes = &mut Vec::new(); - // Record themes whose inherits have been searched. - let search_inherits = &mut Vec::new(); - // Then lookup in the given theme THEMES - .get(self.theme.as_bytes()) - .or_else(|| THEMES.get("hicolor".as_bytes())) + .get(self.theme) + .or_else(|| THEMES.get("hicolor")) .and_then(|icon_themes| { let icon = icon_themes .iter() - // Search the active icon themes - .find_map(|theme| self.search_theme(searched_themes, theme)) - // Search the inherits of those icon themes. - .or_else(|| { - icon_themes.iter().find_map(|t| { - self.search_theme_inherits(search_inherits, searched_themes, t) - }) + .find_map(|theme| { + theme.try_get_icon(self.name, self.size, self.scale, self.force_svg) }) - // Search the cosmic icon theme - .or_else(|| self.search_inherited_theme(searched_themes, "Cosmic".as_bytes())) - // Search the hicolor icon theme if it was not previously searched - .or_else(|| self.search_inherited_theme(searched_themes, "hicolor".as_bytes())) - // GNOME applications may rely on the gnome theme - .or_else(|| self.search_inherited_theme(searched_themes, "gnome".as_bytes())) - // Ubuntu applications may require Yaru - .or_else(|| self.search_inherited_theme(searched_themes, "Yaru".as_bytes())) .or_else(|| { - let extensions = if self.force_svg { - [".svg", ".png", ".xpm"] - } else { - [".png", ".svg", ".xpm"] - }; + // Fallback to the parent themes recursively + let mut parents = icon_themes + .iter() + .flat_map(|t| { + let Ok(file) = theme::read_ini_theme(&t.index) else { + return Vec::new(); + }; - let mut name_buf = String::new(); + let Ok(file) = std::str::from_utf8(file.as_ref()) else { + return Vec::new(); + }; - extensions - .into_iter() - .try_for_each(|ext| { - BASE_PATHS.iter().try_for_each(|theme_base_dir| { - let mut path = theme_base_dir.clone(); - if try_build_icon_path(&mut path, &mut name_buf, self.name, ext) - { - return ControlFlow::Break(path); - } - name_buf.clear(); - ControlFlow::Continue(()) + t.inherits(file) + .into_iter() + .map(String::from) + .collect::>() + }) + .collect::>(); + parents.dedup(); + parents.into_iter().find_map(|parent| { + THEMES.get(&parent).and_then(|parent| { + parent.iter().find_map(|t| { + t.try_get_icon(self.name, self.size, self.scale, self.force_svg) }) }) - .break_value() + }) + }) + .or_else(|| { + THEMES.get("hicolor").and_then(|icon_themes| { + icon_themes.iter().find_map(|theme| { + theme.try_get_icon(self.name, self.size, self.scale, self.force_svg) + }) + }) + }) + .or_else(|| { + for theme_base_dir in BASE_PATHS.iter() { + if let Some(icon) = + try_build_icon_path(self.name, theme_base_dir, self.force_svg) + { + return Some(icon); + } + } + None + }) + .or_else(|| { + try_build_icon_path(self.name, "/usr/share/pixmaps", self.force_svg) + }) + .or_else(|| { + let p = PathBuf::from(&self.name); + if let (Some(name), Some(parent)) = (p.file_stem(), p.parent()) { + try_build_icon_path(&name.to_string_lossy(), parent, self.force_svg) + } else { + None + } }); if self.cache { @@ -381,190 +405,30 @@ impl<'a> LookupBuilder<'a> { CACHE.insert(theme, self.size, self.scale, self.name, &icon); icon } - - /// Search a theme by its path for a matching icon if not already searched. - fn search_theme(&self, searched_themes: &mut Vec, theme: &Theme) -> Option { - // Store hash of the theme. - let theme_hash = { - let mut hasher = std::hash::DefaultHasher::new(); - theme.path.0.hash(&mut hasher); - hasher.finish() - }; - - if let Err(pos) = searched_themes.binary_search(&theme_hash) { - searched_themes.insert(pos, theme_hash); - return theme.try_get_icon(self.name, self.size, self.scale, self.force_svg); - } - - None - } - - // Search the inherits of a theme if not already searched. - fn search_theme_inherits( - &self, - search_inherits: &mut Vec, - searched_themes: &mut Vec, - theme: &Theme, - ) -> Option { - // Store hash of the theme. - let theme_hash = { - let mut hasher = std::hash::DefaultHasher::new(); - theme.path.0.hash(&mut hasher); - hasher.finish() - }; - - if let Err(pos) = search_inherits.binary_search(&theme_hash) { - search_inherits.insert(pos, theme_hash); - let Ok(file) = theme::read_ini_theme(&theme.index) else { - return None; - }; - - // Search all inherited themes that we haven't already searched - return theme - .inherits(file.as_ref()) - .into_iter() - .find_map(|parent| self.search_inherited_theme(searched_themes, parent)); - } - - None - } - - /// Search the inherits of a theme by its name if not already searched. - fn search_inherited_theme( - &self, - searched_themes: &mut Vec, - theme: &[u8], - ) -> Option { - THEMES - .get(theme)? - .iter() - .find_map(|t| self.search_theme(searched_themes, t)) - } } // WARNING: these test are highly dependent on your installed icon-themes. // If you want to run them, make sure you have 'Papirus' and 'Arc' icon-themes installed. #[cfg(test)] +#[cfg(feature = "local_tests")] mod test { use crate::{CACHE, CacheEntry, lookup}; use speculoos::prelude::*; use std::path::PathBuf; #[test] - fn hicolor_firefox_24_png() { + fn simple_lookup() { let firefox = lookup("firefox").find(); - asserting!("Firefox contains only a 16x16 and 32x32 icon, so 16x16 should be returned") - .that(&firefox) - .is_some() - .is_equal_to(PathBuf::from( - "/usr/share/icons/hicolor/16x16/apps/firefox.png", - )); - } - - #[test] - fn hicolor_firefox_48_png() { - let firefox = lookup("firefox").with_size(48).find(); - - asserting!("Firefox has a 48x48 icon, so that should be returned") - .that(&firefox) - .is_some() - .is_equal_to(PathBuf::from( - "/usr/share/icons/hicolor/48x48/apps/firefox.png", - )); - } - - #[test] - fn hicolor_firefox_svg_fallback_to_png() { - let firefox = lookup("firefox").force_svg().find(); - asserting!("Lookup with no parameters should return an existing icon") .that(&firefox) .is_some() .is_equal_to(PathBuf::from( - "/usr/share/icons/hicolor/16x16/apps/firefox.png", + "/usr/share/icons/hicolor/22x22/apps/firefox.png", )); } #[test] - fn cosmic_weather_storm_symbolic() { - let firefox = lookup("weather-storm-symbolic").find(); - - asserting!("Is the cosmic icon theme installed?") - .that(&firefox) - .is_some() - .is_equal_to(PathBuf::from( - "/usr/share/icons/Cosmic/scalable/status/weather-storm-symbolic.svg", - )); - } - - #[test] - fn cosmic_weather_storm_symbolic_force_svg() { - let firefox = lookup("weather-storm-symbolic").force_svg().find(); - - asserting!("Is the cosmic icon theme installed?") - .that(&firefox) - .is_some() - .is_equal_to(PathBuf::from( - "/usr/share/icons/Cosmic/scalable/status/weather-storm-symbolic.svg", - )); - } - - #[test] - fn flatpak_slack() { - let home = std::env::home_dir().unwrap(); - - assert_eq!( - lookup("com.slack.Slack").find(), - Some(home.join( - ".local/share/flatpak/exports/share/icons/hicolor/scalable/apps/com.slack.Slack.svg" - )), - "Is the Slack flatpak installed locally?" - ); - } - - #[test] - fn vscode_pixmap() { - assert_eq!( - lookup("vscode").find(), - Some(PathBuf::from("/usr/share/pixmaps/vscode.png")), - "Is VS Code installed locally on the host?" - ); - } - - #[test] - fn libreoffice_startcenter() { - assert_eq!( - lookup("libreoffice-startcenter").find(), - Some(PathBuf::from( - "/usr/share/icons/hicolor/24x24/apps/libreoffice-startcenter.png" - )), - "Is libreoffice installed locally on the host?" - ); - } - - #[test] - fn gnome_advanced_network() { - assert_eq!( - lookup("preferences-system-network").find(), - Some(PathBuf::from( - "/usr/share/icons/gnome/24x24/categories/preferences-system-network.png" - )), - "Is the gnome icon theme installed?" - ); - } - - #[test] - fn ubuntu_additional_drivers() { - assert_eq!( - lookup("jockey").find(), - Some(PathBuf::from("/usr/share/icons/Yaru/24x24/apps/jockey.png")), - "Is the gnome icon theme installed?" - ); - } - - #[test] - #[cfg(feature = "local_tests")] fn theme_lookup() { let firefox = lookup("firefox").with_theme("Papirus").find(); @@ -577,7 +441,6 @@ mod test { } #[test] - #[cfg(feature = "local_tests")] fn should_fallback_to_parent_theme() { let icon = lookup("video-single-display-symbolic") .with_theme("Arc") @@ -592,7 +455,6 @@ mod test { } #[test] - #[cfg(feature = "local_tests")] fn should_fallback_to_pixmaps_utlimately() { let archlinux_logo = lookup("archlinux-logo") .with_size(16) @@ -606,6 +468,26 @@ mod test { .is_equal_to(PathBuf::from("/usr/share/pixmaps/archlinux-logo.png")); } + #[test] + fn compare_to_linincon_with_theme() { + let lin_wireshark = linicon::lookup_icon("wireshark") + .next() + .unwrap() + .unwrap() + .path; + + let wireshark = lookup("wireshark") + .with_size(16) + .with_scale(1) + .with_theme("Papirus") + .find(); + + asserting!("Given the same input parameter, lookup should output be the same as linincon") + .that(&wireshark) + .is_some() + .is_equal_to(lin_wireshark); + } + #[test] fn should_not_attempt_to_lookup_a_not_found_cached_icon() { let not_found = lookup("not-found").with_cache().find(); @@ -614,9 +496,8 @@ mod test { let expected_cache_result = CACHE.get("hicolor", 24, 1, "not-found"); - assert!( - matches!(expected_cache_result, CacheEntry::NotFound(..)), - "When lookup fails a first time, subsequent attempts should fail from cache" - ); + asserting!("When lookup fails a first time, subsequent attempts should fail from cache") + .that(&expected_cache_result) + .is_equal_to(CacheEntry::NotFound); } } diff --git a/src/theme/directories.rs b/src/theme/directories.rs index ca38f94..c437d31 100644 --- a/src/theme/directories.rs +++ b/src/theme/directories.rs @@ -10,31 +10,47 @@ pub struct Directory<'a> { } impl Directory<'_> { - pub fn directory_size_distance(&self, size: i16, scale: i16) -> i16 { - match self.type_ { - DirectoryType::Fixed => self.size * self.scale - size * scale, + pub fn match_size(&self, size: u16, scale: u16) -> bool { + let scale = scale as i16; + let size = size as i16; - DirectoryType::Scalable => { - let scaled_requested_size = size * scale; - let min_scaled_size = self.minsize * self.scale; - if scaled_requested_size < min_scaled_size { - min_scaled_size - scaled_requested_size - } else { - let max_scaled_size = self.maxsize * self.scale; - if scaled_requested_size < max_scaled_size { - scaled_requested_size - max_scaled_size - } else { - 0 - } + if self.scale != scale { + false + } else { + match self.type_ { + DirectoryType::Fixed => self.size == size, + DirectoryType::Scalable => self.minsize <= size && size <= self.maxsize, + DirectoryType::Threshold => { + self.size - self.threshold <= size && size <= self.size + self.threshold } } + } + } + pub fn directory_size_distance(&self, size: u16, scale: u16) -> i16 { + let scaled_size = self.size * self.scale; + let min_scaled_size = self.minsize * self.scale; + let max_scaled_size = self.maxsize * self.scale; + let scale = scale as i16; + let size = size as i16; + let scaled_requested_size = size * scale; + + match self.type_ { + DirectoryType::Fixed => scaled_size - scaled_requested_size, + DirectoryType::Scalable => { + if scaled_requested_size < min_scaled_size { + min_scaled_size - scaled_requested_size + } else if scaled_requested_size < max_scaled_size { + scaled_requested_size - max_scaled_size + } else { + 0 + } + } DirectoryType::Threshold => { - let scaled_requested_size = size * scale; if scaled_requested_size < (self.size - self.threshold) * scale { - self.minsize * self.scale - scaled_requested_size + min_scaled_size - scaled_requested_size } else if scaled_requested_size > (self.size + self.threshold) * scale { - scaled_requested_size - self.maxsize * self.scale + scaled_requested_size - max_scaled_size } else { 0 } @@ -56,11 +72,11 @@ impl Default for DirectoryType { } } -impl From<&[u8]> for DirectoryType { - fn from(value: &[u8]) -> Self { - match value[0] { - b'F' => DirectoryType::Fixed, - b'S' => DirectoryType::Scalable, +impl From<&str> for DirectoryType { + fn from(value: &str) -> Self { + match value { + "Fixed" => DirectoryType::Fixed, + "Scalable" => DirectoryType::Scalable, _ => DirectoryType::Threshold, } } diff --git a/src/theme/error.rs b/src/theme/error.rs new file mode 100644 index 0000000..5ca693c --- /dev/null +++ b/src/theme/error.rs @@ -0,0 +1,11 @@ +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Error, Debug)] +pub(crate) enum ThemeError { + #[error("No 'index.theme' file for {0}")] + ThemeIndexNotFound(PathBuf), + #[error("IoError: {0}")] + IoError(#[from] io::Error), +} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index a73bf3c..5e0b1a8 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,19 +1,19 @@ -use crate::theme::directories::DirectoryType; +use crate::theme::error::ThemeError; use crate::theme::paths::ThemePath; use memmap2::Mmap; pub(crate) use paths::BASE_PATHS; -use std::cmp::Ordering; use std::collections::BTreeMap; -use std::ops::ControlFlow; -use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::LazyLock; mod directories; +pub mod error; mod parse; mod paths; -pub static THEMES: LazyLock, Vec>> = LazyLock::new(get_all_themes); +type Result = std::result::Result; + +pub static THEMES: LazyLock>> = LazyLock::new(get_all_themes); #[inline] pub fn read_ini_theme(path: &Path) -> std::io::Result { @@ -33,99 +33,76 @@ impl Theme { name: &str, size: u16, scale: u16, - prefer_svg: bool, + force_svg: bool, ) -> Option { let file = read_ini_theme(&self.index).ok()?; - self.try_get_icon_closest_size(file.as_ref(), name, size, scale, prefer_svg) + let file = std::str::from_utf8(file.as_ref()).ok()?; + self.try_get_icon_exact_size(file, name, size, scale, force_svg) + .or_else(|| self.try_get_icon_closest_size(file, name, size, scale, force_svg)) + } + + #[inline] + fn try_get_icon_exact_size( + &self, + file: &str, + name: &str, + size: u16, + scale: u16, + force_svg: bool, + ) -> Option { + self.match_size(file, size, scale) + .find_map(|path| try_build_icon_path(name, path, force_svg)) + } + + #[inline] + fn match_size<'a>( + &'a self, + file: &'a str, + size: u16, + scale: u16, + ) -> impl Iterator + 'a { + let dirs = self.get_all_directories(file); + + dirs.filter(move |directory| directory.match_size(size, scale)) + .map(|dir| dir.name) + .map(|dir| self.path().join(dir)) } #[inline] fn try_get_icon_closest_size( &self, - file: &[u8], + file: &str, name: &str, size: u16, scale: u16, - prefer_svg: bool, + force_svg: bool, ) -> Option { - self.try_fold_icon_path( - self.closest_match_size(file, size, scale, prefer_svg), - name, - prefer_svg, - ) + self.closest_match_size(file, size, scale) + .iter() + .find_map(|path| try_build_icon_path(name, path, force_svg)) } - fn try_fold_icon_path<'a>( - &self, - dir_names: Vec<(&'a str, i16, bool)>, - name: &str, - prefer_svg: bool, - ) -> Option { - let extensions = if prefer_svg { - [".svg", ".png", ".xpm"] - } else { - [".png", ".svg", ".xpm"] - }; + fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec { + let dirs = self.get_all_directories(file); - extensions.into_iter().find_map(|ext| { - dir_names - .iter() - .try_fold( - (self.path().clone(), String::new()), - move |(mut path, mut name_buf), (dir_name, _, _)| { - path.push(dir_name); - if try_build_icon_path(&mut path, &mut name_buf, name, ext) { - ControlFlow::Break(path) - } else { - name_buf.clear(); - let components = dir_name - .as_bytes() - .iter() - .fold(2, |n, c| n + (*c == b'/') as u32) - as usize; + let mut dirs: Vec<_> = dirs + .filter_map(|directory| { + let distance = directory.directory_size_distance(size, scale); + if distance < i16::MAX { + Some((directory, distance.abs())) + } else { + None + } + }) + .collect(); - for _ in 0..components { - path.pop(); - } + dirs.sort_by(|(_, a), (_, b)| a.cmp(b)); - ControlFlow::Continue((path, name_buf)) - } - }, - ) - .break_value() - }) - } - - fn closest_match_size<'a>( - &'a self, - file: &'a [u8], - size: u16, - scale: u16, - prefer_svg: bool, - ) -> Vec<(&'a str, i16, bool)> { - let mut unsorted = self.get_all_directories(file).fold( - Vec::<(&'a str, i16, bool)>::new(), - |mut unsorted, directory| { - let is_scalable = matches!(directory.type_, DirectoryType::Scalable); - let distance = directory.directory_size_distance(size as i16, scale as i16); - unsorted.push((directory.name, distance.abs(), is_scalable)); - unsorted - }, - ); - - unsorted.sort_by(|a, b| { - let ordering = if prefer_svg { - b.2.cmp(&a.2) - } else { - a.2.cmp(&b.2) - }; - match ordering { - Ordering::Equal => a.1.cmp(&b.1), - _ => ordering, - } - }); - - unsorted + dirs.iter() + .map(|(dir, _)| dir) + .map(|dir| dir.name) + .map(|dir| self.path().join(dir)) + .collect() } fn path(&self) -> &PathBuf { @@ -133,28 +110,45 @@ impl Theme { } } -pub(super) fn try_build_icon_path<'a>( - path: &'a mut PathBuf, - name_buf: &'a mut String, +pub(super) fn try_build_icon_path>( name: &str, - extension: &'static str, -) -> bool { - name_buf.push_str(name); - path.push(name); - try_build_ext(path, name_buf, name, extension) + path: P, + force_svg: bool, +) -> Option { + if force_svg { + try_build_svg(name, path.as_ref()) + .or_else(|| try_build_png(name, path.as_ref())) + .or_else(|| try_build_xmp(name, path.as_ref())) + } else { + try_build_png(name, path.as_ref()) + .or_else(|| try_build_svg(name, path.as_ref())) + .or_else(|| try_build_xmp(name, path.as_ref())) + } } -#[inline] -fn try_build_ext(path: &mut PathBuf, name_buf: &mut String, name: &str, ext: &'static str) -> bool { - name_buf.truncate(name.len()); - name_buf.push_str(ext); - path.set_file_name(&name_buf); - path.exists() +fn try_build_svg>(name: &str, path: P) -> Option { + let path = path.as_ref(); + let svg = path.join(format!("{name}.svg")); + + if svg.exists() { Some(svg) } else { None } +} + +fn try_build_png>(name: &str, path: P) -> Option { + let path = path.as_ref(); + let png = path.join(format!("{name}.png")); + + if png.exists() { Some(png) } else { None } +} + +fn try_build_xmp>(name: &str, path: P) -> Option { + let path = path.as_ref(); + let xmp = path.join(format!("{name}.xmp")); + if xmp.exists() { Some(xmp) } else { None } } // Iter through the base paths and get all theme directories -pub(super) fn get_all_themes() -> BTreeMap, Vec> { - let mut icon_themes = BTreeMap::, Vec<_>>::new(); +pub(super) fn get_all_themes() -> BTreeMap> { + let mut icon_themes = BTreeMap::<_, Vec<_>>::new(); let mut found_indices = BTreeMap::new(); let mut to_revisit = Vec::new(); @@ -174,10 +168,8 @@ pub(super) fn get_all_themes() -> BTreeMap, Vec> { if fallback_index.is_none() { found_indices.insert(name.clone(), theme.index.clone()); } - icon_themes - .entry(name.as_bytes().to_owned()) - .or_default() - .push(theme); + let name = name.to_string_lossy().to_string(); + icon_themes.entry(name).or_default().push(theme); } else if entry.path().is_dir() { to_revisit.push(entry); } @@ -188,10 +180,8 @@ pub(super) fn get_all_themes() -> BTreeMap, Vec> { let name = entry.file_name(); let fallback_index = found_indices.get(&name); if let Some(theme) = Theme::from_path(entry.path(), fallback_index) { - icon_themes - .entry(name.as_bytes().to_owned()) - .or_default() - .push(theme); + let name = name.to_string_lossy().to_string(); + icon_themes.entry(name).or_default().push(theme); } } @@ -200,26 +190,24 @@ pub(super) fn get_all_themes() -> BTreeMap, Vec> { impl Theme { pub(crate) fn from_path>(path: P, index: Option<&PathBuf>) -> Option { - let mut path = path.as_ref().to_path_buf(); - let is_dir = path.is_dir(); - path.push("index.theme"); - let local_index_exists = path.exists(); - let has_index = local_index_exists || index.is_some(); + let path = path.as_ref(); - if !has_index || !is_dir { + let has_index = path.join("index.theme").exists() || index.is_some(); + + if !has_index || !path.is_dir() { return None; } - index - .cloned() - .or_else(|| local_index_exists.then_some(path.clone())) - .map(|index| Theme { - path: ThemePath({ - path.pop(); - path - }), - index, - }) + let path = ThemePath(path.into()); + + match (index, path.index()) { + (Some(index), _) => Some(Theme { + path, + index: index.clone(), + }), + (None, Ok(index)) => Some(Theme { path, index }), + _ => None, + } } } @@ -231,61 +219,40 @@ mod test { #[test] fn get_one_icon() { - let themes = THEMES.get(&b"Adwaita"[..]).unwrap(); + let themes = THEMES.get("Adwaita").unwrap(); println!( "{:?}", themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - t.try_get_icon_closest_size(file.as_ref(), "edit-delete-symbolic", 24, 1, false) + let file = std::str::from_utf8(file.as_ref()).ok()?; + t.try_get_icon_exact_size(file, "edit-delete-symbolic", 24, 1, false) }) ); } #[test] fn should_get_png_first() { - let themes = THEMES.get(&b"hicolor"[..]).unwrap(); + let themes = THEMES.get("hicolor").unwrap(); let icon = themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - t.try_get_icon_closest_size(file.as_ref(), "blueman", 22, 1, false) + let file = std::str::from_utf8(file.as_ref()).ok()?; + t.try_get_icon_exact_size(file, "blueman", 24, 1, true) }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( "/usr/share/icons/hicolor/22x22/apps/blueman.png", )); } - #[test] - fn should_get_png_first_92() { - let themes = THEMES.get(&b"hicolor"[..]).unwrap(); - let icon = themes.iter().find_map(|t| { - let file = super::read_ini_theme(&t.index).ok()?; - t.try_get_icon_closest_size(file.as_ref(), "blueman", 92, 1, false) - }); - assert_that!(icon).is_some().is_equal_to(PathBuf::from( - "/usr/share/icons/hicolor/96x96/apps/blueman.png", - )); - } - #[test] fn should_get_svg_first() { - let themes = THEMES.get(&b"hicolor"[..]).unwrap(); + let themes = THEMES.get("hicolor").unwrap(); let icon = themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - t.try_get_icon_closest_size(file.as_ref(), "blueman", 24, 1, true) + let file = std::str::from_utf8(file.as_ref()).ok()?; + t.try_get_icon_exact_size(file, "blueman", 24, 1, false) }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( - "/usr/share/icons/hicolor/scalable/apps/blueman.svg", - )); - } - - #[test] - fn should_get_svg_first_96() { - let themes = THEMES.get(&b"hicolor"[..]).unwrap(); - let icon = themes.iter().find_map(|t| { - let file = super::read_ini_theme(&t.index).ok()?; - t.try_get_icon_closest_size(file.as_ref(), "blueman", 96, 1, true) - }); - assert_that!(icon).is_some().is_equal_to(PathBuf::from( - "/usr/share/icons/hicolor/scalable/apps/blueman.svg", + "/usr/share/icons/hicolor/22x22/apps/blueman.png", )); } } diff --git a/src/theme/parse.rs b/src/theme/parse.rs index 61cde2d..64db96e 100644 --- a/src/theme/parse.rs +++ b/src/theme/parse.rs @@ -1,48 +1,76 @@ use crate::theme::Theme; use crate::theme::directories::{Directory, DirectoryType}; -use bstr::BStr; + +fn icon_theme_section(file: &str) -> impl Iterator + '_ { + ini_core::Parser::new(file) + .skip_while(|item| *item != ini_core::Item::Section("Icon Theme")) + .take_while(|item| match item { + ini_core::Item::Section(value) => *value == "Icon Theme", + _ => true, + }) + .filter_map(|item| { + if let ini_core::Item::Property(key, value) = item { + Some((key, value?)) + } else { + None + } + }) +} + +#[derive(Debug)] +enum DirectorySection<'a> { + Property(&'a str, &'a str), + EndSection, + Section(&'a str), +} + +fn sections(file: &str) -> impl Iterator> { + ini_core::Parser::new(file).filter_map(move |item| match item { + ini_core::Item::Property(key, Some(value)) => Some(DirectorySection::Property(key, value)), + ini_core::Item::Section(section) => Some(DirectorySection::Section(section)), + ini_core::Item::SectionEnd => Some(DirectorySection::EndSection), + _ => None, + }) +} impl Theme { pub(super) fn get_all_directories<'a>( &'a self, - file: &'a [u8], + file: &'a str, ) -> impl Iterator> + 'a { let mut iterator = sections(file); std::iter::from_fn(move || { - let mut is_icon_theme = false; let mut name = ""; let mut size = None; let mut max_size = None; let mut min_size = None; let mut threshold = None; let mut scale = None; + // let mut context = None; let mut dtype = DirectoryType::default(); #[allow(clippy::while_let_on_iterator)] while let Some(event) = iterator.next() { match event { DirectorySection::Property(key, value) => { - if is_icon_theme { + if name.is_empty() || name == "Icon Theme" { continue; } match key { - b"Size" => size = btoi::btoi(value).ok(), - b"Scale" => scale = btoi::btoi(value).ok(), - b"Type" => dtype = DirectoryType::from(value), - b"MaxSize" => max_size = btoi::btoi(value).ok(), - b"MinSize" => min_size = btoi::btoi(value).ok(), - b"Threshold" => threshold = btoi::btoi(value).ok(), + "Size" => size = str::parse(value).ok(), + "Scale" => scale = str::parse(value).ok(), + // "Context" => context = Some(value), + "Type" => dtype = DirectoryType::from(value), + "MaxSize" => max_size = str::parse(value).ok(), + "MinSize" => min_size = str::parse(value).ok(), + "Threshold" => threshold = str::parse(value).ok(), _ => (), } } DirectorySection::Section(new_name) => { - let Ok(new_name) = std::str::from_utf8(new_name) else { - return None; - }; - is_icon_theme = new_name == "Icon Theme"; name = new_name; size = None; max_size = None; @@ -53,7 +81,7 @@ impl Theme { } DirectorySection::EndSection => { - if is_icon_theme { + if name.is_empty() || name == "Icon Theme" { continue; } @@ -63,6 +91,7 @@ impl Theme { name, size, scale: scale.unwrap_or(1), + // context, type_: dtype, maxsize: max_size.unwrap_or(size), minsize: min_size.unwrap_or(size), @@ -76,391 +105,29 @@ impl Theme { }) } - pub fn inherits<'a>(&self, file: &'a [u8]) -> impl Iterator { + pub fn inherits<'a>(&self, file: &'a str) -> Vec<&'a str> { icon_theme_section(file) - .find(|&(key, _)| key == b"Inherits") - .into_iter() - .flat_map(|(_, parents)| { - BStr::new(parents) - .split(|&char| char == b',') + .find(|&(key, _)| key == "Inherits") + .map(|(_, parents)| { + parents + .split(',') // Filtering out 'hicolor' since we are going to fallback there anyway - .filter(|parent| parent != &b"hicolor") + .filter(|parent| parent != &"hicolor") + .collect() }) + .unwrap_or_default() } } -#[derive(Debug)] -enum DirectorySection<'a> { - Property(&'a [u8], &'a [u8]), - EndSection, - Section(&'a [u8]), -} - -fn sections(file: &[u8]) -> impl Iterator> { - let mut finished = false; - let mut table_found = false; - let mut section: &[u8] = b""; - let mut prev = 0; - let mut line_indices = memchr::memchr_iter(b'\n', file); - - std::iter::from_fn(move || { - if finished { - return None; - } - - if !section.is_empty() { - let new_section = section; - section = b""; - return Some(DirectorySection::Section(new_section)); - } - - loop { - let line_pos = match line_indices.next() { - Some(pos) => pos, - None => { - let value = if !finished { - Some(DirectorySection::EndSection) - } else { - None - }; - finished = true; - return value; - } - }; - - let line = BStr::new(unsafe { - // Indices from memchr are valid. - file.get_unchecked(prev..line_pos) - }) - .trim_ascii(); - prev = line_pos + 1; - - if line.is_empty() { - continue; - } - - if line[0] == b'[' { - section = &line[1..line.len() - 1]; - if table_found { - return Some(DirectorySection::EndSection); - } else { - table_found = true; - return Some(DirectorySection::Section(section)); - } - } - - if let Some((key, value)) = memchr::memchr(b'=', line).map(|pos| unsafe { - // Position was already validated by memchr. - line.split_at_unchecked(pos) - }) { - return Some(DirectorySection::Property(key, &value[1..])); - } - } - }) -} - -fn icon_theme_section(file: &[u8]) -> impl Iterator + '_ { - let mut found_table = false; - let mut prev = 0; - let mut line_indices = memchr::memchr_iter(b'\n', file); - - std::iter::from_fn(move || { - loop { - let line_pos = line_indices.next()?; - let line = BStr::new(unsafe { - // Indices from memchr are valid. - file.get_unchecked(prev..line_pos) - }) - .trim_ascii(); - prev = line_pos + 1; - - if line.is_empty() { - continue; - } - - if line[0] == b'[' { - if found_table { - return None; - } else { - let section = &line[1..line.len() - 1]; - found_table = true; - if section != b"Icon Theme" { - return None; - } - } - } - - if let Some((key, value)) = memchr::memchr(b'=', line).map(|pos| unsafe { - // Position was already validated by memchr. - line.split_at_unchecked(pos) - }) { - return Some((key, &value[1..])); - } - } - }) -} - #[cfg(test)] - mod test { - const ADWAITA_INDEX: &str = "[Icon Theme] -Name=Adwaita\u{0020} -Comment=The Only One -Example=folder -Inherits=hicolor - -# KDE Specific Stuff -DisplayDepth=32 - -# Directory list -Directories=16x16/actions,16x16/apps,16x16/categories,16x16/devices,16x16/emblems,16x16/emotes,16x16/legacy,16x16/mimetypes,16x16/places,16x16/status,16x16/ui,scalable/devices,scalable/mimetypes,scalable/places,scalable/status,scalable/actions,scalable/apps,scalable/categories,scalable/emblems,scalable/emotes,scalable/legacy,scalable/ui,symbolic-up-to-32/status,symbolic/actions,symbolic/apps,symbolic/categories,symbolic/devices,symbolic/emblems,symbolic/emotes,symbolic/mimetypes,symbolic/places,symbolic/status,symbolic/legacy,symbolic/ui, - -[16x16/actions] -Context=Actions -Size=16 -Type=Fixed - -[16x16/apps] -Context=Applications -Size=16 -Type=Fixed - -[16x16/categories] -Context=Categories -Size=16 -Type=Fixed - -[16x16/devices] -Context=Devices -Size=16 -Type=Fixed - -[16x16/emblems] -Context=Emblems -Size=16 -Type=Fixed - -[16x16/emotes] -Context=Emotes -Size=16 -Type=Fixed - -[16x16/legacy] -Context=Legacy -Size=16 -Type=Fixed - -[16x16/mimetypes] -Context=MimeTypes -Size=16 -Type=Fixed - -[16x16/places] -Context=Places -Size=16 -Type=Fixed - -[16x16/status] -Context=Status -Size=16 -Type=Fixed - -[16x16/ui] -Context=UI -Size=16 -Type=Fixed - -[scalable/devices] -Context=Devices -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/mimetypes] -Context=MimeTypes -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/places] -Context=Places -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/status] -Context=Status -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/actions] -Context=Actions -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/apps] -Context=Applications -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/categories] -Context=Categories -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/emblems] -Context=Emblems -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/emotes] -Context=Emotes -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/legacy] -Context=Legacy -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[scalable/ui] -Context=UI -Size=128 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic-up-to-32/status] -Context=Status -Size=16 -MinSize=16 -MaxSize=32 -Type=Scalable - -[symbolic/actions] -Context=Actions -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/apps] -Context=Applications -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/categories] -Context=Categories -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/devices] -Context=Devices -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/emblems] -Context=Emblems -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/emotes] -Context=Emotes -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/mimetypes] -Context=MimeTypes -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/places] -Context=Places -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/status] -Context=Status -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/legacy] -Context=Legacy -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable - -[symbolic/ui] -Context=UI -Size=16 -MinSize=8 -MaxSize=512 -Type=Scalable"; - - #[test] - fn icon_theme_section() { - let mut iterator = super::icon_theme_section(ADWAITA_INDEX.as_bytes()); - - let (key, value) = iterator.next().unwrap(); - assert_eq!(key, b"Name"); - assert_eq!(value, b"Adwaita"); - let (key, value) = iterator.next().unwrap(); - assert_eq!(key, b"Comment"); - assert_eq!(value, b"The Only One"); - let (key, value) = iterator.next().unwrap(); - assert_eq!(key, b"Example"); - assert_eq!(value, b"folder"); - let (key, value) = iterator.next().unwrap(); - assert_eq!(key, b"Inherits"); - assert_eq!(value, b"hicolor"); - let (key, value) = iterator.next().unwrap(); - assert_eq!(key, b"DisplayDepth"); - assert_eq!(value, b"32"); - let (key, value) = iterator.next().unwrap(); - assert_eq!(key, b"Directories"); - assert_eq!(value, b"16x16/actions,16x16/apps,16x16/categories,16x16/devices,16x16/emblems,16x16/emotes,16x16/legacy,16x16/mimetypes,16x16/places,16x16/status,16x16/ui,scalable/devices,scalable/mimetypes,scalable/places,scalable/status,scalable/actions,scalable/apps,scalable/categories,scalable/emblems,scalable/emotes,scalable/legacy,scalable/ui,symbolic-up-to-32/status,symbolic/actions,symbolic/apps,symbolic/categories,symbolic/devices,symbolic/emblems,symbolic/emotes,symbolic/mimetypes,symbolic/places,symbolic/status,symbolic/legacy,symbolic/ui,"); - assert_eq!(iterator.next(), None); - } + use crate::THEMES; + use speculoos::prelude::*; #[test] #[cfg(feature = "local_tests")] fn should_get_theme_parents() { - use speculoos::prelude::*; - for theme in crate::THEMES.get("Arc").unwrap() { + for theme in THEMES.get("Arc").unwrap() { let file = crate::theme::read_ini_theme(&theme.index).ok().unwrap(); let file = std::str::from_utf8(file.as_ref()).ok().unwrap(); let parents = theme.inherits(file); diff --git a/src/theme/paths.rs b/src/theme/paths.rs index 9ed7212..c4581e6 100644 --- a/src/theme/paths.rs +++ b/src/theme/paths.rs @@ -1,38 +1,50 @@ +use dirs::home_dir; use std::path::PathBuf; use std::sync::LazyLock; use xdg::BaseDirectories; +use crate::theme; +use crate::theme::error::ThemeError; + pub(crate) static BASE_PATHS: LazyLock> = LazyLock::new(icon_theme_base_paths); /// Look in $HOME/.icons (for backwards compatibility), in $XDG_DATA_DIRS/icons, in $XDG_DATA_DIRS/pixmaps and in /usr/share/pixmaps (in that order). /// Paths that are not found are filtered out. fn icon_theme_base_paths() -> Vec { let base_dirs = BaseDirectories::new(); - - let data_dirs = base_dirs + let mut data_dirs: Vec<_> = base_dirs .get_data_dirs() .into_iter() - .flat_map(|p| [p.join("icons"), p.join("pixmaps")]); + .flat_map(|p| [p.join("icons"), p.join("pixmaps")]) + .collect(); - let data_home_dirs = base_dirs - .get_data_home() - .into_iter() - .flat_map(|data_home| [data_home.join("icons"), data_home.join("pixmaps")].into_iter()); + if let Some(data_home) = base_dirs.get_data_home() { + data_dirs.push(data_home.join("icons")); + data_dirs.push(data_home.join("pixmaps")); + } - let home_dir = std::env::home_dir() - .into_iter() - .map(|home| home.join(".icons")); - - data_dirs - .chain(data_home_dirs) - .chain(home_dir) - .filter(|p| p.exists()) - .collect() + match home_dir().map(|home| home.join(".icons")) { + Some(home_icon_dir) => data_dirs.push(home_icon_dir), + None => tracing::warn!("No $HOME directory found"), + } + data_dirs.into_iter().filter(|p| p.exists()).collect() } #[derive(Clone, Debug)] pub struct ThemePath(pub PathBuf); +impl ThemePath { + pub(super) fn index(&self) -> theme::Result { + let index = self.0.join("index.theme"); + + if !index.exists() { + return Err(ThemeError::ThemeIndexNotFound(index)); + } + + Ok(index) + } +} + #[cfg(test)] mod test { use crate::theme::paths::icon_theme_base_paths; @@ -42,7 +54,7 @@ mod test { #[test] fn should_get_all_themes() { let themes = get_all_themes(); - assert_that!(themes.get(&b"hicolor"[..])).is_some(); + assert_that!(themes.get("hicolor")).is_some(); } #[test]