freedesktop_icon_lookup/
lookup.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use either::Either;
5
6use crate::{Error, IconInfo, IconSearch, Result, Theme};
7
8const DEFAULT_THEME: &str = "hicolor";
9
10pub struct Cache {
13 themes: HashMap<String, Vec<Theme>>,
14 pixmaps: HashMap<String, PathBuf>,
15}
16
17pub struct LookupParam<'a> {
19 name: &'a str,
20 theme: Option<&'a str>,
21 size: Option<u16>,
22 scale: Option<u16>,
23}
24
25impl Cache {
26 pub fn new() -> Result<Self> {
29 Ok(Self {
30 themes: HashMap::new(),
31 pixmaps: {
32 let mut pixmaps = HashMap::new();
33 crate::find_dir_icons("/usr/share/pixmaps", |icon_name, path| {
34 pixmaps.insert(icon_name.into(), path);
35 })?;
36 pixmaps
37 },
38 })
39 }
40
41 pub fn themes(&self) -> impl Iterator<Item = &str> + '_ {
43 self.themes.keys().map(|s| s.as_str())
44 }
45
46 pub fn load_default(&mut self) -> Result<()> {
48 self.load(DEFAULT_THEME)
49 }
50
51 pub fn load(&mut self, theme: impl Into<String>) -> Result<()> {
53 let themes_count = self.themes.len();
54 self.load_inner(theme, 0).and_then(|()| {
55 if themes_count == self.themes.len() {
56 Err(Error::ThemeNotFound)
57 } else {
58 Ok(())
59 }
60 })
61 }
62
63 fn load_inner(&mut self, theme: impl Into<String>, depth: usize) -> Result<()> {
64 let theme = theme.into();
65 if self.themes.contains_key(&theme) {
66 return Ok(());
67 }
68
69 if depth > 10 {
71 return Err(Error::CycleDetected);
72 }
73
74 for path in search_dirs() {
75 let path = path.join(&theme);
76 if path.exists() {
77 let t = match Theme::new(&path) {
78 Ok(t) => t,
79 Err(e) => {
80 #[cfg(feature = "log")]
81 log::error!("theme loading failed: {e} at{}", path.display());
82 #[cfg(not(feature = "log"))]
83 let _ = e;
84 continue;
85 }
86 };
87
88 for inherit in t.inherits() {
89 self.load_inner(inherit, depth + 1)?;
90 }
91
92 if t.inherits().iter().all(|x| self.themes.contains_key(x)) {
93 self.themes.entry(theme.clone()).or_default().push(t);
94 } else {
95 #[cfg(feature = "log")]
96 log::warn!(
97 "skipping {theme} as inherited {} was not loaded",
98 t.inherits().join(",")
99 );
100 }
101 }
102 }
103
104 Ok(())
105 }
106
107 pub fn lookup_advanced<'a, F>(
112 &'a self,
113 name: &str,
114 theme: impl Into<Option<&'a str>>,
115 f: F,
116 ) -> Option<PathBuf>
117 where
118 F: FnMut(&[IconInfo]) -> Option<usize> + Copy,
119 {
120 self.lookup_themed(theme.into().unwrap_or(DEFAULT_THEME), name, f, 0)
121 .map(|s| s.path())
122 .or_else(|| self.pixmaps.get(name).cloned())
123 }
124
125 pub fn lookup<'a>(&'a self, name: &str, theme: impl Into<Option<&'a str>>) -> Option<PathBuf> {
128 self.lookup_param(LookupParam::new(name).with_theme(theme.into()))
129 }
130
131 pub fn lookup_param<'a>(&'a self, param: LookupParam<'a>) -> Option<PathBuf> {
134 self.lookup_themed(
135 param.theme.unwrap_or(DEFAULT_THEME),
136 param.name,
137 |infos| {
138 let (icon_size, icon_scale) = (param.size(), param.scale());
139
140 if let Some(idx) = infos
141 .iter()
142 .position(|i| i.directory().is_matches(icon_size, icon_scale))
143 {
144 return Some(idx);
145 }
146
147 if let Some((idx, _)) = infos
148 .iter()
149 .enumerate()
150 .min_by_key(|(_, i)| i.directory().size_distance(icon_size, icon_scale))
151 {
152 return Some(idx);
153 }
154
155 None
156 },
157 0,
158 )
159 .map(|s| s.path())
160 .or_else(|| self.pixmaps.get(param.name).cloned())
161 }
162
163 fn lookup_themed<'a, F>(
164 &'a self,
165 theme: &str,
166 icon_name: &'a str,
167 f: F,
168 depth: usize,
169 ) -> Option<IconSearch<'a>>
170 where
171 F: FnMut(&[IconInfo]) -> Option<usize> + Copy,
172 {
173 if depth > 10 {
175 return None;
176 }
177
178 let themes = self.themes.get(theme)?;
179 for theme in themes {
180 if let Some(search) = theme.icon_search(icon_name, f) {
181 return Some(search);
182 }
183 }
184
185 for theme in themes.iter().flat_map(|t| t.inherits()) {
186 if let Some(search) = self.lookup_themed(theme, icon_name, f, depth + 1) {
187 return Some(search);
188 }
189 }
190
191 None
192 }
193}
194
195impl<'a> LookupParam<'a> {
196 pub fn new(name: &'a str) -> Self {
197 Self {
198 name,
199 theme: None,
200 size: None,
201 scale: None,
202 }
203 }
204
205 pub fn with_theme(mut self, theme: Option<&'a str>) -> Self {
206 self.theme = theme;
207 self
208 }
209
210 pub fn with_size(mut self, size: u16) -> Self {
211 self.size = Some(size);
212 self
213 }
214
215 pub fn with_scale(mut self, scale: u16) -> Self {
216 self.scale = Some(scale);
217 self
218 }
219
220 fn size(&self) -> u16 {
221 self.size.unwrap_or(48)
222 }
223
224 fn scale(&self) -> u16 {
225 self.size.unwrap_or(1)
226 }
227}
228
229fn search_dirs() -> impl Iterator<Item = PathBuf> {
230 use std::iter::once;
231
232 let home_dir = std::env::var("HOME");
233
234 home_dir
235 .as_ref()
236 .map(|var| PathBuf::from(var).join(".icons"))
237 .into_iter()
238 .chain(
240 std::env::var("XDG_DATA_HOME")
241 .map(|var| PathBuf::from(var).join("icons"))
242 .or_else(|_| home_dir.map(|var| PathBuf::from(var).join(".local/share/icons")))
243 .into_iter(),
244 )
245 .chain(if let Ok(dirs) = std::env::var("XDG_DATA_DIRS") {
246 Either::Left(
247 dirs.split(':')
248 .map(|s| Path::new(s).join("icons"))
249 .collect::<Vec<_>>()
250 .into_iter(),
251 )
252 } else {
253 Either::Right(
254 once(PathBuf::from("/usr/share/local/icons"))
255 .chain(once(PathBuf::from("/usr/share/icons"))),
256 )
257 })
258}
259
260pub(crate) fn find_dir_icons<P, F>(path: P, mut f: F) -> Result<()>
261where
262 P: AsRef<Path>,
263 F: FnMut(&str, PathBuf),
264{
265 let path = path.as_ref();
266
267 fn filter_io_errors<T>(r: std::io::Result<T>) -> Result<Option<T>> {
268 use std::io::ErrorKind;
269 match r {
270 Ok(v) => Ok(Some(v)),
271 Err(e) if matches!(e.kind(), ErrorKind::PermissionDenied | ErrorKind::NotFound) => {
272 Ok(None)
273 }
274 Err(source) => Err(Error::TraverseDir { source }),
275 }
276 }
277
278 for e in filter_io_errors(std::fs::read_dir(path))?
279 .into_iter()
280 .flatten()
281 {
282 if let Some(entry) = filter_io_errors(e)? {
283 if filter_io_errors(entry.file_type())?.map_or(true, |f| f.is_dir()) {
284 continue;
285 }
286
287 let path = entry.path();
288 if let Some(icon_name) = path.file_name().and_then(|s| s.to_str()) {
289 let icon_name = &icon_name[0..icon_name.rfind('.').unwrap_or(0)];
290 f(icon_name, entry.path());
291 }
292 }
293 }
294 Ok(())
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn default_search_dirs() {
303 std::env::remove_var("XDG_DATA_HOME");
304 std::env::remove_var("XDG_DATA_DIRS");
305 std::env::set_var("HOME", "/tmp");
306 assert_eq!(
308 vec![
309 "/tmp/.icons",
310 "/tmp/.local/share/icons",
311 "/usr/share/local/icons",
312 "/usr/share/icons"
313 ],
314 search_dirs()
315 .map(|p| p.to_str().unwrap().to_string())
316 .collect::<Vec<_>>()
317 );
318 }
319}