2022-05-12 15:34:02 +02:00
|
|
|
use crate::theme::paths::ThemePath;
|
2025-04-03 14:26:51 +02:00
|
|
|
use memmap2::Mmap;
|
2022-11-22 17:49:56 -05:00
|
|
|
pub(crate) use paths::BASE_PATHS;
|
2022-05-12 14:41:08 +02:00
|
|
|
use std::collections::BTreeMap;
|
2025-11-25 06:24:34 +01:00
|
|
|
use std::ops::ControlFlow;
|
2022-05-12 14:41:08 +02:00
|
|
|
use std::path::{Path, PathBuf};
|
2025-04-02 17:21:11 +02:00
|
|
|
use std::sync::LazyLock;
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-12 23:12:06 +02:00
|
|
|
mod directories;
|
|
|
|
|
mod parse;
|
|
|
|
|
mod paths;
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-04-02 17:21:11 +02:00
|
|
|
pub static THEMES: LazyLock<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes);
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-04-02 17:21:11 +02:00
|
|
|
#[inline]
|
2025-04-03 14:26:51 +02:00
|
|
|
pub fn read_ini_theme(path: &Path) -> std::io::Result<Mmap> {
|
|
|
|
|
std::fs::File::open(path).and_then(|file| unsafe { Mmap::map(&file) })
|
2025-01-23 11:18:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2022-05-12 14:41:08 +02:00
|
|
|
pub struct Theme {
|
2022-05-13 08:00:52 +02:00
|
|
|
pub path: ThemePath,
|
2025-01-23 11:18:33 +01:00
|
|
|
pub index: PathBuf,
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
impl Theme {
|
2025-04-02 17:21:11 +02:00
|
|
|
#[inline]
|
2022-05-24 09:17:29 +02:00
|
|
|
pub fn try_get_icon(
|
|
|
|
|
&self,
|
|
|
|
|
name: &str,
|
|
|
|
|
size: u16,
|
|
|
|
|
scale: u16,
|
|
|
|
|
force_svg: bool,
|
|
|
|
|
) -> Option<PathBuf> {
|
2025-04-03 14:26:51 +02:00
|
|
|
let file = read_ini_theme(&self.index).ok()?;
|
|
|
|
|
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
|
|
|
|
self.try_get_icon_exact_size(file, name, size, scale, force_svg)
|
|
|
|
|
.or_else(|| self.try_get_icon_closest_size(file, name, size, scale, force_svg))
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 17:21:11 +02:00
|
|
|
#[inline]
|
2022-05-24 09:17:29 +02:00
|
|
|
fn try_get_icon_exact_size(
|
|
|
|
|
&self,
|
2025-01-23 11:18:33 +01:00
|
|
|
file: &str,
|
2022-05-24 09:17:29 +02:00
|
|
|
name: &str,
|
|
|
|
|
size: u16,
|
|
|
|
|
scale: u16,
|
|
|
|
|
force_svg: bool,
|
|
|
|
|
) -> Option<PathBuf> {
|
2025-11-25 06:24:34 +01:00
|
|
|
self.try_fold_icon_path(self.match_size(file, size, scale), name, force_svg)
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 17:21:11 +02:00
|
|
|
#[inline]
|
2025-01-23 11:18:33 +01:00
|
|
|
fn match_size<'a>(
|
|
|
|
|
&'a self,
|
|
|
|
|
file: &'a str,
|
|
|
|
|
size: u16,
|
|
|
|
|
scale: u16,
|
2025-11-25 06:24:34 +01:00
|
|
|
) -> impl Iterator<Item = &'a str> + 'a {
|
|
|
|
|
self.get_all_directories(file)
|
|
|
|
|
.filter(move |directory| directory.match_size(size, scale))
|
2022-05-12 14:41:08 +02:00
|
|
|
.map(|dir| dir.name)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 17:21:11 +02:00
|
|
|
#[inline]
|
2022-05-24 09:17:29 +02:00
|
|
|
fn try_get_icon_closest_size(
|
|
|
|
|
&self,
|
2025-01-23 11:18:33 +01:00
|
|
|
file: &str,
|
2022-05-24 09:17:29 +02:00
|
|
|
name: &str,
|
|
|
|
|
size: u16,
|
|
|
|
|
scale: u16,
|
|
|
|
|
force_svg: bool,
|
|
|
|
|
) -> Option<PathBuf> {
|
2025-11-25 06:24:34 +01:00
|
|
|
self.try_fold_icon_path(self.closest_match_size(file, size, scale), name, force_svg)
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
fn try_fold_icon_path<'a>(
|
|
|
|
|
&self,
|
|
|
|
|
mut dir_names: impl Iterator<Item = &'a str>,
|
|
|
|
|
name: &str,
|
|
|
|
|
force_svg: bool,
|
|
|
|
|
) -> Option<PathBuf> {
|
|
|
|
|
dir_names
|
|
|
|
|
.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)
|
2022-11-24 11:43:48 -05:00
|
|
|
} else {
|
2025-11-25 06:24:34 +01:00
|
|
|
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)
|
2022-11-24 11:43:48 -05:00
|
|
|
}
|
|
|
|
|
})
|
2025-11-25 06:24:34 +01:00
|
|
|
.break_value()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2022-11-24 11:43:48 -05:00
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
dirs.fold(Vec::<(&'a str, i16)>::new(), |mut sorted, directory| {
|
|
|
|
|
let distance = directory.directory_size_distance(size, scale);
|
|
|
|
|
if distance < i16::MAX {
|
|
|
|
|
let a = distance.abs();
|
|
|
|
|
let pos = sorted
|
|
|
|
|
.binary_search_by(|(_, b)| b.cmp(&a))
|
|
|
|
|
.unwrap_or_else(|pos| pos);
|
|
|
|
|
sorted.insert(pos, (directory.name, a));
|
|
|
|
|
}
|
2022-11-24 11:43:48 -05:00
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
sorted
|
|
|
|
|
})
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(name, _)| name)
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path(&self) -> &PathBuf {
|
|
|
|
|
&self.path.0
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
pub(super) fn try_build_icon_path<'a>(path: &'a mut PathBuf, name: &str, force_svg: bool) -> bool {
|
|
|
|
|
let mut name_buf = String::with_capacity(name.len() + 4);
|
|
|
|
|
name_buf.push_str(name);
|
|
|
|
|
path.push(name);
|
2022-05-23 20:36:24 +02:00
|
|
|
if force_svg {
|
2025-11-25 06:24:34 +01:00
|
|
|
try_build_ext(path, &mut name_buf, name, ".svg")
|
|
|
|
|
|| try_build_ext(path, &mut name_buf, name, ".png")
|
|
|
|
|
|| try_build_ext(path, &mut name_buf, name, ".xmp")
|
2022-05-23 20:36:24 +02:00
|
|
|
} else {
|
2025-11-25 06:24:34 +01:00
|
|
|
try_build_ext(path, &mut name_buf, name, ".png")
|
|
|
|
|
|| try_build_ext(path, &mut name_buf, name, ".svg")
|
|
|
|
|
|| try_build_ext(path, &mut name_buf, name, ".xmp")
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-23 20:36:24 +02:00
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
#[inline]
|
|
|
|
|
fn try_build_ext(path: &mut PathBuf, name_buf: &mut String, name: &str, ext: &'static str) -> bool {
|
|
|
|
|
name_buf.truncate(name.len());
|
|
|
|
|
name_buf.push_str(ext);
|
|
|
|
|
path.set_file_name(&name_buf);
|
|
|
|
|
path.exists()
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Iter through the base paths and get all theme directories
|
2024-12-05 12:03:44 +01:00
|
|
|
pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
|
2023-01-09 23:54:07 +01:00
|
|
|
let mut icon_themes = BTreeMap::<_, Vec<_>>::new();
|
2024-01-23 13:21:49 -05:00
|
|
|
let mut found_indices = BTreeMap::new();
|
|
|
|
|
let mut to_revisit = Vec::new();
|
|
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
for theme_base_dir in BASE_PATHS.iter() {
|
2024-12-05 12:03:44 +01:00
|
|
|
let dir_iter = match theme_base_dir.read_dir() {
|
|
|
|
|
Ok(dir) => dir,
|
|
|
|
|
Err(why) => {
|
|
|
|
|
tracing::error!(?why, dir = ?theme_base_dir, "unable to read icon theme directory");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for entry in dir_iter.filter_map(std::io::Result::ok) {
|
2024-01-23 13:21:49 -05:00
|
|
|
let name = entry.file_name();
|
|
|
|
|
let fallback_index = found_indices.get(&name);
|
|
|
|
|
if let Some(theme) = Theme::from_path(entry.path(), fallback_index) {
|
|
|
|
|
if fallback_index.is_none() {
|
|
|
|
|
found_indices.insert(name.clone(), theme.index.clone());
|
|
|
|
|
}
|
|
|
|
|
let name = name.to_string_lossy().to_string();
|
2023-01-09 23:54:07 +01:00
|
|
|
icon_themes.entry(name).or_default().push(theme);
|
2024-01-23 13:21:49 -05:00
|
|
|
} else if entry.path().is_dir() {
|
|
|
|
|
to_revisit.push(entry);
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-23 13:21:49 -05:00
|
|
|
|
|
|
|
|
for entry in to_revisit {
|
|
|
|
|
let name = entry.file_name();
|
|
|
|
|
let fallback_index = found_indices.get(&name);
|
|
|
|
|
if let Some(theme) = Theme::from_path(entry.path(), fallback_index) {
|
2025-11-25 06:24:34 +01:00
|
|
|
if let Some(name) = name.to_str() {
|
|
|
|
|
icon_themes.entry(name.to_owned()).or_default().push(theme);
|
|
|
|
|
}
|
2024-01-23 13:21:49 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-05 12:03:44 +01:00
|
|
|
icon_themes
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
impl Theme {
|
2025-01-23 11:18:33 +01:00
|
|
|
pub(crate) fn from_path<P: AsRef<Path>>(path: P, index: Option<&PathBuf>) -> Option<Self> {
|
2025-11-25 06:24:34 +01:00
|
|
|
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();
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
if !has_index || !is_dir {
|
2022-05-12 14:41:08 +02:00
|
|
|
return None;
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 06:24:34 +01:00
|
|
|
index
|
|
|
|
|
.cloned()
|
|
|
|
|
.or_else(|| local_index_exists.then_some(path.clone()))
|
|
|
|
|
.map(|index| Theme {
|
|
|
|
|
path: ThemePath({
|
|
|
|
|
path.pop();
|
|
|
|
|
path
|
|
|
|
|
}),
|
|
|
|
|
index,
|
|
|
|
|
})
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
|
|
|
|
use crate::THEMES;
|
2022-05-24 09:17:29 +02:00
|
|
|
use speculoos::prelude::*;
|
|
|
|
|
use std::path::PathBuf;
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
#[test]
|
|
|
|
|
fn get_one_icon() {
|
2023-01-09 23:54:07 +01:00
|
|
|
let themes = THEMES.get("Adwaita").unwrap();
|
2022-05-12 14:41:08 +02:00
|
|
|
println!(
|
|
|
|
|
"{:?}",
|
2025-01-23 11:18:33 +01:00
|
|
|
themes.iter().find_map(|t| {
|
2025-04-03 14:26:51 +02:00
|
|
|
let file = super::read_ini_theme(&t.index).ok()?;
|
|
|
|
|
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
|
|
|
|
t.try_get_icon_exact_size(file, "edit-delete-symbolic", 24, 1, false)
|
2025-01-23 11:18:33 +01:00
|
|
|
})
|
2022-05-12 14:41:08 +02:00
|
|
|
);
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
2022-05-23 20:36:24 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn should_get_png_first() {
|
2023-01-09 23:54:07 +01:00
|
|
|
let themes = THEMES.get("hicolor").unwrap();
|
2025-01-23 11:18:33 +01:00
|
|
|
let icon = themes.iter().find_map(|t| {
|
2025-04-03 14:26:51 +02:00
|
|
|
let file = super::read_ini_theme(&t.index).ok()?;
|
|
|
|
|
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
|
|
|
|
t.try_get_icon_exact_size(file, "blueman", 24, 1, true)
|
2025-01-23 11:18:33 +01:00
|
|
|
});
|
2022-05-24 09:17:29 +02:00
|
|
|
assert_that!(icon).is_some().is_equal_to(PathBuf::from(
|
2025-04-16 09:00:07 +02:00
|
|
|
"/usr/share/icons/hicolor/22x22/apps/blueman.png",
|
2022-05-24 09:17:29 +02:00
|
|
|
));
|
2022-05-23 20:36:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn should_get_svg_first() {
|
2023-01-09 23:54:07 +01:00
|
|
|
let themes = THEMES.get("hicolor").unwrap();
|
2025-01-23 11:18:33 +01:00
|
|
|
let icon = themes.iter().find_map(|t| {
|
2025-04-03 14:26:51 +02:00
|
|
|
let file = super::read_ini_theme(&t.index).ok()?;
|
|
|
|
|
let file = std::str::from_utf8(file.as_ref()).ok()?;
|
|
|
|
|
t.try_get_icon_exact_size(file, "blueman", 24, 1, false)
|
2025-01-23 11:18:33 +01:00
|
|
|
});
|
2022-05-24 09:17:29 +02:00
|
|
|
assert_that!(icon).is_some().is_equal_to(PathBuf::from(
|
|
|
|
|
"/usr/share/icons/hicolor/22x22/apps/blueman.png",
|
|
|
|
|
));
|
2022-05-23 20:36:24 +02:00
|
|
|
}
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|