diff --git a/Cargo.toml b/Cargo.toml index e14a113..298d939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,16 @@ readme = "README.md" keywords = ["icons", "gui", "freedesktop"] [dependencies] -dirs = "6.0" -thiserror = "2.0" -xdg = "3.0" -tracing = "0.1.41" -ini_core = "0.2.0" +bstr = "1.12.1" +btoi = "0.5.0" +memchr = "2.7.6" memmap2 = "0.9" +thiserror = "2.0" +tracing = { version = "0.1.41", default-features = false } +xdg = "3.0" [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 8fd6c32..cbfb0b1 100644 --- a/benches/simple_lookup.rs +++ b/benches/simple_lookup.rs @@ -1,9 +1,8 @@ +use cosmic_freedesktop_icons::lookup; use criterion::{ - AxisScale, BenchmarkId, Criterion, PlotConfiguration, black_box, criterion_group, - criterion_main, + AxisScale, BenchmarkId, Criterion, PlotConfiguration, criterion_group, criterion_main, }; -use freedesktop_icons::lookup; -use gtk4::{IconLookupFlags, IconTheme, TextDirection}; +use std::hint::black_box; pub fn bench_lookups(c: &mut Criterion) { let mut group = c.benchmark_group("ComparisonsLookups"); @@ -11,21 +10,13 @@ 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 - "archlinux-logo", // 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 + "com.valvesoftware.Steam", // 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, @@ -40,33 +31,6 @@ 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 deleted file mode 100644 index 9c31812..0000000 --- a/benches/tests.rs +++ /dev/null @@ -1,105 +0,0 @@ -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 f5d7d19..cadf666 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,15 +1,17 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use std::sync::LazyLock; -use std::sync::Mutex; +use std::sync::{LazyLock, RwLock}; use std::time::Instant; pub(crate) static CACHE: LazyLock = LazyLock::new(Cache::default); -type IconMap = BTreeMap<(String, u16, u16), CacheEntry>; -type ThemeMap = BTreeMap; +type Theme = Box; +type Icon = Box; +type SizedMap = BTreeMap<(u16, u16), CacheEntry>; +type IconMap = BTreeMap; +type ThemeMap = BTreeMap; #[derive(Default)] -pub(crate) struct Cache(Mutex); +pub(crate) struct Cache(RwLock); #[derive(Debug, Clone, PartialEq)] pub enum CacheEntry { @@ -23,7 +25,7 @@ pub enum CacheEntry { impl Cache { pub fn clear(&self) { - self.0.lock().unwrap().clear(); + self.0.write().unwrap().clear(); } pub fn insert>( @@ -34,41 +36,38 @@ impl Cache { icon_name: &str, icon_path: &Option

, ) { - let mut theme_map = self.0.lock().unwrap(); + let mut inner = self.0.write().unwrap(); let entry = icon_path .as_ref() .map(|path| CacheEntry::Found(path.as_ref().to_path_buf())) .unwrap_or(CacheEntry::NotFound(Instant::now())); - 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); - } - } + inner + .entry(theme.into()) + .or_insert_with(IconMap::default) + .entry(icon_name.into()) + .or_insert_with(BTreeMap::default) + .insert((size, scale), entry); } pub fn get(&self, theme: &str, size: u16, scale: u16, icon_name: &str) -> CacheEntry { - let theme_map = self.0.lock().unwrap(); + let inner = self.0.read().unwrap(); - theme_map + inner .get(theme) - .map(|icon_map| icon_map.get(&(icon_name.to_string(), size, scale))) - .and_then(|path| path.cloned()) + .and_then(|icon_map| icon_map.get(icon_name)) + .and_then(|icon_map| icon_map.get(&(size, scale)).cloned()) .unwrap_or(CacheEntry::Unknown) } pub fn reset_none(&self) { - 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; + 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; + } } } } diff --git a/src/lib.rs b/src/lib.rs index 2b642e1..5353ad9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,11 +51,14 @@ //! .find(); //! # } //! ``` +use memmap2::Mmap; use theme::BASE_PATHS; use crate::cache::{CACHE, CacheEntry}; -use crate::theme::{THEMES, try_build_icon_path}; +use crate::theme::{THEMES, Theme, try_build_icon_path}; +use std::hash::{Hash, Hasher}; use std::io::BufRead; +use std::ops::ControlFlow; use std::path::PathBuf; use std::time::Instant; @@ -82,8 +85,10 @@ pub fn list_themes() -> Vec { .flatten() .map(|path| &path.index) .filter_map(|index| { - let file = std::fs::File::open(index).ok()?; - let mut reader = std::io::BufReader::new(file); + 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 mut line = String::new(); while let Ok(read) = reader.read_line(&mut line) { @@ -105,56 +110,6 @@ 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, @@ -163,6 +118,7 @@ pub struct LookupBuilder<'a> { scale: u16, size: u16, theme: &'a str, + extra_paths: &'a [PathBuf], } /// Build an icon lookup for the given icon name. @@ -269,6 +225,14 @@ 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`. @@ -290,6 +254,7 @@ impl<'a> LookupBuilder<'a> { scale: 1, size: 24, theme: "hicolor", + extra_paths: &[], } } @@ -301,80 +266,91 @@ 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.duration_since(Instant::now()).as_secs() < 5 => - { + CacheEntry::NotFound(last_check) if last_check.elapsed().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) - .or_else(|| THEMES.get("hicolor")) + .get(self.theme.as_bytes()) + .or_else(|| THEMES.get("hicolor".as_bytes())) .and_then(|icon_themes| { let icon = icon_themes .iter() - .find_map(|theme| { - theme.try_get_icon(self.name, self.size, self.scale, self.force_svg) - }) + // Search the active icon themes + .find_map(|theme| self.search_theme(searched_themes, theme)) + // Search the inherits of those icon themes. .or_else(|| { - // 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(); - }; + icon_themes.iter().find_map(|t| { + self.search_theme_inherits(search_inherits, searched_themes, t) + }) + }) + // 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"] + }; - let Ok(file) = std::str::from_utf8(file.as_ref()) else { - return Vec::new(); - }; + let mut name_buf = String::new(); - 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) + 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(()) }) }) - }) - }) - .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 - } + .break_value() }); if self.cache { @@ -405,30 +381,190 @@ 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 simple_lookup() { + fn hicolor_firefox_24_png() { 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/22x22/apps/firefox.png", + "/usr/share/icons/hicolor/16x16/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(); @@ -441,6 +577,7 @@ mod test { } #[test] + #[cfg(feature = "local_tests")] fn should_fallback_to_parent_theme() { let icon = lookup("video-single-display-symbolic") .with_theme("Arc") @@ -455,6 +592,7 @@ mod test { } #[test] + #[cfg(feature = "local_tests")] fn should_fallback_to_pixmaps_utlimately() { let archlinux_logo = lookup("archlinux-logo") .with_size(16) @@ -468,26 +606,6 @@ 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(); @@ -496,8 +614,9 @@ mod test { let expected_cache_result = CACHE.get("hicolor", 24, 1, "not-found"); - asserting!("When lookup fails a first time, subsequent attempts should fail from cache") - .that(&expected_cache_result) - .is_equal_to(CacheEntry::NotFound); + assert!( + matches!(expected_cache_result, CacheEntry::NotFound(..)), + "When lookup fails a first time, subsequent attempts should fail from cache" + ); } } diff --git a/src/theme/directories.rs b/src/theme/directories.rs index c437d31..ca38f94 100644 --- a/src/theme/directories.rs +++ b/src/theme/directories.rs @@ -10,47 +10,31 @@ pub struct Directory<'a> { } impl Directory<'_> { - pub fn match_size(&self, size: u16, scale: u16) -> bool { - let scale = scale as i16; - let size = size as i16; - - 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; - + pub fn directory_size_distance(&self, size: i16, scale: i16) -> i16 { match self.type_ { - DirectoryType::Fixed => scaled_size - scaled_requested_size, + DirectoryType::Fixed => self.size * self.scale - size * scale, + 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 if scaled_requested_size < max_scaled_size { - scaled_requested_size - max_scaled_size } else { - 0 + let max_scaled_size = self.maxsize * self.scale; + 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 { - min_scaled_size - scaled_requested_size + self.minsize * self.scale - scaled_requested_size } else if scaled_requested_size > (self.size + self.threshold) * scale { - scaled_requested_size - max_scaled_size + scaled_requested_size - self.maxsize * self.scale } else { 0 } @@ -72,11 +56,11 @@ impl Default for DirectoryType { } } -impl From<&str> for DirectoryType { - fn from(value: &str) -> Self { - match value { - "Fixed" => DirectoryType::Fixed, - "Scalable" => DirectoryType::Scalable, +impl From<&[u8]> for DirectoryType { + fn from(value: &[u8]) -> Self { + match value[0] { + b'F' => DirectoryType::Fixed, + b'S' => DirectoryType::Scalable, _ => DirectoryType::Threshold, } } diff --git a/src/theme/error.rs b/src/theme/error.rs deleted file mode 100644 index 5ca693c..0000000 --- a/src/theme/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -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 5e0b1a8..a73bf3c 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,19 +1,19 @@ -use crate::theme::error::ThemeError; +use crate::theme::directories::DirectoryType; 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; -type Result = std::result::Result; - -pub static THEMES: LazyLock>> = LazyLock::new(get_all_themes); +pub static THEMES: LazyLock, Vec>> = LazyLock::new(get_all_themes); #[inline] pub fn read_ini_theme(path: &Path) -> std::io::Result { @@ -33,76 +33,99 @@ impl Theme { name: &str, size: u16, scale: u16, - force_svg: bool, + prefer_svg: bool, ) -> Option { let file = read_ini_theme(&self.index).ok()?; - 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)) + self.try_get_icon_closest_size(file.as_ref(), name, size, scale, prefer_svg) } #[inline] fn try_get_icon_closest_size( &self, - file: &str, + file: &[u8], name: &str, size: u16, scale: u16, - force_svg: bool, + prefer_svg: bool, ) -> Option { - self.closest_match_size(file, size, scale) - .iter() - .find_map(|path| try_build_icon_path(name, path, force_svg)) + self.try_fold_icon_path( + self.closest_match_size(file, size, scale, prefer_svg), + name, + prefer_svg, + ) } - fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec { - let dirs = self.get_all_directories(file); + 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"] + }; - 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(); + 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; - dirs.sort_by(|(_, a), (_, b)| a.cmp(b)); + for _ in 0..components { + path.pop(); + } - dirs.iter() - .map(|(dir, _)| dir) - .map(|dir| dir.name) - .map(|dir| self.path().join(dir)) - .collect() + 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 } fn path(&self) -> &PathBuf { @@ -110,45 +133,28 @@ impl Theme { } } -pub(super) fn try_build_icon_path>( +pub(super) fn try_build_icon_path<'a>( + path: &'a mut PathBuf, + name_buf: &'a mut String, name: &str, - 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())) - } + extension: &'static str, +) -> bool { + name_buf.push_str(name); + path.push(name); + try_build_ext(path, name_buf, name, extension) } -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 } +#[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() } // Iter through the base paths and get all theme directories -pub(super) fn get_all_themes() -> BTreeMap> { - let mut icon_themes = BTreeMap::<_, Vec<_>>::new(); +pub(super) fn get_all_themes() -> BTreeMap, Vec> { + let mut icon_themes = BTreeMap::, Vec<_>>::new(); let mut found_indices = BTreeMap::new(); let mut to_revisit = Vec::new(); @@ -168,8 +174,10 @@ pub(super) fn get_all_themes() -> BTreeMap> { if fallback_index.is_none() { found_indices.insert(name.clone(), theme.index.clone()); } - let name = name.to_string_lossy().to_string(); - icon_themes.entry(name).or_default().push(theme); + icon_themes + .entry(name.as_bytes().to_owned()) + .or_default() + .push(theme); } else if entry.path().is_dir() { to_revisit.push(entry); } @@ -180,8 +188,10 @@ pub(super) fn get_all_themes() -> BTreeMap> { let name = entry.file_name(); let fallback_index = found_indices.get(&name); if let Some(theme) = Theme::from_path(entry.path(), fallback_index) { - let name = name.to_string_lossy().to_string(); - icon_themes.entry(name).or_default().push(theme); + icon_themes + .entry(name.as_bytes().to_owned()) + .or_default() + .push(theme); } } @@ -190,24 +200,26 @@ pub(super) fn get_all_themes() -> BTreeMap> { impl Theme { pub(crate) fn from_path>(path: P, index: Option<&PathBuf>) -> Option { - let path = path.as_ref(); + 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 has_index = path.join("index.theme").exists() || index.is_some(); - - if !has_index || !path.is_dir() { + if !has_index || !is_dir { return None; } - 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, - } + index + .cloned() + .or_else(|| local_index_exists.then_some(path.clone())) + .map(|index| Theme { + path: ThemePath({ + path.pop(); + path + }), + index, + }) } } @@ -219,24 +231,22 @@ mod test { #[test] fn get_one_icon() { - let themes = THEMES.get("Adwaita").unwrap(); + let themes = THEMES.get(&b"Adwaita"[..]).unwrap(); println!( "{:?}", themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - t.try_get_icon_exact_size(file, "edit-delete-symbolic", 24, 1, false) + t.try_get_icon_closest_size(file.as_ref(), "edit-delete-symbolic", 24, 1, false) }) ); } #[test] fn should_get_png_first() { - let themes = THEMES.get("hicolor").unwrap(); + let themes = THEMES.get(&b"hicolor"[..]).unwrap(); let icon = themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - t.try_get_icon_exact_size(file, "blueman", 24, 1, true) + t.try_get_icon_closest_size(file.as_ref(), "blueman", 22, 1, false) }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( "/usr/share/icons/hicolor/22x22/apps/blueman.png", @@ -244,15 +254,38 @@ mod test { } #[test] - fn should_get_svg_first() { - let themes = THEMES.get("hicolor").unwrap(); + 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()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - t.try_get_icon_exact_size(file, "blueman", 24, 1, false) + 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/22x22/apps/blueman.png", + "/usr/share/icons/hicolor/96x96/apps/blueman.png", + )); + } + + #[test] + fn should_get_svg_first() { + 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", 24, 1, true) + }); + 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", )); } } diff --git a/src/theme/parse.rs b/src/theme/parse.rs index 64db96e..61cde2d 100644 --- a/src/theme/parse.rs +++ b/src/theme/parse.rs @@ -1,76 +1,48 @@ use crate::theme::Theme; use crate::theme::directories::{Directory, DirectoryType}; - -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, - }) -} +use bstr::BStr; impl Theme { pub(super) fn get_all_directories<'a>( &'a self, - file: &'a str, + file: &'a [u8], ) -> 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 name.is_empty() || name == "Icon Theme" { + if is_icon_theme { continue; } match key { - "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(), + 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(), _ => (), } } 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; @@ -81,7 +53,7 @@ impl Theme { } DirectorySection::EndSection => { - if name.is_empty() || name == "Icon Theme" { + if is_icon_theme { continue; } @@ -91,7 +63,6 @@ impl Theme { name, size, scale: scale.unwrap_or(1), - // context, type_: dtype, maxsize: max_size.unwrap_or(size), minsize: min_size.unwrap_or(size), @@ -105,29 +76,391 @@ impl Theme { }) } - pub fn inherits<'a>(&self, file: &'a str) -> Vec<&'a str> { + pub fn inherits<'a>(&self, file: &'a [u8]) -> impl Iterator { icon_theme_section(file) - .find(|&(key, _)| key == "Inherits") - .map(|(_, parents)| { - parents - .split(',') + .find(|&(key, _)| key == b"Inherits") + .into_iter() + .flat_map(|(_, parents)| { + BStr::new(parents) + .split(|&char| char == b',') // Filtering out 'hicolor' since we are going to fallback there anyway - .filter(|parent| parent != &"hicolor") - .collect() + .filter(|parent| parent != &b"hicolor") }) - .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 { - use crate::THEMES; - use speculoos::prelude::*; + 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); + } #[test] #[cfg(feature = "local_tests")] fn should_get_theme_parents() { - for theme in THEMES.get("Arc").unwrap() { + use speculoos::prelude::*; + for theme in crate::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 c4581e6..9ed7212 100644 --- a/src/theme/paths.rs +++ b/src/theme/paths.rs @@ -1,50 +1,38 @@ -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 mut data_dirs: Vec<_> = base_dirs + + let data_dirs = base_dirs .get_data_dirs() .into_iter() - .flat_map(|p| [p.join("icons"), p.join("pixmaps")]) - .collect(); + .flat_map(|p| [p.join("icons"), p.join("pixmaps")]); - 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 data_home_dirs = base_dirs + .get_data_home() + .into_iter() + .flat_map(|data_home| [data_home.join("icons"), data_home.join("pixmaps")].into_iter()); - 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() + 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() } #[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; @@ -54,7 +42,7 @@ mod test { #[test] fn should_get_all_themes() { let themes = get_all_themes(); - assert_that!(themes.get("hicolor")).is_some(); + assert_that!(themes.get(&b"hicolor"[..])).is_some(); } #[test]