cosmic_freedesktop_icons/theme/
mod.rs

1use crate::theme::error::ThemeError;
2use crate::theme::paths::ThemePath;
3use memmap2::Mmap;
4pub(crate) use paths::BASE_PATHS;
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7use std::sync::LazyLock;
8
9mod directories;
10pub mod error;
11mod parse;
12mod paths;
13
14type Result<T> = std::result::Result<T, ThemeError>;
15
16pub static THEMES: LazyLock<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes);
17
18#[inline]
19pub fn read_ini_theme(path: &Path) -> std::io::Result<Mmap> {
20    std::fs::File::open(path).and_then(|file| unsafe { Mmap::map(&file) })
21}
22
23#[derive(Debug)]
24pub struct Theme {
25    pub path: ThemePath,
26    pub index: PathBuf,
27}
28
29impl Theme {
30    #[inline]
31    pub fn try_get_icon(
32        &self,
33        name: &str,
34        size: u16,
35        scale: u16,
36        force_svg: bool,
37    ) -> Option<PathBuf> {
38        let file = read_ini_theme(&self.index).ok()?;
39        let file = std::str::from_utf8(file.as_ref()).ok()?;
40        self.try_get_icon_exact_size(file, name, size, scale, force_svg)
41            .or_else(|| self.try_get_icon_closest_size(file, name, size, scale, force_svg))
42    }
43
44    #[inline]
45    fn try_get_icon_exact_size(
46        &self,
47        file: &str,
48        name: &str,
49        size: u16,
50        scale: u16,
51        force_svg: bool,
52    ) -> Option<PathBuf> {
53        self.match_size(file, size, scale)
54            .find_map(|path| try_build_icon_path(name, path, force_svg))
55    }
56
57    #[inline]
58    fn match_size<'a>(
59        &'a self,
60        file: &'a str,
61        size: u16,
62        scale: u16,
63    ) -> impl Iterator<Item = PathBuf> + 'a {
64        let dirs = self.get_all_directories(file);
65
66        dirs.filter(move |directory| directory.match_size(size, scale))
67            .map(|dir| dir.name)
68            .map(|dir| self.path().join(dir))
69    }
70
71    #[inline]
72    fn try_get_icon_closest_size(
73        &self,
74        file: &str,
75        name: &str,
76        size: u16,
77        scale: u16,
78        force_svg: bool,
79    ) -> Option<PathBuf> {
80        self.closest_match_size(file, size, scale)
81            .iter()
82            .find_map(|path| try_build_icon_path(name, path, force_svg))
83    }
84
85    fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec<PathBuf> {
86        let dirs = self.get_all_directories(file);
87
88        let mut dirs: Vec<_> = dirs
89            .filter_map(|directory| {
90                let distance = directory.directory_size_distance(size, scale);
91                if distance < i16::MAX {
92                    Some((directory, distance.abs()))
93                } else {
94                    None
95                }
96            })
97            .collect();
98
99        dirs.sort_by(|(_, a), (_, b)| a.cmp(b));
100
101        dirs.iter()
102            .map(|(dir, _)| dir)
103            .map(|dir| dir.name)
104            .map(|dir| self.path().join(dir))
105            .collect()
106    }
107
108    fn path(&self) -> &PathBuf {
109        &self.path.0
110    }
111}
112
113pub(super) fn try_build_icon_path<P: AsRef<Path>>(
114    name: &str,
115    path: P,
116    force_svg: bool,
117) -> Option<PathBuf> {
118    if force_svg {
119        try_build_svg(name, path.as_ref())
120    } else {
121        try_build_png(name, path.as_ref())
122            .or_else(|| try_build_svg(name, path.as_ref()))
123            .or_else(|| try_build_xmp(name, path.as_ref()))
124    }
125}
126
127fn try_build_svg<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
128    let path = path.as_ref();
129    let svg = path.join(format!("{name}.svg"));
130
131    if svg.exists() {
132        Some(svg)
133    } else {
134        None
135    }
136}
137
138fn try_build_png<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
139    let path = path.as_ref();
140    let png = path.join(format!("{name}.png"));
141
142    if png.exists() {
143        Some(png)
144    } else {
145        None
146    }
147}
148
149fn try_build_xmp<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
150    let path = path.as_ref();
151    let xmp = path.join(format!("{name}.xmp"));
152    if xmp.exists() {
153        Some(xmp)
154    } else {
155        None
156    }
157}
158
159// Iter through the base paths and get all theme directories
160pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
161    let mut icon_themes = BTreeMap::<_, Vec<_>>::new();
162    let mut found_indices = BTreeMap::new();
163    let mut to_revisit = Vec::new();
164
165    for theme_base_dir in BASE_PATHS.iter() {
166        let dir_iter = match theme_base_dir.read_dir() {
167            Ok(dir) => dir,
168            Err(why) => {
169                tracing::error!(?why, dir = ?theme_base_dir, "unable to read icon theme directory");
170                continue;
171            }
172        };
173
174        for entry in dir_iter.filter_map(std::io::Result::ok) {
175            let name = entry.file_name();
176            let fallback_index = found_indices.get(&name);
177            if let Some(theme) = Theme::from_path(entry.path(), fallback_index) {
178                if fallback_index.is_none() {
179                    found_indices.insert(name.clone(), theme.index.clone());
180                }
181                let name = name.to_string_lossy().to_string();
182                icon_themes.entry(name).or_default().push(theme);
183            } else if entry.path().is_dir() {
184                to_revisit.push(entry);
185            }
186        }
187    }
188
189    for entry in to_revisit {
190        let name = entry.file_name();
191        let fallback_index = found_indices.get(&name);
192        if let Some(theme) = Theme::from_path(entry.path(), fallback_index) {
193            let name = name.to_string_lossy().to_string();
194            icon_themes.entry(name).or_default().push(theme);
195        }
196    }
197
198    icon_themes
199}
200
201impl Theme {
202    pub(crate) fn from_path<P: AsRef<Path>>(path: P, index: Option<&PathBuf>) -> Option<Self> {
203        let path = path.as_ref();
204
205        let has_index = path.join("index.theme").exists() || index.is_some();
206
207        if !has_index || !path.is_dir() {
208            return None;
209        }
210
211        let path = ThemePath(path.into());
212
213        match (index, path.index()) {
214            (Some(index), _) => Some(Theme {
215                path,
216                index: index.clone(),
217            }),
218            (None, Ok(index)) => Some(Theme { path, index }),
219            _ => None,
220        }
221    }
222}
223
224#[cfg(test)]
225mod test {
226    use crate::THEMES;
227    use speculoos::prelude::*;
228    use std::path::PathBuf;
229
230    #[test]
231    fn get_one_icon() {
232        let themes = THEMES.get("Adwaita").unwrap();
233        println!(
234            "{:?}",
235            themes.iter().find_map(|t| {
236                let file = super::read_ini_theme(&t.index).ok()?;
237                let file = std::str::from_utf8(file.as_ref()).ok()?;
238                t.try_get_icon_exact_size(file, "edit-delete-symbolic", 24, 1, false)
239            })
240        );
241    }
242
243    #[test]
244    fn should_get_png_first() {
245        let themes = THEMES.get("hicolor").unwrap();
246        let icon = themes.iter().find_map(|t| {
247            let file = super::read_ini_theme(&t.index).ok()?;
248            let file = std::str::from_utf8(file.as_ref()).ok()?;
249            t.try_get_icon_exact_size(file, "blueman", 24, 1, true)
250        });
251        assert_that!(icon).is_some().is_equal_to(PathBuf::from(
252            "/usr/share/icons/hicolor/scalable/apps/blueman.svg",
253        ));
254    }
255
256    #[test]
257    fn should_get_svg_first() {
258        let themes = THEMES.get("hicolor").unwrap();
259        let icon = themes.iter().find_map(|t| {
260            let file = super::read_ini_theme(&t.index).ok()?;
261            let file = std::str::from_utf8(file.as_ref()).ok()?;
262            t.try_get_icon_exact_size(file, "blueman", 24, 1, false)
263        });
264        assert_that!(icon).is_some().is_equal_to(PathBuf::from(
265            "/usr/share/icons/hicolor/22x22/apps/blueman.png",
266        ));
267    }
268}