Compare commits
10 commits
062ec71b1a
...
739e266210
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
739e266210 | ||
|
|
9c562fe3ec | ||
|
|
7a61a704f6 | ||
|
|
7bb19f1b50 | ||
|
|
702ea3ed5c | ||
|
|
54520b0a55 | ||
|
|
8b045f90c7 | ||
|
|
44edef9673 | ||
|
|
db4d26e591 | ||
|
|
7173f91ccb |
10 changed files with 874 additions and 571 deletions
13
Cargo.toml
13
Cargo.toml
|
|
@ -10,17 +10,16 @@ readme = "README.md"
|
||||||
keywords = ["icons", "gui", "freedesktop"]
|
keywords = ["icons", "gui", "freedesktop"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dirs = "6.0"
|
bstr = "1.12.1"
|
||||||
thiserror = "2.0"
|
btoi = "0.5.0"
|
||||||
xdg = "3.0"
|
memchr = "2.7.6"
|
||||||
tracing = "0.1.41"
|
|
||||||
ini_core = "0.2.0"
|
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
|
thiserror = "2.0"
|
||||||
|
tracing = { version = "0.1.41", default-features = false }
|
||||||
|
xdg = "3.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
speculoos = "0.13.0"
|
speculoos = "0.13.0"
|
||||||
linicon = "2.3.0"
|
|
||||||
gtk4 = "0.10"
|
|
||||||
criterion = "0.7"
|
criterion = "0.7"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
105
benches/tests.rs
105
benches/tests.rs
|
|
@ -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"));
|
|
||||||
}
|
|
||||||
49
src/cache.rs
49
src/cache.rs
|
|
@ -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,39 +36,35 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -74,3 +72,4 @@ impl Cache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
391
src/lib.rs
391
src/lib.rs
|
|
@ -51,11 +51,14 @@
|
||||||
//! .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, try_build_icon_path};
|
use crate::theme::{THEMES, Theme, try_build_icon_path};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
|
@ -82,8 +85,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) {
|
||||||
|
|
@ -105,56 +110,6 @@ pub fn list_themes() -> Vec<String> {
|
||||||
themes
|
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<String> {
|
|
||||||
// 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.
|
/// The lookup builder struct, holding all the lookup query parameters.
|
||||||
pub struct LookupBuilder<'a> {
|
pub struct LookupBuilder<'a> {
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
|
|
@ -163,6 +118,7 @@ pub struct LookupBuilder<'a> {
|
||||||
scale: u16,
|
scale: u16,
|
||||||
size: u16,
|
size: u16,
|
||||||
theme: &'a str,
|
theme: &'a str,
|
||||||
|
extra_paths: &'a [PathBuf],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an icon lookup for the given icon name.
|
/// Build an icon lookup for the given icon name.
|
||||||
|
|
@ -269,6 +225,14 @@ impl<'a> LookupBuilder<'a> {
|
||||||
self
|
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
|
/// Execute the current lookup
|
||||||
/// if no icon is found in the current theme fallback to
|
/// if no icon is found in the current theme fallback to
|
||||||
/// `/usr/share/icons/hicolor` theme and then to `/usr/share/pixmaps`.
|
/// `/usr/share/icons/hicolor` theme and then to `/usr/share/pixmaps`.
|
||||||
|
|
@ -290,6 +254,7 @@ impl<'a> LookupBuilder<'a> {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
size: 24,
|
size: 24,
|
||||||
theme: "hicolor",
|
theme: "hicolor",
|
||||||
|
extra_paths: &[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,80 +266,91 @@ impl<'a> LookupBuilder<'a> {
|
||||||
if self.cache {
|
if self.cache {
|
||||||
match self.cache_lookup(self.theme) {
|
match self.cache_lookup(self.theme) {
|
||||||
CacheEntry::Found(icon) => return Some(icon),
|
CacheEntry::Found(icon) => return Some(icon),
|
||||||
CacheEntry::NotFound(last_check)
|
CacheEntry::NotFound(last_check) if last_check.elapsed().as_secs() < 5 => {
|
||||||
if last_check.duration_since(Instant::now()).as_secs() < 5 =>
|
|
||||||
{
|
|
||||||
return None;
|
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
|
// Then lookup in the given theme
|
||||||
THEMES
|
THEMES
|
||||||
.get(self.theme)
|
.get(self.theme.as_bytes())
|
||||||
.or_else(|| THEMES.get("hicolor"))
|
.or_else(|| THEMES.get("hicolor".as_bytes()))
|
||||||
.and_then(|icon_themes| {
|
.and_then(|icon_themes| {
|
||||||
let icon = icon_themes
|
let icon = icon_themes
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|theme| {
|
// Search the active icon themes
|
||||||
theme.try_get_icon(self.name, self.size, self.scale, self.force_svg)
|
.find_map(|theme| self.search_theme(searched_themes, theme))
|
||||||
})
|
// Search the inherits of those icon themes.
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// Fallback to the parent themes recursively
|
icon_themes.iter().find_map(|t| {
|
||||||
let mut parents = icon_themes
|
self.search_theme_inherits(search_inherits, searched_themes, t)
|
||||||
.iter()
|
|
||||||
.flat_map(|t| {
|
|
||||||
let Ok(file) = theme::read_ini_theme(&t.index) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(file) = std::str::from_utf8(file.as_ref()) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
t.inherits(file)
|
|
||||||
.into_iter()
|
|
||||||
.map(String::from)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
// 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(|| {
|
.or_else(|| {
|
||||||
THEMES.get("hicolor").and_then(|icon_themes| {
|
let extensions = if self.force_svg {
|
||||||
icon_themes.iter().find_map(|theme| {
|
[".svg", ".png", ".xpm"]
|
||||||
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 {
|
} else {
|
||||||
None
|
[".png", ".svg", ".xpm"]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut name_buf = String::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(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.break_value()
|
||||||
});
|
});
|
||||||
|
|
||||||
if self.cache {
|
if self.cache {
|
||||||
|
|
@ -405,30 +381,190 @@ impl<'a> LookupBuilder<'a> {
|
||||||
CACHE.insert(theme, self.size, self.scale, self.name, &icon);
|
CACHE.insert(theme, self.size, self.scale, self.name, &icon);
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search a theme by its path for a matching icon if not already searched.
|
||||||
|
fn search_theme(&self, searched_themes: &mut Vec<u64>, theme: &Theme) -> Option<PathBuf> {
|
||||||
|
// 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<u64>,
|
||||||
|
searched_themes: &mut Vec<u64>,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
// 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<u64>,
|
||||||
|
theme: &[u8],
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
THEMES
|
||||||
|
.get(theme)?
|
||||||
|
.iter()
|
||||||
|
.find_map(|t| self.search_theme(searched_themes, t))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WARNING: these test are highly dependent on your installed icon-themes.
|
// 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.
|
// If you want to run them, make sure you have 'Papirus' and 'Arc' icon-themes installed.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "local_tests")]
|
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{CACHE, CacheEntry, lookup};
|
use crate::{CACHE, CacheEntry, lookup};
|
||||||
use speculoos::prelude::*;
|
use speculoos::prelude::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_lookup() {
|
fn hicolor_firefox_24_png() {
|
||||||
let firefox = lookup("firefox").find();
|
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")
|
asserting!("Lookup with no parameters should return an existing icon")
|
||||||
.that(&firefox)
|
.that(&firefox)
|
||||||
.is_some()
|
.is_some()
|
||||||
.is_equal_to(PathBuf::from(
|
.is_equal_to(PathBuf::from(
|
||||||
"/usr/share/icons/hicolor/22x22/apps/firefox.png",
|
"/usr/share/icons/hicolor/16x16/apps/firefox.png",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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() {
|
fn theme_lookup() {
|
||||||
let firefox = lookup("firefox").with_theme("Papirus").find();
|
let firefox = lookup("firefox").with_theme("Papirus").find();
|
||||||
|
|
||||||
|
|
@ -441,6 +577,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(feature = "local_tests")]
|
||||||
fn should_fallback_to_parent_theme() {
|
fn should_fallback_to_parent_theme() {
|
||||||
let icon = lookup("video-single-display-symbolic")
|
let icon = lookup("video-single-display-symbolic")
|
||||||
.with_theme("Arc")
|
.with_theme("Arc")
|
||||||
|
|
@ -455,6 +592,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(feature = "local_tests")]
|
||||||
fn should_fallback_to_pixmaps_utlimately() {
|
fn should_fallback_to_pixmaps_utlimately() {
|
||||||
let archlinux_logo = lookup("archlinux-logo")
|
let archlinux_logo = lookup("archlinux-logo")
|
||||||
.with_size(16)
|
.with_size(16)
|
||||||
|
|
@ -468,26 +606,6 @@ mod test {
|
||||||
.is_equal_to(PathBuf::from("/usr/share/pixmaps/archlinux-logo.png"));
|
.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]
|
#[test]
|
||||||
fn should_not_attempt_to_lookup_a_not_found_cached_icon() {
|
fn should_not_attempt_to_lookup_a_not_found_cached_icon() {
|
||||||
let not_found = lookup("not-found").with_cache().find();
|
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");
|
let expected_cache_result = CACHE.get("hicolor", 24, 1, "not-found");
|
||||||
|
|
||||||
asserting!("When lookup fails a first time, subsequent attempts should fail from cache")
|
assert!(
|
||||||
.that(&expected_cache_result)
|
matches!(expected_cache_result, CacheEntry::NotFound(..)),
|
||||||
.is_equal_to(CacheEntry::NotFound);
|
"When lookup fails a first time, subsequent attempts should fail from cache"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,47 +10,31 @@ pub struct Directory<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Directory<'_> {
|
impl Directory<'_> {
|
||||||
pub fn match_size(&self, size: u16, scale: u16) -> bool {
|
pub fn directory_size_distance(&self, size: i16, scale: i16) -> i16 {
|
||||||
let scale = scale as i16;
|
|
||||||
let size = size as i16;
|
|
||||||
|
|
||||||
if self.scale != scale {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
match self.type_ {
|
match self.type_ {
|
||||||
DirectoryType::Fixed => self.size == size,
|
DirectoryType::Fixed => self.size * self.scale - size * scale,
|
||||||
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 => {
|
DirectoryType::Scalable => {
|
||||||
|
let scaled_requested_size = size * scale;
|
||||||
|
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 => {
|
||||||
|
let scaled_requested_size = size * scale;
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
@ -72,11 +56,11 @@ impl Default for DirectoryType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for DirectoryType {
|
impl From<&[u8]> for DirectoryType {
|
||||||
fn from(value: &str) -> Self {
|
fn from(value: &[u8]) -> Self {
|
||||||
match value {
|
match value[0] {
|
||||||
"Fixed" => DirectoryType::Fixed,
|
b'F' => DirectoryType::Fixed,
|
||||||
"Scalable" => DirectoryType::Scalable,
|
b'S' => DirectoryType::Scalable,
|
||||||
_ => DirectoryType::Threshold,
|
_ => DirectoryType::Threshold,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
}
|
|
||||||
269
src/theme/mod.rs
269
src/theme/mod.rs
|
|
@ -1,19 +1,19 @@
|
||||||
use crate::theme::error::ThemeError;
|
use crate::theme::directories::DirectoryType;
|
||||||
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::cmp::Ordering;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
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<Vec<u8>, Vec<Theme>>> = LazyLock::new(get_all_themes);
|
||||||
|
|
||||||
pub static THEMES: LazyLock<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes);
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn read_ini_theme(path: &Path) -> std::io::Result<Mmap> {
|
pub fn read_ini_theme(path: &Path) -> std::io::Result<Mmap> {
|
||||||
|
|
@ -33,76 +33,99 @@ impl Theme {
|
||||||
name: &str,
|
name: &str,
|
||||||
size: u16,
|
size: u16,
|
||||||
scale: u16,
|
scale: u16,
|
||||||
force_svg: bool,
|
prefer_svg: bool,
|
||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
let file = read_ini_theme(&self.index).ok()?;
|
let file = read_ini_theme(&self.index).ok()?;
|
||||||
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
self.try_get_icon_closest_size(file.as_ref(), name, size, scale, prefer_svg)
|
||||||
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<PathBuf> {
|
|
||||||
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<Item = PathBuf> + '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]
|
#[inline]
|
||||||
fn try_get_icon_closest_size(
|
fn try_get_icon_closest_size(
|
||||||
&self,
|
&self,
|
||||||
file: &str,
|
file: &[u8],
|
||||||
name: &str,
|
name: &str,
|
||||||
size: u16,
|
size: u16,
|
||||||
scale: u16,
|
scale: u16,
|
||||||
force_svg: bool,
|
prefer_svg: bool,
|
||||||
) -> Option<PathBuf> {
|
) -> Option<PathBuf> {
|
||||||
self.closest_match_size(file, size, scale)
|
self.try_fold_icon_path(
|
||||||
.iter()
|
self.closest_match_size(file, size, scale, prefer_svg),
|
||||||
.find_map(|path| try_build_icon_path(name, path, force_svg))
|
name,
|
||||||
|
prefer_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,
|
||||||
|
dir_names: Vec<(&'a str, i16, bool)>,
|
||||||
let mut dirs: Vec<_> = dirs
|
name: &str,
|
||||||
.filter_map(|directory| {
|
prefer_svg: bool,
|
||||||
let distance = directory.directory_size_distance(size, scale);
|
) -> Option<PathBuf> {
|
||||||
if distance < i16::MAX {
|
let extensions = if prefer_svg {
|
||||||
Some((directory, distance.abs()))
|
[".svg", ".png", ".xpm"]
|
||||||
} else {
|
} else {
|
||||||
None
|
[".png", ".svg", ".xpm"]
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
for _ in 0..components {
|
||||||
|
path.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ControlFlow::Continue((path, name_buf))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.break_value()
|
||||||
})
|
})
|
||||||
.collect();
|
}
|
||||||
|
|
||||||
dirs.sort_by(|(_, a), (_, b)| a.cmp(b));
|
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
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
dirs.iter()
|
unsorted.sort_by(|a, b| {
|
||||||
.map(|(dir, _)| dir)
|
let ordering = if prefer_svg {
|
||||||
.map(|dir| dir.name)
|
b.2.cmp(&a.2)
|
||||||
.map(|dir| self.path().join(dir))
|
} else {
|
||||||
.collect()
|
a.2.cmp(&b.2)
|
||||||
|
};
|
||||||
|
match ordering {
|
||||||
|
Ordering::Equal => a.1.cmp(&b.1),
|
||||||
|
_ => ordering,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
unsorted
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self) -> &PathBuf {
|
fn path(&self) -> &PathBuf {
|
||||||
|
|
@ -110,45 +133,28 @@ 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_buf: &'a mut String,
|
||||||
name: &str,
|
name: &str,
|
||||||
path: P,
|
extension: &'static str,
|
||||||
force_svg: bool,
|
) -> bool {
|
||||||
) -> Option<PathBuf> {
|
name_buf.push_str(name);
|
||||||
if force_svg {
|
path.push(name);
|
||||||
try_build_svg(name, path.as_ref())
|
try_build_ext(path, name_buf, name, extension)
|
||||||
.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()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
|
pub(super) fn get_all_themes() -> BTreeMap<Vec<u8>, Vec<Theme>> {
|
||||||
let mut icon_themes = BTreeMap::<_, Vec<_>>::new();
|
let mut icon_themes = BTreeMap::<Vec<u8>, Vec<_>>::new();
|
||||||
let mut found_indices = BTreeMap::new();
|
let mut found_indices = BTreeMap::new();
|
||||||
let mut to_revisit = Vec::new();
|
let mut to_revisit = Vec::new();
|
||||||
|
|
||||||
|
|
@ -168,8 +174,10 @@ pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
|
||||||
if fallback_index.is_none() {
|
if fallback_index.is_none() {
|
||||||
found_indices.insert(name.clone(), theme.index.clone());
|
found_indices.insert(name.clone(), theme.index.clone());
|
||||||
}
|
}
|
||||||
let name = name.to_string_lossy().to_string();
|
icon_themes
|
||||||
icon_themes.entry(name).or_default().push(theme);
|
.entry(name.as_bytes().to_owned())
|
||||||
|
.or_default()
|
||||||
|
.push(theme);
|
||||||
} else if entry.path().is_dir() {
|
} else if entry.path().is_dir() {
|
||||||
to_revisit.push(entry);
|
to_revisit.push(entry);
|
||||||
}
|
}
|
||||||
|
|
@ -180,8 +188,10 @@ 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();
|
icon_themes
|
||||||
icon_themes.entry(name).or_default().push(theme);
|
.entry(name.as_bytes().to_owned())
|
||||||
|
.or_default()
|
||||||
|
.push(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,24 +200,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,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,24 +231,22 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_one_icon() {
|
fn get_one_icon() {
|
||||||
let themes = THEMES.get("Adwaita").unwrap();
|
let themes = THEMES.get(&b"Adwaita"[..]).unwrap();
|
||||||
println!(
|
println!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
themes.iter().find_map(|t| {
|
themes.iter().find_map(|t| {
|
||||||
let file = super::read_ini_theme(&t.index).ok()?;
|
let file = super::read_ini_theme(&t.index).ok()?;
|
||||||
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
t.try_get_icon_closest_size(file.as_ref(), "edit-delete-symbolic", 24, 1, false)
|
||||||
t.try_get_icon_exact_size(file, "edit-delete-symbolic", 24, 1, false)
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_get_png_first() {
|
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 icon = themes.iter().find_map(|t| {
|
||||||
let file = super::read_ini_theme(&t.index).ok()?;
|
let file = super::read_ini_theme(&t.index).ok()?;
|
||||||
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
t.try_get_icon_closest_size(file.as_ref(), "blueman", 22, 1, false)
|
||||||
t.try_get_icon_exact_size(file, "blueman", 24, 1, true)
|
|
||||||
});
|
});
|
||||||
assert_that!(icon).is_some().is_equal_to(PathBuf::from(
|
assert_that!(icon).is_some().is_equal_to(PathBuf::from(
|
||||||
"/usr/share/icons/hicolor/22x22/apps/blueman.png",
|
"/usr/share/icons/hicolor/22x22/apps/blueman.png",
|
||||||
|
|
@ -244,15 +254,38 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_get_svg_first() {
|
fn should_get_png_first_92() {
|
||||||
let themes = THEMES.get("hicolor").unwrap();
|
let themes = THEMES.get(&b"hicolor"[..]).unwrap();
|
||||||
let icon = themes.iter().find_map(|t| {
|
let icon = themes.iter().find_map(|t| {
|
||||||
let file = super::read_ini_theme(&t.index).ok()?;
|
let file = super::read_ini_theme(&t.index).ok()?;
|
||||||
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
t.try_get_icon_closest_size(file.as_ref(), "blueman", 92, 1, false)
|
||||||
t.try_get_icon_exact_size(file, "blueman", 24, 1, false)
|
|
||||||
});
|
});
|
||||||
assert_that!(icon).is_some().is_equal_to(PathBuf::from(
|
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",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,48 @@
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
use crate::theme::directories::{Directory, DirectoryType};
|
use crate::theme::directories::{Directory, DirectoryType};
|
||||||
|
use bstr::BStr;
|
||||||
fn icon_theme_section(file: &str) -> impl Iterator<Item = (&str, &str)> + '_ {
|
|
||||||
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<Item = DirectorySection<'_>> {
|
|
||||||
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 {
|
impl Theme {
|
||||||
pub(super) fn get_all_directories<'a>(
|
pub(super) fn get_all_directories<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
file: &'a str,
|
file: &'a [u8],
|
||||||
) -> impl Iterator<Item = Directory<'a>> + 'a {
|
) -> impl Iterator<Item = Directory<'a>> + 'a {
|
||||||
let mut iterator = sections(file);
|
let mut iterator = sections(file);
|
||||||
|
|
||||||
std::iter::from_fn(move || {
|
std::iter::from_fn(move || {
|
||||||
|
let mut is_icon_theme = false;
|
||||||
let mut name = "";
|
let mut name = "";
|
||||||
let mut size = None;
|
let mut size = None;
|
||||||
let mut max_size = None;
|
let mut max_size = None;
|
||||||
let mut min_size = None;
|
let mut min_size = None;
|
||||||
let mut threshold = None;
|
let mut threshold = None;
|
||||||
let mut scale = None;
|
let mut scale = None;
|
||||||
// let mut context = None;
|
|
||||||
let mut dtype = DirectoryType::default();
|
let mut dtype = DirectoryType::default();
|
||||||
|
|
||||||
#[allow(clippy::while_let_on_iterator)]
|
#[allow(clippy::while_let_on_iterator)]
|
||||||
while let Some(event) = iterator.next() {
|
while let Some(event) = iterator.next() {
|
||||||
match event {
|
match event {
|
||||||
DirectorySection::Property(key, value) => {
|
DirectorySection::Property(key, value) => {
|
||||||
if name.is_empty() || name == "Icon Theme" {
|
if is_icon_theme {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"Size" => size = str::parse(value).ok(),
|
b"Size" => size = btoi::btoi(value).ok(),
|
||||||
"Scale" => scale = str::parse(value).ok(),
|
b"Scale" => scale = btoi::btoi(value).ok(),
|
||||||
// "Context" => context = Some(value),
|
b"Type" => dtype = DirectoryType::from(value),
|
||||||
"Type" => dtype = DirectoryType::from(value),
|
b"MaxSize" => max_size = btoi::btoi(value).ok(),
|
||||||
"MaxSize" => max_size = str::parse(value).ok(),
|
b"MinSize" => min_size = btoi::btoi(value).ok(),
|
||||||
"MinSize" => min_size = str::parse(value).ok(),
|
b"Threshold" => threshold = btoi::btoi(value).ok(),
|
||||||
"Threshold" => threshold = str::parse(value).ok(),
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectorySection::Section(new_name) => {
|
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;
|
name = new_name;
|
||||||
size = None;
|
size = None;
|
||||||
max_size = None;
|
max_size = None;
|
||||||
|
|
@ -81,7 +53,7 @@ impl Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectorySection::EndSection => {
|
DirectorySection::EndSection => {
|
||||||
if name.is_empty() || name == "Icon Theme" {
|
if is_icon_theme {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +63,6 @@ impl Theme {
|
||||||
name,
|
name,
|
||||||
size,
|
size,
|
||||||
scale: scale.unwrap_or(1),
|
scale: scale.unwrap_or(1),
|
||||||
// context,
|
|
||||||
type_: dtype,
|
type_: dtype,
|
||||||
maxsize: max_size.unwrap_or(size),
|
maxsize: max_size.unwrap_or(size),
|
||||||
minsize: min_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<Item = &'a [u8]> {
|
||||||
icon_theme_section(file)
|
icon_theme_section(file)
|
||||||
.find(|&(key, _)| key == "Inherits")
|
.find(|&(key, _)| key == b"Inherits")
|
||||||
.map(|(_, parents)| {
|
.into_iter()
|
||||||
parents
|
.flat_map(|(_, parents)| {
|
||||||
.split(',')
|
BStr::new(parents)
|
||||||
|
.split(|&char| char == b',')
|
||||||
// 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 != &b"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<Item = DirectorySection<'_>> {
|
||||||
|
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<Item = (&[u8], &[u8])> + '_ {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
|
|
||||||
mod test {
|
mod test {
|
||||||
use crate::THEMES;
|
const ADWAITA_INDEX: &str = "[Icon Theme]
|
||||||
use speculoos::prelude::*;
|
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]
|
#[test]
|
||||||
#[cfg(feature = "local_tests")]
|
#[cfg(feature = "local_tests")]
|
||||||
fn should_get_theme_parents() {
|
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 = crate::theme::read_ini_theme(&theme.index).ok().unwrap();
|
||||||
let file = std::str::from_utf8(file.as_ref()).ok().unwrap();
|
let file = std::str::from_utf8(file.as_ref()).ok().unwrap();
|
||||||
let parents = theme.inherits(file);
|
let parents = theme.inherits(file);
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,38 @@
|
||||||
use dirs::home_dir;
|
|
||||||
use std::path::PathBuf;
|
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 = std::env::home_dir()
|
||||||
Some(home_icon_dir) => data_dirs.push(home_icon_dir),
|
.into_iter()
|
||||||
None => tracing::warn!("No $HOME directory found"),
|
.map(|home| home.join(".icons"));
|
||||||
}
|
|
||||||
data_dirs.into_iter().filter(|p| p.exists()).collect()
|
data_dirs
|
||||||
|
.chain(data_home_dirs)
|
||||||
|
.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;
|
||||||
|
|
@ -54,7 +42,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn should_get_all_themes() {
|
fn should_get_all_themes() {
|
||||||
let themes = get_all_themes();
|
let themes = get_all_themes();
|
||||||
assert_that!(themes.get("hicolor")).is_some();
|
assert_that!(themes.get(&b"hicolor"[..])).is_some();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue