perf: optimizations and additional test cases

This commit is contained in:
Michael Aaron Murphy 2025-11-25 06:24:34 +01:00
parent db4d26e591
commit 44edef9673
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
9 changed files with 225 additions and 226 deletions

View file

@ -1,9 +1,8 @@
use cosmic_freedesktop_icons::lookup;
use criterion::{ use criterion::{
AxisScale, BenchmarkId, Criterion, PlotConfiguration, black_box, criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration, criterion_group, criterion_main,
criterion_main,
}; };
use freedesktop_icons::lookup; use std::hint::black_box;
use gtk4::{IconLookupFlags, IconTheme, TextDirection};
pub fn bench_lookups(c: &mut Criterion) { pub fn bench_lookups(c: &mut Criterion) {
let mut group = c.benchmark_group("ComparisonsLookups"); let mut group = c.benchmark_group("ComparisonsLookups");
@ -13,19 +12,11 @@ pub fn bench_lookups(c: &mut Criterion) {
let args = [ let args = [
"user-home", // (Best case) An icon that can be found in the current theme "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 "firefox", // An icon that can be found in the hicolor default theme
"archlinux-logo", // An icon that resides in /usr/share/pixmaps "com.valvesoftware.Steam", // An icon that resides in /usr/share/pixmaps
"not-found", // (Worst case) An icon that does not exist "not-found", // (Worst case) An icon that does not exist
]; ];
for arg in args { 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( group.bench_with_input(
BenchmarkId::new("freedesktop-icons-cache", arg), BenchmarkId::new("freedesktop-icons-cache", arg),
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(); group.finish();

View file

@ -1,4 +1,4 @@
use freedesktop_icons::lookup; use cosmic_freedesktop_icons::lookup;
use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use gtk4::{IconLookupFlags, IconTheme, TextDirection};
use speculoos::prelude::*; use speculoos::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -1,15 +1,17 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::LazyLock; use std::sync::{LazyLock, RwLock};
use std::sync::Mutex;
use std::time::Instant; use std::time::Instant;
pub(crate) static CACHE: LazyLock<Cache> = LazyLock::new(Cache::default); pub(crate) static CACHE: LazyLock<Cache> = LazyLock::new(Cache::default);
type IconMap = BTreeMap<(String, u16, u16), CacheEntry>; type Theme = Box<str>;
type ThemeMap = BTreeMap<String, IconMap>; type Icon = Box<str>;
type SizedMap = BTreeMap<(u16, u16), CacheEntry>;
type IconMap = BTreeMap<Icon, SizedMap>;
type ThemeMap = BTreeMap<Theme, IconMap>;
#[derive(Default)] #[derive(Default)]
pub(crate) struct Cache(Mutex<ThemeMap>); pub(crate) struct Cache(RwLock<ThemeMap>);
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum CacheEntry { pub enum CacheEntry {
@ -23,7 +25,7 @@ pub enum CacheEntry {
impl Cache { impl Cache {
pub fn clear(&self) { pub fn clear(&self) {
self.0.lock().unwrap().clear(); self.0.write().unwrap().clear();
} }
pub fn insert<P: AsRef<Path>>( pub fn insert<P: AsRef<Path>>(
@ -34,43 +36,40 @@ impl Cache {
icon_name: &str, icon_name: &str,
icon_path: &Option<P>, icon_path: &Option<P>,
) { ) {
let mut theme_map = self.0.lock().unwrap(); let mut inner = self.0.write().unwrap();
let entry = icon_path let entry = icon_path
.as_ref() .as_ref()
.map(|path| CacheEntry::Found(path.as_ref().to_path_buf())) .map(|path| CacheEntry::Found(path.as_ref().to_path_buf()))
.unwrap_or(CacheEntry::NotFound(Instant::now())); .unwrap_or(CacheEntry::NotFound(Instant::now()));
match theme_map.get_mut(theme) { inner
Some(icon_map) => { .entry(theme.into())
icon_map.insert((icon_name.to_string(), size, scale), entry); .or_insert_with(IconMap::default)
} .entry(icon_name.into())
None => { .or_insert_with(BTreeMap::default)
let mut icon_map = BTreeMap::new(); .insert((size, scale), entry);
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 { 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) .get(theme)
.map(|icon_map| icon_map.get(&(icon_name.to_string(), size, scale))) .and_then(|icon_map| icon_map.get(icon_name))
.and_then(|path| path.cloned()) .and_then(|icon_map| icon_map.get(&(size, scale)).cloned())
.unwrap_or(CacheEntry::Unknown) .unwrap_or(CacheEntry::Unknown)
} }
pub fn reset_none(&self) { pub fn reset_none(&self) {
let mut theme_map = self.0.lock().unwrap(); let mut inner = self.0.write().unwrap();
for (_theme_name, theme) in inner.iter_mut() {
for (_theme_name, theme) in theme_map.iter_mut() { for (_, cached_icons) in theme.iter_mut() {
for (_icon_data, cached_icon) in theme.iter_mut() { for (_, cached_icon) in cached_icons.iter_mut() {
if matches!(cached_icon, CacheEntry::NotFound(_)) { if matches!(cached_icon, CacheEntry::NotFound(_)) {
*cached_icon = CacheEntry::Unknown; *cached_icon = CacheEntry::Unknown;
} }
} }
} }
} }
}
} }

View file

@ -51,11 +51,11 @@
//! .find(); //! .find();
//! # } //! # }
//! ``` //! ```
use memmap2::Mmap;
use theme::BASE_PATHS; use theme::BASE_PATHS;
use crate::cache::{CACHE, CacheEntry}; use crate::cache::{CACHE, CacheEntry};
use crate::theme::{THEMES, Theme, try_build_icon_path}; use crate::theme::{THEMES, Theme, try_build_icon_path};
use std::collections::HashSet;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::BufRead; use std::io::BufRead;
use std::path::PathBuf; use std::path::PathBuf;
@ -84,8 +84,10 @@ pub fn list_themes() -> Vec<String> {
.flatten() .flatten()
.map(|path| &path.index) .map(|path| &path.index)
.filter_map(|index| { .filter_map(|index| {
let file = std::fs::File::open(index).ok()?; let file = std::fs::File::open(index)
let mut reader = std::io::BufReader::new(file); .and_then(|file| unsafe { Mmap::map(&file) })
.ok()?;
let mut reader = std::io::Cursor::new(file.as_ref());
let mut line = String::new(); let mut line = String::new();
while let Ok(read) = reader.read_line(&mut line) { while let Ok(read) = reader.read_line(&mut line) {
@ -133,8 +135,10 @@ pub fn default_theme_gtk() -> Option<String> {
let name = name.trim().trim_matches('\''); let name = name.trim().trim_matches('\'');
THEMES.get(name).and_then(|themes| { THEMES.get(name).and_then(|themes| {
themes.first().and_then(|path| { themes.first().and_then(|path| {
let file = std::fs::File::open(&path.index).ok()?; let file = std::fs::File::open(&path.index)
let mut reader = std::io::BufReader::new(file); .and_then(|file| unsafe { Mmap::map(&file) })
.ok()?;
let mut reader = std::io::Cursor::new(file.as_ref());
let mut line = String::new(); let mut line = String::new();
while let Ok(read) = reader.read_line(&mut line) { while let Ok(read) = reader.read_line(&mut line) {
@ -313,9 +317,9 @@ impl<'a> LookupBuilder<'a> {
} }
// Records theme paths that have already been searched. // Records theme paths that have already been searched.
let searched_themes = &mut HashSet::new(); let searched_themes = &mut Vec::new();
// Record themes whose inherits have been searched. // Record themes whose inherits have been searched.
let search_inherits = &mut HashSet::new(); let search_inherits = &mut Vec::new();
// Then lookup in the given theme // Then lookup in the given theme
THEMES THEMES
@ -332,14 +336,19 @@ impl<'a> LookupBuilder<'a> {
self.search_theme_inherits(search_inherits, searched_themes, t) self.search_theme_inherits(search_inherits, searched_themes, t)
}) })
}) })
// Search the cosmic icon theme
.or_else(|| self.search_inherited_theme(searched_themes, "Cosmic"))
// Search the hicolor icon theme if it was not previously searched // Search the hicolor icon theme if it was not previously searched
.or_else(|| self.search_inherited_theme(searched_themes, "hicolor")) .or_else(|| self.search_inherited_theme(searched_themes, "hicolor"))
// GNOME applications may rely on the gnome theme
.or_else(|| self.search_inherited_theme(searched_themes, "gnome"))
// Ubuntu applications may require Yaru
.or_else(|| self.search_inherited_theme(searched_themes, "Yaru"))
.or_else(|| { .or_else(|| {
for theme_base_dir in BASE_PATHS.iter() { for theme_base_dir in BASE_PATHS.iter() {
if let Some(icon) = let mut path = theme_base_dir.clone();
try_build_icon_path(self.name, theme_base_dir, self.force_svg) if try_build_icon_path(&mut path, self.name, self.force_svg) {
{ return Some(path);
return Some(icon);
} }
} }
None None
@ -347,7 +356,9 @@ impl<'a> LookupBuilder<'a> {
.or_else(|| { .or_else(|| {
let p = PathBuf::from(&self.name); let p = PathBuf::from(&self.name);
if let (Some(name), Some(parent)) = (p.file_stem(), p.parent()) { if let (Some(name), Some(parent)) = (p.file_stem(), p.parent()) {
try_build_icon_path(&name.to_string_lossy(), parent, self.force_svg) let mut path = parent.to_path_buf();
try_build_icon_path(&mut path, &name.to_string_lossy(), self.force_svg)
.then_some(path)
} else { } else {
None None
} }
@ -383,7 +394,7 @@ impl<'a> LookupBuilder<'a> {
} }
/// Search a theme by its path for a matching icon if not already searched. /// Search a theme by its path for a matching icon if not already searched.
fn search_theme(&self, searched_themes: &mut HashSet<u64>, theme: &Theme) -> Option<PathBuf> { fn search_theme(&self, searched_themes: &mut Vec<u64>, theme: &Theme) -> Option<PathBuf> {
// Store hash of the theme. // Store hash of the theme.
let theme_hash = { let theme_hash = {
let mut hasher = std::hash::DefaultHasher::new(); let mut hasher = std::hash::DefaultHasher::new();
@ -391,7 +402,8 @@ impl<'a> LookupBuilder<'a> {
hasher.finish() hasher.finish()
}; };
if searched_themes.insert(theme_hash) { 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); return theme.try_get_icon(self.name, self.size, self.scale, self.force_svg);
} }
@ -401,8 +413,8 @@ impl<'a> LookupBuilder<'a> {
// Search the inherits of a theme if not already searched. // Search the inherits of a theme if not already searched.
fn search_theme_inherits( fn search_theme_inherits(
&self, &self,
search_inherits: &mut HashSet<u64>, search_inherits: &mut Vec<u64>,
searched_themes: &mut HashSet<u64>, searched_themes: &mut Vec<u64>,
theme: &Theme, theme: &Theme,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
// Store hash of the theme. // Store hash of the theme.
@ -412,7 +424,8 @@ impl<'a> LookupBuilder<'a> {
hasher.finish() hasher.finish()
}; };
if search_inherits.insert(theme_hash) { 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 { let Ok(file) = theme::read_ini_theme(&theme.index) else {
return None; return None;
}; };
@ -434,7 +447,7 @@ impl<'a> LookupBuilder<'a> {
/// Search the inherits of a theme by its name if not already searched. /// Search the inherits of a theme by its name if not already searched.
fn search_inherited_theme( fn search_inherited_theme(
&self, &self,
searched_themes: &mut HashSet<u64>, searched_themes: &mut Vec<u64>,
theme: &str, theme: &str,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
THEMES THEMES
@ -490,7 +503,7 @@ mod test {
#[test] #[test]
fn cosmic_weather_storm_symbolic() { fn cosmic_weather_storm_symbolic() {
let firefox = lookup("weather-storm-symbolic").with_theme("Cosmic").find(); let firefox = lookup("weather-storm-symbolic").find();
asserting!("Is the cosmic icon theme installed?") asserting!("Is the cosmic icon theme installed?")
.that(&firefox) .that(&firefox)
@ -502,10 +515,7 @@ mod test {
#[test] #[test]
fn cosmic_weather_storm_symbolic_force_svg() { fn cosmic_weather_storm_symbolic_force_svg() {
let firefox = lookup("weather-storm-symbolic") let firefox = lookup("weather-storm-symbolic").force_svg().find();
.with_theme("Cosmic")
.force_svg()
.find();
asserting!("Is the cosmic icon theme installed?") asserting!("Is the cosmic icon theme installed?")
.that(&firefox) .that(&firefox)
@ -528,6 +538,46 @@ mod test {
); );
} }
#[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] #[test]
#[cfg(feature = "local_tests")] #[cfg(feature = "local_tests")]
fn theme_lookup() { fn theme_lookup() {

View file

@ -28,29 +28,29 @@ impl Directory<'_> {
} }
pub fn directory_size_distance(&self, size: u16, scale: u16) -> i16 { 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 scale = scale as i16;
let size = size as i16; let scaled_requested_size = size as i16 * scale;
let scaled_requested_size = size * scale;
match self.type_ { match self.type_ {
DirectoryType::Fixed => scaled_size - scaled_requested_size, DirectoryType::Fixed => self.size * self.scale - scaled_requested_size,
DirectoryType::Scalable => { DirectoryType::Scalable => {
let min_scaled_size = self.minsize * self.scale;
if scaled_requested_size < min_scaled_size { if scaled_requested_size < min_scaled_size {
min_scaled_size - scaled_requested_size min_scaled_size - scaled_requested_size
} else if scaled_requested_size < max_scaled_size { } else {
let max_scaled_size = self.maxsize * self.scale;
if scaled_requested_size < max_scaled_size {
scaled_requested_size - max_scaled_size scaled_requested_size - max_scaled_size
} else { } else {
0 0
} }
} }
}
DirectoryType::Threshold => { DirectoryType::Threshold => {
if scaled_requested_size < (self.size - self.threshold) * 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 { } else if scaled_requested_size > (self.size + self.threshold) * scale {
scaled_requested_size - max_scaled_size scaled_requested_size - self.maxsize * self.scale
} else { } else {
0 0
} }
@ -74,9 +74,9 @@ impl Default for DirectoryType {
impl From<&str> for DirectoryType { impl From<&str> for DirectoryType {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
match value { match value.as_bytes()[0] {
"Fixed" => DirectoryType::Fixed, b'F' => DirectoryType::Fixed,
"Scalable" => DirectoryType::Scalable, b'S' => DirectoryType::Scalable,
_ => DirectoryType::Threshold, _ => DirectoryType::Threshold,
} }
} }

View file

@ -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),
}

View file

@ -1,18 +1,15 @@
use crate::theme::error::ThemeError;
use crate::theme::paths::ThemePath; use crate::theme::paths::ThemePath;
use memmap2::Mmap; use memmap2::Mmap;
pub(crate) use paths::BASE_PATHS; pub(crate) use paths::BASE_PATHS;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::ControlFlow;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::LazyLock; use std::sync::LazyLock;
mod directories; mod directories;
pub mod error;
mod parse; mod parse;
mod paths; mod paths;
type Result<T> = std::result::Result<T, ThemeError>;
pub static THEMES: LazyLock<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes); pub static THEMES: LazyLock<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes);
#[inline] #[inline]
@ -50,8 +47,7 @@ impl Theme {
scale: u16, scale: u16,
force_svg: bool, force_svg: bool,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
self.match_size(file, size, scale) self.try_fold_icon_path(self.match_size(file, size, scale), name, force_svg)
.find_map(|path| try_build_icon_path(name, path, force_svg))
} }
#[inline] #[inline]
@ -60,12 +56,10 @@ impl Theme {
file: &'a str, file: &'a str,
size: u16, size: u16,
scale: u16, scale: u16,
) -> impl Iterator<Item = PathBuf> + 'a { ) -> impl Iterator<Item = &'a str> + 'a {
let dirs = self.get_all_directories(file); self.get_all_directories(file)
.filter(move |directory| directory.match_size(size, scale))
dirs.filter(move |directory| directory.match_size(size, scale))
.map(|dir| dir.name) .map(|dir| dir.name)
.map(|dir| self.path().join(dir))
} }
#[inline] #[inline]
@ -77,32 +71,59 @@ impl Theme {
scale: u16, scale: u16,
force_svg: bool, force_svg: bool,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
self.closest_match_size(file, size, scale) self.try_fold_icon_path(self.closest_match_size(file, size, scale), name, force_svg)
.iter()
.find_map(|path| try_build_icon_path(name, path, force_svg))
} }
fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec<PathBuf> { fn try_fold_icon_path<'a>(
let dirs = self.get_all_directories(file); &self,
mut dir_names: impl Iterator<Item = &'a str>,
let mut dirs: Vec<_> = dirs name: &str,
.filter_map(|directory| { force_svg: bool,
let distance = directory.directory_size_distance(size, scale); ) -> Option<PathBuf> {
if distance < i16::MAX { dir_names
Some((directory, distance.abs())) .try_fold(self.path().clone(), move |mut path, dir_name| {
path.push(dir_name);
if try_build_icon_path(&mut path, name, force_svg) {
ControlFlow::Break(path)
} else { } else {
None let components = dir_name
.as_bytes()
.iter()
.fold(2, |n, c| n + (*c == b'/') as u32)
as usize;
for _ in 0..components {
path.pop();
}
ControlFlow::Continue(path)
} }
}) })
.collect(); .break_value()
}
dirs.sort_by(|(_, a), (_, b)| a.cmp(b)); fn closest_match_size<'a>(
&'a self,
file: &'a str,
size: u16,
scale: u16,
) -> impl Iterator<Item = &'a str> + 'a {
let dirs = self.get_all_directories(file);
dirs.iter() dirs.fold(Vec::<(&'a str, i16)>::new(), |mut sorted, directory| {
.map(|(dir, _)| dir) let distance = directory.directory_size_distance(size, scale);
.map(|dir| dir.name) if distance < i16::MAX {
.map(|dir| self.path().join(dir)) let a = distance.abs();
.collect() let pos = sorted
.binary_search_by(|(_, b)| b.cmp(&a))
.unwrap_or_else(|pos| pos);
sorted.insert(pos, (directory.name, a));
}
sorted
})
.into_iter()
.map(|(name, _)| name)
} }
fn path(&self) -> &PathBuf { fn path(&self) -> &PathBuf {
@ -110,40 +131,27 @@ impl Theme {
} }
} }
pub(super) fn try_build_icon_path<P: AsRef<Path>>( pub(super) fn try_build_icon_path<'a>(path: &'a mut PathBuf, name: &str, force_svg: bool) -> bool {
name: &str, let mut name_buf = String::with_capacity(name.len() + 4);
path: P, name_buf.push_str(name);
force_svg: bool, path.push(name);
) -> Option<PathBuf> {
if force_svg { if force_svg {
try_build_svg(name, path.as_ref()) try_build_ext(path, &mut name_buf, name, ".svg")
.or_else(|| try_build_png(name, path.as_ref())) || try_build_ext(path, &mut name_buf, name, ".png")
.or_else(|| try_build_xmp(name, path.as_ref())) || try_build_ext(path, &mut name_buf, name, ".xmp")
} else { } else {
try_build_png(name, path.as_ref()) try_build_ext(path, &mut name_buf, name, ".png")
.or_else(|| try_build_svg(name, path.as_ref())) || try_build_ext(path, &mut name_buf, name, ".svg")
.or_else(|| try_build_xmp(name, path.as_ref())) || try_build_ext(path, &mut name_buf, name, ".xmp")
} }
} }
fn try_build_svg<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> { #[inline]
let path = path.as_ref(); fn try_build_ext(path: &mut PathBuf, name_buf: &mut String, name: &str, ext: &'static str) -> bool {
let svg = path.join(format!("{name}.svg")); name_buf.truncate(name.len());
name_buf.push_str(ext);
if svg.exists() { Some(svg) } else { None } path.set_file_name(&name_buf);
} path.exists()
fn try_build_png<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
let path = path.as_ref();
let png = path.join(format!("{name}.png"));
if png.exists() { Some(png) } else { None }
}
fn try_build_xmp<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
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 // Iter through the base paths and get all theme directories
@ -180,8 +188,9 @@ pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
let name = entry.file_name(); let name = entry.file_name();
let fallback_index = found_indices.get(&name); let fallback_index = found_indices.get(&name);
if let Some(theme) = Theme::from_path(entry.path(), fallback_index) { if let Some(theme) = Theme::from_path(entry.path(), fallback_index) {
let name = name.to_string_lossy().to_string(); if let Some(name) = name.to_str() {
icon_themes.entry(name).or_default().push(theme); icon_themes.entry(name.to_owned()).or_default().push(theme);
}
} }
} }
@ -190,24 +199,26 @@ pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
impl Theme { impl Theme {
pub(crate) fn from_path<P: AsRef<Path>>(path: P, index: Option<&PathBuf>) -> Option<Self> { pub(crate) fn from_path<P: AsRef<Path>>(path: P, index: Option<&PathBuf>) -> Option<Self> {
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 || !is_dir {
if !has_index || !path.is_dir() {
return None; return None;
} }
let path = ThemePath(path.into()); index
.cloned()
match (index, path.index()) { .or_else(|| local_index_exists.then_some(path.clone()))
(Some(index), _) => Some(Theme { .map(|index| Theme {
path, path: ThemePath({
index: index.clone(), path.pop();
path
}), }),
(None, Ok(index)) => Some(Theme { path, index }), index,
_ => None, })
}
} }
} }

View file

@ -105,27 +105,26 @@ impl Theme {
}) })
} }
pub fn inherits<'a>(&self, file: &'a str) -> Vec<&'a str> { pub fn inherits<'a>(&self, file: &'a str) -> impl Iterator<Item = &'a str> {
icon_theme_section(file) icon_theme_section(file)
.find(|&(key, _)| key == "Inherits") .find(|&(key, _)| key == "Inherits")
.map(|(_, parents)| { .into_iter()
.flat_map(|(_, parents)| {
parents parents
.split(',') .split(',')
// Filtering out 'hicolor' since we are going to fallback there anyway // Filtering out 'hicolor' since we are going to fallback there anyway
.filter(|parent| parent != &"hicolor") .filter(|parent| parent != &"hicolor")
.collect()
}) })
.unwrap_or_default()
} }
} }
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "local_tests")]
mod test { mod test {
use crate::THEMES; use crate::THEMES;
use speculoos::prelude::*; use speculoos::prelude::*;
#[test] #[test]
#[cfg(feature = "local_tests")]
fn should_get_theme_parents() { fn should_get_theme_parents() {
for theme in THEMES.get("Arc").unwrap() { for theme in THEMES.get("Arc").unwrap() {
let file = crate::theme::read_ini_theme(&theme.index).ok().unwrap(); let file = crate::theme::read_ini_theme(&theme.index).ok().unwrap();

View file

@ -3,48 +3,35 @@ use std::path::PathBuf;
use std::sync::LazyLock; use std::sync::LazyLock;
use xdg::BaseDirectories; use xdg::BaseDirectories;
use crate::theme;
use crate::theme::error::ThemeError;
pub(crate) static BASE_PATHS: LazyLock<Vec<PathBuf>> = LazyLock::new(icon_theme_base_paths); pub(crate) static BASE_PATHS: LazyLock<Vec<PathBuf>> = 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). /// 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. /// Paths that are not found are filtered out.
fn icon_theme_base_paths() -> Vec<PathBuf> { fn icon_theme_base_paths() -> Vec<PathBuf> {
let base_dirs = BaseDirectories::new(); let base_dirs = BaseDirectories::new();
let mut data_dirs: Vec<_> = base_dirs
let data_dirs = base_dirs
.get_data_dirs() .get_data_dirs()
.into_iter() .into_iter()
.flat_map(|p| [p.join("icons"), p.join("pixmaps")]) .flat_map(|p| [p.join("icons"), p.join("pixmaps")]);
.collect();
if let Some(data_home) = base_dirs.get_data_home() { let data_home_dirs = base_dirs
data_dirs.push(data_home.join("icons")); .get_data_home()
data_dirs.push(data_home.join("pixmaps")); .into_iter()
} .flat_map(|data_home| [data_home.join("icons"), data_home.join("pixmaps")].into_iter());
match home_dir().map(|home| home.join(".icons")) { let home_dir = home_dir().into_iter().map(|home| home.join(".icons"));
Some(home_icon_dir) => data_dirs.push(home_icon_dir),
None => tracing::warn!("No $HOME directory found"), data_dirs
} .chain(data_home_dirs)
data_dirs.into_iter().filter(|p| p.exists()).collect() .chain(home_dir)
.filter(|p| p.exists())
.collect()
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ThemePath(pub PathBuf); pub struct ThemePath(pub PathBuf);
impl ThemePath {
pub(super) fn index(&self) -> theme::Result<PathBuf> {
let index = self.0.join("index.theme");
if !index.exists() {
return Err(ThemeError::ThemeIndexNotFound(index));
}
Ok(index)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::theme::paths::icon_theme_base_paths; use crate::theme::paths::icon_theme_base_paths;