diff --git a/.gitignore b/.gitignore index 07a1e39..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target Cargo.lock - diff --git a/Cargo.toml b/Cargo.toml index 19bf4ea..eaa6881 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,17 @@ keywords = ["icons", "gui", "freedesktop"] [dependencies] dirs = "5.0.1" -rust-ini = "0.20.0" thiserror = "1.0.56" once_cell = "1.19.0" xdg = "2.5.2" tracing = "0.1.41" +ini_core = "0.2.0" [dev-dependencies] speculoos = "0.11.0" -anyhow = "1.0.79" linicon = "2.3.0" -gtk4 = "0.4.7" -criterion = "0.3.5" +gtk4 = "0.9" +criterion = "0.5" [features] default = [] diff --git a/src/lib.rs b/src/lib.rs index 67105cb..5f72649 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ use theme::BASE_PATHS; use crate::cache::{CacheEntry, CACHE}; use crate::theme::{try_build_icon_path, THEMES}; +use std::io::BufRead; use std::path::PathBuf; mod cache; @@ -74,15 +75,29 @@ mod theme; /// "Papirus-Light", "Breeze", "Breeze Dark", "Breeze", "ePapirus", "ePapirus-Dark", "Hicolor" /// ]) /// # } -pub fn list_themes() -> Vec<&'static str> { +pub fn list_themes() -> Vec { let mut themes = THEMES .values() .flatten() .map(|path| &path.index) .filter_map(|index| { - index - .section(Some("Icon Theme")) - .and_then(|section| section.get("Name")) + let file = std::fs::File::open(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 }) .collect::>(); themes.dedup(); @@ -99,7 +114,7 @@ pub fn list_themes() -> Vec<&'static str> { /// /// assert_eq!(Some("Adwaita"), theme); /// ``` -pub fn default_theme_gtk() -> Option<&'static str> { +pub fn default_theme_gtk() -> Option { // Calling gsettings is the simplest way to retrieve the default icon theme without adding // GTK as a dependency. There seems to be several ways to set the default GTK theme // including a file in XDG_CONFIG_HOME as well as an env var. Gsettings is the most @@ -115,9 +130,23 @@ pub fn default_theme_gtk() -> Option<&'static str> { let name = name.trim().trim_matches('\''); THEMES.get(name).and_then(|themes| { themes.first().and_then(|path| { - path.index - .section(Some("Icon Theme")) - .and_then(|section| section.get("Name")) + 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 { @@ -261,7 +290,7 @@ impl<'a> LookupBuilder<'a> { if self.cache { if let CacheEntry::Found(icon) = self.cache_lookup(self.theme) { return Some(icon); - }; + } } // Then lookup in the given theme @@ -278,11 +307,18 @@ impl<'a> LookupBuilder<'a> { // Fallback to the parent themes recursively let mut parents = icon_themes .iter() - .flat_map(|t| t.inherits()) + .flat_map(|t| { + let file = theme::read_ini_theme(&t.index); + + t.inherits(file.as_ref()) + .into_iter() + .map(String::from) + .collect::>() + }) .collect::>(); parents.dedup(); parents.into_iter().find_map(|parent| { - THEMES.get(parent).and_then(|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) }) diff --git a/src/theme/error.rs b/src/theme/error.rs index 732654c..5ca693c 100644 --- a/src/theme/error.rs +++ b/src/theme/error.rs @@ -8,6 +8,4 @@ pub(crate) enum ThemeError { ThemeIndexNotFound(PathBuf), #[error("IoError: {0}")] IoError(#[from] io::Error), - #[error("IniError: {0}")] - IniError(#[from] ini::Error), } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index ac1ef02..5089b9f 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,10 +1,8 @@ use crate::theme::error::ThemeError; use crate::theme::paths::ThemePath; -use ini::Ini; use once_cell::sync::Lazy; pub(crate) use paths::BASE_PATHS; use std::collections::BTreeMap; -use std::fmt::{Debug, Formatter}; use std::path::{Path, PathBuf}; mod directories; @@ -16,9 +14,14 @@ type Result = std::result::Result; pub static THEMES: Lazy>> = Lazy::new(get_all_themes); +pub fn read_ini_theme(path: &Path) -> String { + std::fs::read_to_string(path).unwrap_or_default() +} + +#[derive(Debug)] pub struct Theme { pub path: ThemePath, - pub index: Ini, + pub index: PathBuf, } impl Theme { @@ -29,23 +32,30 @@ impl Theme { scale: u16, force_svg: bool, ) -> Option { - self.try_get_icon_exact_size(name, size, scale, force_svg) - .or_else(|| self.try_get_icon_closest_size(name, size, scale, force_svg)) + 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)) } fn try_get_icon_exact_size( &self, + file: &str, name: &str, size: u16, scale: u16, force_svg: bool, ) -> Option { - self.match_size(size, scale) + self.match_size(file, size, scale) .find_map(|path| try_build_icon_path(name, path, force_svg)) } - fn match_size(&self, size: u16, scale: u16) -> impl Iterator + '_ { - let dirs = self.get_all_directories(); + fn match_size<'a>( + &'a self, + file: &'a str, + size: u16, + scale: u16, + ) -> impl Iterator + 'a { + let dirs = self.get_all_directories(file); dirs.filter(move |directory| directory.match_size(size, scale)) .map(|dir| dir.name) @@ -54,18 +64,19 @@ impl Theme { fn try_get_icon_closest_size( &self, + file: &str, name: &str, size: u16, scale: u16, force_svg: bool, ) -> Option { - self.closest_match_size(size, scale) + self.closest_match_size(file, size, scale) .iter() .find_map(|path| try_build_icon_path(name, path, force_svg)) } - fn closest_match_size(&self, size: u16, scale: u16) -> Vec { - let dirs = self.get_all_directories(); + fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec { + let dirs = self.get_all_directories(file); let mut dirs: Vec<_> = dirs .filter_map(|directory| { @@ -181,7 +192,7 @@ pub(super) fn get_all_themes() -> BTreeMap> { } impl Theme { - pub(crate) fn from_path>(path: P, index: Option<&Ini>) -> Option { + pub(crate) fn from_path>(path: P, index: Option<&PathBuf>) -> Option { let path = path.as_ref(); let has_index = path.join("index.theme").exists() || index.is_some(); @@ -203,15 +214,6 @@ impl Theme { } } -impl Debug for Theme { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut content = vec![]; - self.index.write_to(&mut content).expect("Write error"); - let content = String::from_utf8_lossy(&content); - writeln!(f, "ThemeIndex{{path: {:?}, index: {content:?}}}", self.path) - } -} - #[cfg(test)] mod test { use crate::THEMES; @@ -223,21 +225,20 @@ mod test { let themes = THEMES.get("Adwaita").unwrap(); println!( "{:?}", - themes.iter().find_map(|t| t.try_get_icon_exact_size( - "edit-delete-symbolic", - 24, - 1, - false - )) + 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) + }) ); } #[test] fn should_get_png_first() { let themes = THEMES.get("hicolor").unwrap(); - let icon = themes - .iter() - .find_map(|t| t.try_get_icon_exact_size("blueman", 24, 1, true)); + 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) + }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( "/usr/share/icons/hicolor/scalable/apps/blueman.svg", )); @@ -246,9 +247,10 @@ mod test { #[test] fn should_get_svg_first() { let themes = THEMES.get("hicolor").unwrap(); - let icon = themes - .iter() - .find_map(|t| t.try_get_icon_exact_size("blueman", 24, 1, false)); + 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) + }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( "/usr/share/icons/hicolor/22x22/apps/blueman.png", )); diff --git a/src/theme/parse.rs b/src/theme/parse.rs index 3d52ad8..ea40bf4 100644 --- a/src/theme/parse.rs +++ b/src/theme/parse.rs @@ -1,34 +1,114 @@ use crate::theme::directories::{Directory, DirectoryType}; use crate::theme::Theme; -use ini::Properties; + +fn icon_theme_section(file: &str) -> impl Iterator + '_ { + ini_core::Parser::new(file) + .skip_while(|item| *item != ini_core::Item::Section("Icon Theme")) + .take_while(|item| match item { + ini_core::Item::Section(value) => *value == "Icon Theme", + _ => true, + }) + .filter_map(|item| { + if let ini_core::Item::Property(key, value) = item { + Some((key, value?)) + } else { + None + } + }) +} + +#[derive(Debug)] +enum DirectorySection<'a> { + Property(&'a str, &'a str), + EndSection, + Section(&'a str), +} + +fn sections(file: &str) -> impl Iterator { + ini_core::Parser::new(file).filter_map(move |item| match item { + ini_core::Item::Property(key, Some(value)) => Some(DirectorySection::Property(key, value)), + ini_core::Item::Section(section) => Some(DirectorySection::Section(section)), + ini_core::Item::SectionEnd => Some(DirectorySection::EndSection), + _ => None, + }) +} impl Theme { - pub(super) fn get_all_directories(&self) -> impl Iterator { - self.directories() - .into_iter() - .filter_map(|name| self.get_directory(name)) - .chain( - self.scaled_directories() - .into_iter() - .filter_map(|name| self.get_directory(name)), - ) + pub(super) fn get_all_directories<'a>( + &'a self, + file: &'a str, + ) -> impl Iterator> + 'a { + let mut iterator = sections(file); + + std::iter::from_fn(move || { + let mut name = ""; + let mut size = None; + let mut max_size = None; + let mut min_size = None; + let mut threshold = None; + let mut scale = None; + // let mut context = None; + let mut dtype = DirectoryType::default(); + + #[allow(clippy::while_let_on_iterator)] + while let Some(event) = iterator.next() { + match event { + DirectorySection::Property(key, value) => { + if name.is_empty() || name == "Icon Theme" { + continue; + } + + match key { + "Size" => size = str::parse(value).ok(), + "Scale" => scale = str::parse(value).ok(), + // "Context" => context = Some(value), + "Type" => dtype = DirectoryType::from(value), + "MaxSize" => max_size = str::parse(value).ok(), + "MinSize" => min_size = str::parse(value).ok(), + "Threshold" => threshold = str::parse(value).ok(), + _ => (), + } + } + + DirectorySection::Section(new_name) => { + name = new_name; + size = None; + max_size = None; + min_size = None; + threshold = None; + scale = None; + dtype = DirectoryType::default(); + } + + DirectorySection::EndSection => { + if name.is_empty() || name == "Icon Theme" { + continue; + } + + let size = size.take()?; + + return Some(Directory { + name, + size, + scale: scale.unwrap_or(1), + // context, + type_: dtype, + maxsize: max_size.unwrap_or(size), + minsize: min_size.unwrap_or(size), + threshold: threshold.unwrap_or(2), + }); + } + } + } + + None + }) } - fn scaled_directories(&self) -> Vec<&str> { - self.get_icon_theme_section() - .and_then(|props| props.get("ScaledDirectories")) - .map(|dirs| dirs.split(',').collect()) - .unwrap_or_default() - } - - fn get_icon_theme_section(&self) -> Option<&Properties> { - self.index.section(Some("Icon Theme")) - } - - pub fn inherits(&self) -> Vec<&str> { - self.get_icon_theme_section() - .and_then(|props| props.get("Inherits")) - .map(|parents| { + pub fn inherits<'a>(&self, file: &'a str) -> Vec<&'a str> { + icon_theme_section(file) + .find(|&(key, _)| key == "Inherits") + .map(|(_, parents)| { parents .split(',') // Filtering out 'hicolor' since we are going to fallback there anyway @@ -37,47 +117,6 @@ impl Theme { }) .unwrap_or_default() } - - fn directories(&self) -> Vec<&str> { - self.index - .section(Some("Icon Theme")) - .and_then(|props| props.get("Directories")) - .map(|dirs| dirs.split(',').collect()) - .unwrap_or_default() - } - - fn get_directory<'a>(&'a self, name: &'a str) -> Option> { - self.index.section(Some(name)).map(|props| { - let size = props - .get("Size") - .and_then(|size| str::parse(size).ok()) - .expect("Size not found for icon"); - Directory { - name, - size, - scale: props - .get("Scale") - .and_then(|scale| str::parse(scale).ok()) - .unwrap_or(1), - type_: props - .get("Type") - .map(DirectoryType::from) - .unwrap_or_default(), - maxsize: props - .get("MaxSize") - .and_then(|max| str::parse(max).ok()) - .unwrap_or(size), - minsize: props - .get("MinSize") - .and_then(|min| str::parse(min).ok()) - .unwrap_or(size), - threshold: props - .get("Threshold") - .and_then(|thrsh| str::parse(thrsh).ok()) - .unwrap_or(2), - } - }) - } } #[cfg(test)] @@ -88,7 +127,8 @@ mod test { #[test] fn should_get_theme_parents() { for theme in THEMES.get("Arc").unwrap() { - let parents = theme.inherits(); + let file = crate::theme::read_ini_theme(&theme.index); + let parents = theme.inherits(&file); assert_that!(parents).does_not_contain("hicolor"); diff --git a/src/theme/paths.rs b/src/theme/paths.rs index 93b45d5..dea8f48 100644 --- a/src/theme/paths.rs +++ b/src/theme/paths.rs @@ -1,7 +1,6 @@ use std::path::PathBuf; use dirs::home_dir; -use ini::Ini; use once_cell::sync::Lazy; use xdg::BaseDirectories; @@ -13,7 +12,6 @@ pub(crate) static BASE_PATHS: Lazy> = Lazy::new(icon_theme_base_pat /// Look in $HOME/.icons (for backwards compatibility), in $XDG_DATA_DIRS/icons, in $XDG_DATA_DIRS/pixmaps and in /usr/share/pixmaps (in that order). /// Paths that are not found are filtered out. fn icon_theme_base_paths() -> Vec { - let home_icon_dir = home_dir().expect("No $HOME directory").join(".icons"); let mut data_dirs: Vec<_> = BaseDirectories::new() .map(|bd| { let mut data_dirs: Vec<_> = bd @@ -27,22 +25,25 @@ fn icon_theme_base_paths() -> Vec { data_dirs }) .unwrap_or_default(); - data_dirs.push(home_icon_dir); + match home_dir().map(|home| home.join(".icons")) { + Some(home_icon_dir) => data_dirs.push(home_icon_dir), + None => tracing::warn!("No $HOME directory found"), + } data_dirs.into_iter().filter(|p| p.exists()).collect() } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ThemePath(pub PathBuf); impl ThemePath { - pub(super) fn index(&self) -> theme::Result { + pub(super) fn index(&self) -> theme::Result { let index = self.0.join("index.theme"); if !index.exists() { return Err(ThemeError::ThemeIndexNotFound(index)); } - Ok(Ini::load_from_file(index)?) + Ok(index) } } @@ -50,7 +51,6 @@ impl ThemePath { mod test { use crate::theme::paths::icon_theme_base_paths; use crate::theme::{get_all_themes, Theme}; - use anyhow::Result; use speculoos::prelude::*; #[test] @@ -66,10 +66,9 @@ mod test { } #[test] - fn should_read_theme_index() -> Result<()> { + fn should_read_theme_index() { let themes = get_all_themes(); let themes: Vec<&Theme> = themes.values().flatten().collect(); assert_that!(themes).is_not_empty(); - Ok(()) } }