cosmic_freedesktop_icons/lib.rs
1//! # freedesktop-icons
2//!
3//! This crate provides a [freedesktop icon](https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#implementation_notes) lookup implementation.
4//!
5//! It exposes a single lookup function to find icons based on their `name`, `theme`, `size` and `scale`.
6//!
7//! ## Example
8//!
9//! **Simple lookup:**
10//!
11//! The following snippet get an icon from the default 'hicolor' theme
12//! with the default scale (`1`) and the default size (`24`).
13//!
14//! ```rust
15//! # fn main() {
16//! use cosmic_freedesktop_icons::lookup;
17//!
18//! let icon = lookup("firefox").find();
19//! # }
20//!```
21//!
22//! **Complex lookup:**
23//!
24//! If you have specific requirements for your lookup you can use the provided builder functions:
25//!
26//! ```rust
27//! # fn main() {
28//! use cosmic_freedesktop_icons::lookup;
29//!
30//! let icon = lookup("firefox")
31//! .with_size(48)
32//! .with_scale(2)
33//! .with_theme("Arc")
34//! .find();
35//! # }
36//!```
37//! **Cache:**
38//!
39//! If your application is going to repeat the same icon lookups multiple times
40//! you can use the internal cache to improve performance.
41//!
42//! ```rust
43//! # fn main() {
44//! use cosmic_freedesktop_icons::lookup;
45//!
46//! let icon = lookup("firefox")
47//! .with_size(48)
48//! .with_scale(2)
49//! .with_theme("Arc")
50//! .with_cache()
51//! .find();
52//! # }
53//! ```
54use theme::BASE_PATHS;
55
56use crate::cache::{CacheEntry, CACHE};
57use crate::theme::{try_build_icon_path, THEMES};
58use std::io::BufRead;
59use std::path::PathBuf;
60use std::time::Instant;
61
62mod cache;
63mod theme;
64
65/// Return the list of installed themes on the system
66///
67/// ## Example
68/// ```rust,no_run
69/// # fn main() {
70/// use cosmic_freedesktop_icons::list_themes;
71///
72/// let themes: Vec<&str> = list_themes();
73///
74/// assert_eq!(themes, vec![
75/// "Adwaita", "Arc", "Breeze Light", "HighContrast", "Papirus", "Papirus-Dark",
76/// "Papirus-Light", "Breeze", "Breeze Dark", "Breeze", "ePapirus", "ePapirus-Dark", "Hicolor"
77/// ])
78/// # }
79pub fn list_themes() -> Vec<String> {
80 let mut themes = THEMES
81 .values()
82 .flatten()
83 .map(|path| &path.index)
84 .filter_map(|index| {
85 let file = std::fs::File::open(index).ok()?;
86 let mut reader = std::io::BufReader::new(file);
87
88 let mut line = String::new();
89 while let Ok(read) = reader.read_line(&mut line) {
90 if read == 0 {
91 break;
92 }
93
94 if let Some(name) = line.strip_prefix("Name=") {
95 return Some(name.trim().to_owned());
96 }
97
98 line.clear();
99 }
100
101 None
102 })
103 .collect::<Vec<_>>();
104 themes.dedup();
105 themes
106}
107
108/// Return the default GTK theme if set.
109///
110/// ## Example
111/// ```rust, no_run
112/// use freedesktop_icons::default_theme_gtk;
113///
114/// let theme = default_theme_gtk();
115///
116/// assert_eq!(Some("Adwaita"), theme);
117/// ```
118pub fn default_theme_gtk() -> Option<String> {
119 // Calling gsettings is the simplest way to retrieve the default icon theme without adding
120 // GTK as a dependency. There seems to be several ways to set the default GTK theme
121 // including a file in XDG_CONFIG_HOME as well as an env var. Gsettings is the most
122 // straightforward method.
123 let gsettings = std::process::Command::new("gsettings")
124 .args(["get", "org.gnome.desktop.interface", "icon-theme"])
125 .output()
126 .ok()?;
127
128 // Only return the theme if it's in the cache.
129 if gsettings.status.success() {
130 let name = String::from_utf8(gsettings.stdout).ok()?;
131 let name = name.trim().trim_matches('\'');
132 THEMES.get(name).and_then(|themes| {
133 themes.first().and_then(|path| {
134 let file = std::fs::File::open(&path.index).ok()?;
135 let mut reader = std::io::BufReader::new(file);
136
137 let mut line = String::new();
138 while let Ok(read) = reader.read_line(&mut line) {
139 if read == 0 {
140 break;
141 }
142
143 if let Some(name) = line.strip_prefix("Name=") {
144 return Some(name.trim().to_owned());
145 }
146
147 line.clear();
148 }
149
150 None
151 })
152 })
153 } else {
154 None
155 }
156}
157
158/// The lookup builder struct, holding all the lookup query parameters.
159pub struct LookupBuilder<'a> {
160 name: &'a str,
161 cache: bool,
162 force_svg: bool,
163 scale: u16,
164 size: u16,
165 theme: &'a str,
166}
167
168/// Build an icon lookup for the given icon name.
169///
170/// ## Example
171/// ```rust
172/// # fn main() {
173/// use cosmic_freedesktop_icons::lookup;
174///
175/// let icon = lookup("firefox").find();
176/// # }
177pub fn lookup(name: &str) -> LookupBuilder {
178 LookupBuilder::new(name)
179}
180
181impl<'a> LookupBuilder<'a> {
182 /// Restrict the lookup to the given icon size.
183 ///
184 /// ## Example
185 /// ```rust
186 /// # fn main() {
187 /// use cosmic_freedesktop_icons::lookup;
188 ///
189 /// let icon = lookup("firefox")
190 /// .with_size(48)
191 /// .find();
192 /// # }
193 #[inline]
194 pub fn with_size(mut self, size: u16) -> Self {
195 self.size = size;
196 self
197 }
198
199 /// Restrict the lookup to the given scale.
200 ///
201 /// ## Example
202 /// ```rust
203 /// # fn main() {
204 /// use cosmic_freedesktop_icons::lookup;
205 ///
206 /// let icon = lookup("firefox")
207 /// .with_scale(2)
208 /// .find();
209 /// # }
210 #[inline]
211 pub fn with_scale(mut self, scale: u16) -> Self {
212 self.scale = scale;
213 self
214 }
215
216 /// Add the given theme to the current lookup :
217 /// ## Example
218 /// ```rust
219 /// # fn main() {
220 /// use cosmic_freedesktop_icons::lookup;
221 ///
222 /// let icon = lookup("firefox")
223 /// .with_theme("Papirus")
224 /// .find();
225 /// # }
226 #[inline]
227 pub fn with_theme<'b: 'a>(mut self, theme: &'b str) -> Self {
228 self.theme = theme;
229 self
230 }
231
232 /// Store the result of the lookup in cache, subsequent
233 /// lookup will first try to get the cached icon.
234 /// This can drastically increase lookup performances for application
235 /// that repeat the same lookups, an application launcher for instance.
236 ///
237 /// ## Example
238 /// ```rust
239 /// # fn main() {
240 /// use cosmic_freedesktop_icons::lookup;
241 ///
242 /// let icon = lookup("firefox")
243 /// .with_scale(2)
244 /// .with_cache()
245 /// .find();
246 /// # }
247 #[inline]
248 pub fn with_cache(mut self) -> Self {
249 self.cache = true;
250 self
251 }
252
253 /// By default [`find`] will prioritize Png over Svg icon.
254 /// Use this if you need to prioritize Svg icons. This could be useful
255 /// if you need a modifiable icon, to match a user theme for instance.
256 ///
257 /// ## Example
258 /// ```rust
259 /// # fn main() {
260 /// use cosmic_freedesktop_icons::lookup;
261 ///
262 /// let icon = lookup("firefox")
263 /// .force_svg()
264 /// .find();
265 /// # }
266 #[inline]
267 pub fn force_svg(mut self) -> Self {
268 self.force_svg = true;
269 self
270 }
271
272 /// Execute the current lookup
273 /// if no icon is found in the current theme fallback to
274 /// `/usr/share/icons/hicolor` theme and then to `/usr/share/pixmaps`.
275 #[inline]
276 pub fn find(self) -> Option<PathBuf> {
277 if self.name.is_empty() {
278 return None;
279 }
280
281 // Lookup for an icon in the given theme and fallback to 'hicolor' default theme
282 self.lookup_in_theme()
283 }
284
285 fn new<'b: 'a>(name: &'b str) -> Self {
286 Self {
287 name,
288 cache: false,
289 force_svg: false,
290 scale: 1,
291 size: 24,
292 theme: "hicolor",
293 }
294 }
295
296 // Recursively lookup for icon in the given theme and its parents
297 fn lookup_in_theme(&self) -> Option<PathBuf> {
298 // If cache is activated, attempt to get the icon there first
299 // If the icon was previously search but not found, we return
300 // `None` early, otherwise, attempt to perform a lookup
301 if self.cache {
302 match self.cache_lookup(self.theme) {
303 CacheEntry::Found(icon) => return Some(icon),
304 CacheEntry::NotFound(last_check)
305 if last_check.duration_since(Instant::now()).as_secs() < 5 =>
306 {
307 return None
308 }
309 _ => (),
310 }
311 }
312
313 // Then lookup in the given theme
314 THEMES
315 .get(self.theme)
316 .or_else(|| THEMES.get("hicolor"))
317 .and_then(|icon_themes| {
318 let icon = icon_themes
319 .iter()
320 .find_map(|theme| {
321 theme.try_get_icon(self.name, self.size, self.scale, self.force_svg)
322 })
323 .or_else(|| {
324 // Fallback to the parent themes recursively
325 let mut parents = icon_themes
326 .iter()
327 .flat_map(|t| {
328 let Ok(file) = theme::read_ini_theme(&t.index) else {
329 return Vec::new();
330 };
331
332 let Ok(file) = std::str::from_utf8(file.as_ref()) else {
333 return Vec::new();
334 };
335
336 t.inherits(file)
337 .into_iter()
338 .map(String::from)
339 .collect::<Vec<String>>()
340 })
341 .collect::<Vec<_>>();
342 parents.dedup();
343 parents.into_iter().find_map(|parent| {
344 THEMES.get(&parent).and_then(|parent| {
345 parent.iter().find_map(|t| {
346 t.try_get_icon(self.name, self.size, self.scale, self.force_svg)
347 })
348 })
349 })
350 })
351 .or_else(|| {
352 THEMES.get("hicolor").and_then(|icon_themes| {
353 icon_themes.iter().find_map(|theme| {
354 theme.try_get_icon(self.name, self.size, self.scale, self.force_svg)
355 })
356 })
357 })
358 .or_else(|| {
359 for theme_base_dir in BASE_PATHS.iter() {
360 if let Some(icon) =
361 try_build_icon_path(self.name, theme_base_dir, self.force_svg)
362 {
363 return Some(icon);
364 }
365 }
366 None
367 })
368 .or_else(|| {
369 try_build_icon_path(self.name, "/usr/share/pixmaps", self.force_svg)
370 })
371 .or_else(|| {
372 let p = PathBuf::from(&self.name);
373 if let (Some(name), Some(parent)) = (p.file_stem(), p.parent()) {
374 try_build_icon_path(&name.to_string_lossy(), parent, self.force_svg)
375 } else {
376 None
377 }
378 });
379
380 if self.cache {
381 self.store(self.theme, icon)
382 } else {
383 icon
384 }
385 })
386 }
387
388 #[inline]
389 pub fn cache_clear(&mut self) {
390 CACHE.clear();
391 }
392
393 #[inline]
394 pub fn cache_reset_none(&mut self) {
395 CACHE.reset_none();
396 }
397
398 #[inline]
399 fn cache_lookup(&self, theme: &str) -> CacheEntry {
400 CACHE.get(theme, self.size, self.scale, self.name)
401 }
402
403 #[inline]
404 fn store(&self, theme: &str, icon: Option<PathBuf>) -> Option<PathBuf> {
405 CACHE.insert(theme, self.size, self.scale, self.name, &icon);
406 icon
407 }
408}
409
410// WARNING: these test are highly dependent on your installed icon-themes.
411// If you want to run them, make sure you have 'Papirus' and 'Arc' icon-themes installed.
412#[cfg(test)]
413#[cfg(feature = "local_tests")]
414mod test {
415 use crate::{lookup, CacheEntry, CACHE};
416 use speculoos::prelude::*;
417 use std::path::PathBuf;
418
419 #[test]
420 fn simple_lookup() {
421 let firefox = lookup("firefox").find();
422
423 asserting!("Lookup with no parameters should return an existing icon")
424 .that(&firefox)
425 .is_some()
426 .is_equal_to(PathBuf::from(
427 "/usr/share/icons/hicolor/22x22/apps/firefox.png",
428 ));
429 }
430
431 #[test]
432 fn theme_lookup() {
433 let firefox = lookup("firefox").with_theme("Papirus").find();
434
435 asserting!("Lookup with no parameters should return an existing icon")
436 .that(&firefox)
437 .is_some()
438 .is_equal_to(PathBuf::from(
439 "/usr/share/icons/Papirus/24x24/apps/firefox.svg",
440 ));
441 }
442
443 #[test]
444 fn should_fallback_to_parent_theme() {
445 let icon = lookup("video-single-display-symbolic")
446 .with_theme("Arc")
447 .find();
448
449 asserting!("Lookup for an icon in the Arc theme should find the icon in its parent")
450 .that(&icon)
451 .is_some()
452 .is_equal_to(PathBuf::from(
453 "/usr/share/icons/Adwaita/symbolic/devices/video-single-display-symbolic.svg",
454 ));
455 }
456
457 #[test]
458 fn should_fallback_to_pixmaps_utlimately() {
459 let archlinux_logo = lookup("archlinux-logo")
460 .with_size(16)
461 .with_scale(1)
462 .with_theme("Papirus")
463 .find();
464
465 asserting!("When lookup fail in theme, icon should be found in '/usr/share/pixmaps'")
466 .that(&archlinux_logo)
467 .is_some()
468 .is_equal_to(PathBuf::from("/usr/share/pixmaps/archlinux-logo.png"));
469 }
470
471 #[test]
472 fn compare_to_linincon_with_theme() {
473 let lin_wireshark = linicon::lookup_icon("wireshark")
474 .next()
475 .unwrap()
476 .unwrap()
477 .path;
478
479 let wireshark = lookup("wireshark")
480 .with_size(16)
481 .with_scale(1)
482 .with_theme("Papirus")
483 .find();
484
485 asserting!("Given the same input parameter, lookup should output be the same as linincon")
486 .that(&wireshark)
487 .is_some()
488 .is_equal_to(lin_wireshark);
489 }
490
491 #[test]
492 fn should_not_attempt_to_lookup_a_not_found_cached_icon() {
493 let not_found = lookup("not-found").with_cache().find();
494
495 assert_that!(not_found).is_none();
496
497 let expected_cache_result = CACHE.get("hicolor", 24, 1, "not-found");
498
499 asserting!("When lookup fails a first time, subsequent attempts should fail from cache")
500 .that(&expected_cache_result)
501 .is_equal_to(CacheEntry::NotFound);
502 }
503}