2022-05-12 10:10:48 +02:00
|
|
|
use crate::theme::error::ThemeError;
|
2022-05-12 15:34:02 +02:00
|
|
|
use crate::theme::paths::ThemePath;
|
2022-05-12 14:41:08 +02:00
|
|
|
use once_cell::sync::Lazy;
|
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;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-12 23:12:06 +02:00
|
|
|
mod directories;
|
2022-05-12 10:10:48 +02:00
|
|
|
pub mod error;
|
2022-05-12 23:12:06 +02:00
|
|
|
mod parse;
|
|
|
|
|
mod paths;
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
type Result<T> = std::result::Result<T, ThemeError>;
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-01-23 11:09:45 +01:00
|
|
|
pub static THEMES: Lazy<BTreeMap<String, Vec<Theme>>> = Lazy::new(get_all_themes);
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-01-23 11:18:33 +01:00
|
|
|
pub fn read_ini_theme(path: &Path) -> String {
|
|
|
|
|
std::fs::read_to_string(path).unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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 {
|
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-01-23 11:18:33 +01:00
|
|
|
let file = read_ini_theme(&self.index);
|
|
|
|
|
self.try_get_icon_exact_size(file.as_str(), name, size, scale, force_svg)
|
|
|
|
|
.or_else(|| self.try_get_icon_closest_size(file.as_str(), name, size, scale, force_svg))
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
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-01-23 11:18:33 +01:00
|
|
|
self.match_size(file, size, scale)
|
2022-05-23 20:36:24 +02:00
|
|
|
.find_map(|path| try_build_icon_path(name, path, force_svg))
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-01-23 11:18:33 +01:00
|
|
|
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);
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-05-13 08:00:52 +02:00
|
|
|
dirs.filter(move |directory| directory.match_size(size, scale))
|
2022-05-12 14:41:08 +02:00
|
|
|
.map(|dir| dir.name)
|
|
|
|
|
.map(|dir| self.path().join(dir))
|
|
|
|
|
}
|
|
|
|
|
|
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-01-23 11:18:33 +01:00
|
|
|
self.closest_match_size(file, size, scale)
|
2022-05-12 14:41:08 +02:00
|
|
|
.iter()
|
2022-05-23 20:36:24 +02:00
|
|
|
.find_map(|path| try_build_icon_path(name, path, force_svg))
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2025-01-23 11:18:33 +01:00
|
|
|
fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec<PathBuf> {
|
|
|
|
|
let dirs = self.get_all_directories(file);
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2022-11-24 11:43:48 -05:00
|
|
|
let mut dirs: Vec<_> = dirs
|
|
|
|
|
.filter_map(|directory| {
|
|
|
|
|
let distance = directory.directory_size_distance(size, scale);
|
|
|
|
|
if distance < i16::MAX {
|
2025-01-23 17:46:50 +08:00
|
|
|
Some((directory, distance.abs()))
|
2022-11-24 11:43:48 -05:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
dirs.sort_by(|(_, a), (_, b)| a.cmp(b));
|
|
|
|
|
|
|
|
|
|
dirs.iter()
|
|
|
|
|
.map(|(dir, _)| dir)
|
2022-05-12 14:41:08 +02:00
|
|
|
.map(|dir| dir.name)
|
|
|
|
|
.map(|dir| self.path().join(dir))
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path(&self) -> &PathBuf {
|
|
|
|
|
&self.path.0
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
|
2022-05-24 09:17:29 +02:00
|
|
|
pub(super) fn try_build_icon_path<P: AsRef<Path>>(
|
|
|
|
|
name: &str,
|
|
|
|
|
path: P,
|
|
|
|
|
force_svg: bool,
|
|
|
|
|
) -> Option<PathBuf> {
|
2022-05-23 20:36:24 +02:00
|
|
|
if force_svg {
|
|
|
|
|
try_build_svg(name, path.as_ref())
|
2025-04-12 13:04:52 +05:30
|
|
|
.or_else(|| try_build_png(name, path.as_ref()))
|
|
|
|
|
.or_else(|| try_build_xmp(name, path.as_ref()))
|
2022-05-23 20:36:24 +02:00
|
|
|
} else {
|
|
|
|
|
try_build_png(name, path.as_ref())
|
2022-05-24 09:17:29 +02:00
|
|
|
.or_else(|| try_build_svg(name, path.as_ref()))
|
|
|
|
|
.or_else(|| try_build_xmp(name, path.as_ref()))
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-23 20:36:24 +02:00
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
|
2022-05-23 20:36:24 +02:00
|
|
|
fn try_build_svg<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
|
|
|
|
|
let path = path.as_ref();
|
2022-05-12 14:41:08 +02:00
|
|
|
let svg = path.join(format!("{name}.svg"));
|
2022-11-22 17:49:56 -05:00
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
if svg.exists() {
|
2022-05-23 20:36:24 +02:00
|
|
|
Some(svg)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-23 20:36:24 +02:00
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
|
2022-05-23 20:36:24 +02:00
|
|
|
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"));
|
2022-11-22 17:49:56 -05:00
|
|
|
|
2022-05-23 20:36:24 +02:00
|
|
|
if png.exists() {
|
|
|
|
|
Some(png)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
2022-05-23 20:36:24 +02:00
|
|
|
}
|
2022-05-12 14:41:08 +02:00
|
|
|
|
2022-05-23 20:36:24 +02:00
|
|
|
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
|
|
|
|
|
}
|
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) {
|
|
|
|
|
let name = name.to_string_lossy().to_string();
|
|
|
|
|
icon_themes.entry(name).or_default().push(theme);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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> {
|
2022-05-12 14:41:08 +02:00
|
|
|
let path = path.as_ref();
|
2022-05-12 10:10:48 +02:00
|
|
|
|
2024-01-23 13:21:49 -05:00
|
|
|
let has_index = path.join("index.theme").exists() || index.is_some();
|
2022-05-12 14:41:08 +02:00
|
|
|
|
|
|
|
|
if !has_index || !path.is_dir() {
|
|
|
|
|
return None;
|
2022-05-12 10:10:48 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-12 14:41:08 +02:00
|
|
|
let path = ThemePath(path.into());
|
|
|
|
|
|
2024-01-23 13:21:49 -05:00
|
|
|
match (index, path.index()) {
|
|
|
|
|
(Some(index), _) => Some(Theme {
|
|
|
|
|
path,
|
|
|
|
|
index: index.clone(),
|
|
|
|
|
}),
|
|
|
|
|
(None, Ok(index)) => Some(Theme { path, index }),
|
|
|
|
|
_ => None,
|
2022-05-12 14:41:08 +02:00
|
|
|
}
|
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| {
|
|
|
|
|
let file = crate::theme::read_ini_theme(&t.index);
|
|
|
|
|
t.try_get_icon_exact_size(file.as_str(), "edit-delete-symbolic", 24, 1, false)
|
|
|
|
|
})
|
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| {
|
|
|
|
|
let file = crate::theme::read_ini_theme(&t.index);
|
|
|
|
|
t.try_get_icon_exact_size(file.as_str(), "blueman", 24, 1, true)
|
|
|
|
|
});
|
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| {
|
|
|
|
|
let file = crate::theme::read_ini_theme(&t.index);
|
|
|
|
|
t.try_get_icon_exact_size(file.as_str(), "blueman", 24, 1, false)
|
|
|
|
|
});
|
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
|
|
|
}
|