2022-05-13 11:16:09 +02:00
//! # freedesktop-icons
2022-05-12 23:12:06 +02:00
//!
//! This crate provides a [freedesktop icon](https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#implementation_notes) lookup implementation.
//!
2022-05-13 11:28:56 +02:00
//! It exposes a single lookup function to find icons based on their `name`, `theme`, `size` and `scale`.
2022-05-12 23:12:06 +02:00
//!
//! ## Example
//!
//! **Simple lookup:**
//!
//! The following snippet get an icon from the default 'hicolor' theme
//! with the default scale (`1`) and the default size (`24`).
//!
//! ```rust
//! # fn main() {
//! use freedesktop_icons::lookup;
//!
//! let icon = lookup("firefox").find();
//! # }
//!```
//!
//! **Complex lookup:**
//!
2022-05-13 11:28:56 +02:00
//! If you have specific requirements for your lookup you can use the provided builder functions:
2022-05-12 23:12:06 +02:00
//!
//! ```rust
//! # fn main() {
//! use freedesktop_icons::lookup;
//!
//! let icon = lookup("firefox")
//! .with_size(48)
//! .with_scale(2)
//! .with_theme("Arc")
//! .find();
//! # }
//!```
2022-05-13 08:00:52 +02:00
//! **Cache:**
//!
2022-05-13 11:16:09 +02:00
//! If your application is going to repeat the same icon lookups multiple times
2022-05-13 08:00:52 +02:00
//! you can use the internal cache to improve performance.
//!
//! ```rust
//! # fn main() {
//! use freedesktop_icons::lookup;
//!
//! let icon = lookup("firefox")
//! .with_size(48)
//! .with_scale(2)
//! .with_theme("Arc")
//! .with_cache()
//! .find();
//! # }
2022-05-13 11:16:09 +02:00
//! ```
2022-11-22 17:49:56 -05:00
use theme ::{ Theme , BASE_PATHS } ;
2022-05-13 10:11:02 +02:00
use crate ::cache ::{ CacheEntry , CACHE } ;
2022-05-12 15:34:02 +02:00
use crate ::theme ::{ try_build_icon_path , THEMES } ;
2022-05-12 14:41:08 +02:00
use std ::path ::PathBuf ;
2022-05-12 23:12:06 +02:00
mod cache ;
mod theme ;
2022-05-12 15:34:02 +02:00
2022-05-13 08:00:52 +02:00
/// Return the list of installed themes on the system
///
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::list_themes;
///
/// let themes: Vec<&str> = list_themes();
///
/// assert_eq!(themes, vec![
/// "Adwaita", "Arc", "Breeze Light", "HighContrast", "Papirus", "Papirus-Dark",
/// "Papirus-Light", "Breeze", "Breeze Dark", "Breeze", "ePapirus", "ePapirus-Dark", "Hicolor"
/// ])
/// # }
pub fn list_themes ( ) -> Vec < & 'static str > {
THEMES
. values ( )
. map ( | path | & path . index )
. filter_map ( | index | {
index
. section ( Some ( " Icon Theme " ) )
. and_then ( | section | section . get ( " Name " ) )
} )
. collect ( )
}
2022-05-12 23:12:06 +02:00
/// The lookup builder struct, holding all the lookup query parameters.
2022-05-12 15:34:02 +02:00
pub struct LookupBuilder < ' a > {
name : & ' a str ,
2022-05-13 08:00:52 +02:00
cache : bool ,
2022-05-23 20:36:24 +02:00
force_svg : bool ,
2022-05-12 15:34:02 +02:00
scale : u16 ,
size : u16 ,
2022-05-13 10:11:02 +02:00
theme : & ' a str ,
2022-05-12 15:34:02 +02:00
}
2022-05-12 23:12:06 +02:00
/// Build an icon lookup for the given icon name.
///
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::lookup;
///
/// let icon = lookup("firefox").find();
/// # }
pub fn lookup ( name : & str ) -> LookupBuilder {
LookupBuilder ::new ( name )
}
2022-05-12 15:34:02 +02:00
impl < ' a > LookupBuilder < ' a > {
2022-05-12 23:12:06 +02:00
/// Restrict the lookup to the given icon size.
///
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::lookup;
///
/// let icon = lookup("firefox")
/// .with_size(48)
/// .find();
/// # }
2022-05-17 07:11:19 +02:00
pub fn with_size ( mut self , size : u16 ) -> Self {
self . size = size ;
2022-05-12 15:34:02 +02:00
self
2022-05-12 14:41:08 +02:00
}
2022-05-12 23:12:06 +02:00
/// Restrict the lookup to the given scale.
///
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::lookup;
///
/// let icon = lookup("firefox")
/// .with_scale(2)
/// .find();
/// # }
2022-05-17 07:11:19 +02:00
pub fn with_scale ( mut self , scale : u16 ) -> Self {
self . scale = scale ;
2022-05-12 15:34:02 +02:00
self
}
2022-05-12 23:12:06 +02:00
/// Add the given theme to the current lookup :
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::lookup;
///
/// let icon = lookup("firefox")
/// .with_theme("Papirus")
/// .find();
/// # }
2022-05-12 15:34:02 +02:00
pub fn with_theme < ' b : ' a > ( mut self , theme : & ' b str ) -> Self {
2022-05-13 10:11:02 +02:00
self . theme = theme ;
2022-05-12 15:34:02 +02:00
self
}
2022-05-13 08:00:52 +02:00
/// Store the result of the lookup in cache, subsequent
2022-05-13 11:28:56 +02:00
/// lookup will first try to get the cached icon.
2022-05-13 08:00:52 +02:00
/// This can drastically increase lookup performances for application
/// that repeat the same lookups, an application launcher for instance.
///
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::lookup;
///
/// let icon = lookup("firefox")
/// .with_scale(2)
/// .with_cache()
/// .find();
/// # }
pub fn with_cache ( mut self ) -> Self {
self . cache = true ;
self
}
2022-05-23 20:36:24 +02:00
/// By default [`find`] will prioritize Png over Svg icon.
/// Use this if you need to prioritize Svg icons. This could be useful
/// if you need a modifiable icon, to match a user theme for instance.
///
/// ## Example
/// ```rust
/// # fn main() {
/// use freedesktop_icons::lookup;
///
/// let icon = lookup("firefox")
/// .force_svg()
/// .find();
/// # }
pub fn force_svg ( mut self ) -> Self {
self . force_svg = true ;
self
}
2022-05-12 23:12:06 +02:00
/// Execute the current lookup
/// if no icon is found in the current theme fallback to
2022-05-13 11:28:56 +02:00
/// `/usr/share/icons/hicolor` theme and then to `/usr/share/pixmaps`.
2022-05-12 23:12:06 +02:00
pub fn find ( self ) -> Option < PathBuf > {
// Lookup for an icon in the given theme and fallback to 'hicolor' default theme
2022-05-13 10:11:02 +02:00
self . lookup_in_theme ( )
2022-05-12 23:12:06 +02:00
}
2022-05-12 15:34:02 +02:00
fn new < ' b : ' a > ( name : & ' b str ) -> Self {
Self {
name ,
2022-05-13 08:00:52 +02:00
cache : false ,
2022-05-23 20:36:24 +02:00
force_svg : false ,
2022-05-12 15:34:02 +02:00
scale : 1 ,
size : 24 ,
2022-05-13 10:11:02 +02:00
theme : " hicolor " ,
2022-05-12 14:41:08 +02:00
}
}
2022-05-12 23:12:06 +02:00
// Recursively lookup for icon in the given theme and its parents
2022-05-13 10:11:02 +02:00
fn lookup_in_theme ( & self ) -> Option < PathBuf > {
2022-05-13 08:00:52 +02:00
// If cache is activated, attempt to get the icon there first
2022-05-13 10:11:02 +02:00
// If the icon was previously search but not found, we return
// `None` early, otherwise, attempt to perform a lookup
2022-05-13 08:00:52 +02:00
if self . cache {
2022-05-13 10:11:02 +02:00
match self . cache_lookup ( self . theme ) {
CacheEntry ::Found ( icon ) = > {
return Some ( icon ) ;
}
CacheEntry ::NotFound = > {
return None ;
}
CacheEntry ::Unknown = > { }
} ;
2022-05-12 15:34:02 +02:00
}
2022-05-13 08:00:52 +02:00
// Then lookup in the given theme
2022-05-13 10:11:02 +02:00
THEMES . get ( self . theme ) . and_then ( | icon_theme | {
2022-05-13 08:00:52 +02:00
let icon = icon_theme
2022-05-23 20:36:24 +02:00
. try_get_icon ( self . name , self . size , self . scale , self . force_svg )
2022-05-12 23:12:06 +02:00
. or_else ( | | {
2022-05-13 08:00:52 +02:00
// Fallback to the parent themes recursively
icon_theme . inherits ( ) . into_iter ( ) . find_map ( | parent | {
THEMES . get ( parent ) . and_then ( | parent | {
2022-05-23 20:36:24 +02:00
parent . try_get_icon ( self . name , self . size , self . scale , self . force_svg )
2022-05-13 08:00:52 +02:00
} )
} )
2022-05-13 10:11:02 +02:00
} )
. or_else ( | | {
2022-11-22 17:49:56 -05:00
for theme_base_dir in BASE_PATHS . iter ( ) {
let theme = Theme ::from_path ( theme_base_dir . join ( " hicolor " ) ) ;
if let Some ( icon ) = theme . and_then ( | theme | {
theme . try_get_icon ( self . name , self . size , self . scale , self . force_svg )
} ) {
return Some ( icon ) ;
}
}
None
2022-05-13 10:11:02 +02:00
} )
2022-11-22 17:49:56 -05:00
. or_else ( | | try_build_icon_path ( self . name , " /usr/share/pixmaps " , self . force_svg ) )
. or_else ( | | {
let p = PathBuf ::from ( & self . name ) ;
if let ( Some ( name ) , Some ( parent ) ) = ( p . file_name ( ) , p . parent ( ) ) {
try_build_icon_path ( & name . to_string_lossy ( ) , parent , self . force_svg )
} else {
None
}
} ) ;
2022-05-13 08:00:52 +02:00
if self . cache {
2022-05-13 10:11:02 +02:00
self . store ( self . theme , icon )
2022-05-13 08:00:52 +02:00
} else {
icon
}
} )
}
#[ inline ]
2022-05-13 10:11:02 +02:00
fn cache_lookup ( & self , theme : & str ) -> CacheEntry {
2022-05-13 08:00:52 +02:00
CACHE . get ( theme , self . size , self . scale , self . name )
}
#[ inline ]
fn store ( & self , theme : & str , icon : Option < PathBuf > ) -> Option < PathBuf > {
2022-05-13 10:11:02 +02:00
CACHE . insert ( theme , self . size , self . scale , self . name , & icon ) ;
icon
2022-05-12 15:34:02 +02:00
}
2022-05-12 14:41:08 +02:00
}
2022-05-13 10:11:02 +02:00
// WARNING: these test are highly dependent on your installed icon-themes.
// If you want to run them, make sure you have 'Papirus' and 'Arc' icon-themes installed.
2022-05-12 14:41:08 +02:00
#[ cfg(test) ]
mod test {
2022-05-13 10:11:02 +02:00
use crate ::{ lookup , CacheEntry , CACHE } ;
2022-05-12 14:41:08 +02:00
use speculoos ::prelude ::* ;
2022-05-12 15:34:02 +02:00
use std ::path ::PathBuf ;
2022-05-12 14:41:08 +02:00
#[ test ]
2022-05-12 15:34:02 +02:00
fn simple_lookup ( ) {
2022-05-12 23:12:06 +02:00
let firefox = lookup ( " firefox " ) . find ( ) ;
2022-05-13 08:01:21 +02:00
asserting! ( " Lookup with no parameters should return an existing icon " )
. that ( & firefox )
. is_some ( )
. is_equal_to ( PathBuf ::from (
" /usr/share/icons/hicolor/22x22/apps/firefox.png " ,
) ) ;
}
#[ test ]
2022-05-13 10:11:02 +02:00
fn theme_lookup ( ) {
let firefox = lookup ( " firefox " ) . with_theme ( " Papirus " ) . find ( ) ;
asserting! ( " Lookup with no parameters should return an existing icon " )
. that ( & firefox )
. is_some ( )
. is_equal_to ( PathBuf ::from (
" /usr/share/icons/Papirus/24x24/apps/firefox.svg " ,
) ) ;
}
#[ test ]
fn should_fallback_to_parent_theme ( ) {
2022-05-13 08:01:21 +02:00
let icon = lookup ( " video-single-display-symbolic " )
. with_theme ( " Arc " )
. find ( ) ;
asserting! ( " Lookup for an icon in the Arc theme should find the icon in its parent " )
. that ( & icon )
. is_some ( )
. is_equal_to ( PathBuf ::from (
" /usr/share/icons/Adwaita/scalable/devices/video-single-display-symbolic.svg " ,
) ) ;
2022-05-12 15:34:02 +02:00
}
2022-05-13 10:11:02 +02:00
#[ test ]
fn should_fallback_to_pixmaps_utlimately ( ) {
let archlinux_logo = lookup ( " archlinux-logo " )
. with_size ( 16 )
. with_scale ( 1 )
. with_theme ( " Papirus " )
. find ( ) ;
asserting! ( " When lookup fail in theme, icon should be found in '/usr/share/pixmaps' " )
. that ( & archlinux_logo )
. is_some ( )
. is_equal_to ( PathBuf ::from ( " /usr/share/pixmaps/archlinux-logo.png " ) ) ;
}
2022-05-12 15:34:02 +02:00
#[ test ]
fn compare_to_linincon_with_theme ( ) {
2022-05-12 14:41:08 +02:00
let lin_wireshark = linicon ::lookup_icon ( " wireshark " )
. next ( )
. unwrap ( )
. unwrap ( )
. path ;
2022-05-12 15:34:02 +02:00
let wireshark = lookup ( " wireshark " )
. with_size ( 16 )
. with_scale ( 1 )
. with_theme ( " Papirus " )
2022-05-12 23:12:06 +02:00
. find ( ) ;
2022-05-12 14:41:08 +02:00
2022-12-18 17:53:41 +01:00
asserting! ( " Given the same input parameter, lookup should output be the same as linincon " )
2022-05-13 08:01:21 +02:00
. that ( & wireshark )
. is_some ( )
. is_equal_to ( lin_wireshark ) ;
2022-05-12 14:41:08 +02:00
}
#[ test ]
2022-05-13 10:11:02 +02:00
fn should_not_attempt_to_lookup_a_not_found_cached_icon ( ) {
let not_found = lookup ( " not-found " ) . with_cache ( ) . find ( ) ;
2022-05-12 14:41:08 +02:00
2022-05-13 10:11:02 +02:00
assert_that! ( not_found ) . is_none ( ) ;
let expected_cache_result = CACHE . get ( " hicolor " , 24 , 1 , " not-found " ) ;
asserting! ( " When lookup fails a first time, subsequent attempts should fail from cache " )
. that ( & expected_cache_result )
. is_equal_to ( CacheEntry ::NotFound ) ;
2022-05-12 14:41:08 +02:00
}
}