Skip to main content

xdgkit/
icon_finder.rs

1/*!
2# Icon finder
3
4This is the rustification of the example psuedo code for finding icons a.k.a "the algorithm described in the [Icon Theme Specification](https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup)"
5
6to find one single icon use:
7```
8use xdgkit::icon_finder::find_icon;
9let icon = find_icon("firefox".to_string(), 48, 1);
10```
11to find multiple icons use:
12```
13use xdgkit::icon_theme::IconTheme;
14use xdgkit::icon_finder::{multiple_find_icon, generate_dir_list, user_theme, DirList};
15
16let dir_list_vector = generate_dir_list();
17let mut theme = user_theme(dir_list_vector.clone());
18if theme.is_none() {
19    theme = Some(IconTheme::empty());
20}
21let theme:IconTheme = theme.unwrap();
22let list:Vec<String> = vec![
23    "firefox".to_string(),
24    "mypaint".to_string(),
25    "kate".to_string(),
26    "geany".to_string(),
27];
28for name in list.clone() {
29    let icon = match multiple_find_icon(name.clone(), 48, 1, dir_list_vector.clone(), theme.clone()) {
30        Some(i) => i,
31        None => continue,
32    };
33    let icon = match icon.to_str() {
34        Some(i) => {
35            println!("found:{}", i);
36            i
37        },
38        None => {
39            println!("Did not find:{}", name.as_str());
40            continue
41        },
42    };
43}
44*/
45
46// icon_finder.rs
47// Rusified in 2021 Copyright Israel Dahl. All rights reserved.
48// 
49//        /VVVV\
50//      /V      V\
51//    /V          V\
52//   /      0 0     \
53//   \|\|\</\/\>/|/|/
54//        \_/\_/
55// 
56// This program is free software; you can redistribute it and/or modify
57// it under the terms of the GNU General Public License version 2 as
58// published by the Free Software Foundation.
59//
60// This program is distributed in the hope that it will be useful,
61// but WITHOUT ANY WARRANTY; without even the implied warranty of
62// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
63// GNU General Public License for more details.
64// 
65// You should have received a copy of the GNU General Public License
66// along with this program; if not, write to the Free Software
67// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
68
69use crate::icon_theme::*;
70use crate::basedir::*;
71use std::path::PathBuf;
72use std::path::Path;
73extern crate tini;
74use tini::Ini;
75use std::collections::HashMap;
76
77/// Our icon file extensions
78const EXTENTIONS:[&str; 3] = [".png", ".svg", ".xpm"];
79/// the index file for themes
80const INDEX_FILE:&str = "index.theme";
81
82/// A simple structure to hold directory and theme list.
83///
84/// The reason this is needed is because the **theme** name is **not** the same as the **directory name** `:-P`
85#[derive(Debug, Clone)]
86pub struct DirList {
87    /// the PathBuf
88    pub dirs:Vec<PathBuf>,
89    /// The name of the theme
90    pub theme:String,
91}
92impl DirList {
93    /// In a nutshell, return `dir.push("/index.theme")`
94    pub fn index(&self) -> PathBuf {
95        let mut return_value = self.dirs[0].to_owned();
96        return_value.push(INDEX_FILE);
97        return_value
98    }
99}
100/// Make the list of `DirList` structures by reading the `$XDG_DATA_DIRS/icons`
101pub fn generate_dir_list() -> Vec<DirList>{
102    let mut return_value:Vec< DirList> = vec![];
103    
104    // make our directory of icons
105    let directory_vec = icon_dirs_vector();
106    //println!("search");
107
108    let mut themes = HashMap::<String, Vec<PathBuf>>::new(); // theme dirname -> path to topmost directory of the theme
109    // find all theme dir basenames, put those in a hashmap, then make a DirList each to traverse all of the directories that are named the same.
110    for directory in directory_vec {
111        let path = Path::new(directory.as_str());
112        if path.is_dir() {
113            let dir_path = std::fs::read_dir(path);
114            if let Ok(dp) = dir_path {
115                for entity in dp.flatten() {
116                    //println!("entity:{:?}", entity.file_name());
117                    let return_path = entity.path();
118                    let basename = entity.file_name();
119                    let theme_name = match basename.to_str() {
120                        Some(v) => v,
121                        None => continue
122                    };
123                    themes.entry(theme_name.to_string()).and_modify(|paths| paths.push(return_path.clone())).or_insert(Vec::<PathBuf>::from([return_path.clone()]));
124                }
125            }
126        }
127    }
128
129    for (_theme_name, ref mut return_paths) in themes {
130        let mut index_theme_path = None;
131        let mut index_theme_index = 0;
132        for (i, return_path) in return_paths.iter().enumerate() {
133            let mut index = return_path.clone();
134            index.push(INDEX_FILE);
135            if index.is_file() {
136                index_theme_path = Some(index);
137                index_theme_index = i;
138            }
139        }
140        if index_theme_path.is_none() {
141            continue
142        }
143        // make sure that the entry with index.theme in it is in front
144        return_paths.swap(0, index_theme_index);
145        let index_theme_path = index_theme_path.unwrap();
146        let theme = IconTheme::new(index_theme_path.to_owned().to_str().unwrap().to_string());
147        let theme_name = match theme.name {
148            Some(name) => name.to_owned(),
149            None => continue,
150        };
151        //println!("index file:{}", index_theme_path.display());
152        //println!(" dirs: {:?}", return_paths);
153        //println!("{:?}",index.to_owned());
154        return_value.push(
155                            DirList{
156                                theme:theme_name.to_owned(),
157                                dirs:return_paths.clone(),
158                            }
159                        );
160    }
161    //return_value.sort();
162    return_value
163}
164
165fn get_theme(place: &str, dir_list_vector:Vec<DirList>) -> Option<DirList> {
166    for theme in &dir_list_vector {
167        if theme.theme == place {
168            return Some(theme.clone())
169        }
170    }
171    // maybe that's actually trying to find PLACE as a filename? That sounds weird.
172    for theme in dir_list_vector {
173        for dir in theme.dirs.iter() {
174            if let Some(file_name) = dir.file_name() {
175                if file_name == place {
176                    return Some(theme.clone())
177                }
178            }
179        }
180    }
181    return None
182}
183
184fn check_user_config(file:&str, section:&str, item:&str, dir_list_vector:Vec<DirList>) -> Option<IconTheme> {
185    let conf:String = match config_home() {
186        Ok(conf) => format!("{}/{}",conf, file),
187        Err(e) => {
188            println!("Error:{}", e);
189            return None
190        },
191    };
192    if Path::new(conf.as_str()).is_file() {
193        let test_ini = Ini::from_file(&conf);
194        if let Ok(conf) = test_ini {
195            let theme:Option<String> = conf.get(section,item);
196            //println!("{:?}", theme.clone());
197            if let Some(themed) = theme {
198                let theme_file:PathBuf = match get_theme(
199                                                 &themed,
200                                                 dir_list_vector.
201                                                          clone()
202                                                  ).map(|theme| theme.index()) {
203                     Some(theme_file) =>theme_file,
204                     None =>PathBuf::new(),
205                };
206                if theme_file.exists() {
207                     return Some(IconTheme::from_pathbuff(theme_file));
208                }
209            }
210        }
211    }
212    None
213}
214/// This function looks in the ini files of KDE and GTK to find the icon theme!
215pub fn user_theme(dir_list_vector:Vec<DirList>) -> Option<IconTheme> {
216    let theme_dir = icon_dirs();
217    if theme_dir.is_ok() {
218        // let us look for the user icon theme now that we have directories to look in 
219        if let Some(kde) = check_user_config("kdeglobals",
220                                            "Icons",
221                                            "Theme",
222                                            dir_list_vector.clone()) {
223            return Some(kde);
224        };
225        if let Some(gtk) = check_user_config("gtk-3.0/settings.ini", 
226                                          "Settings",
227                                          "gtk-icon-theme-name",
228                                          dir_list_vector.clone()) {
229            return Some(gtk);
230        };
231        if let Some(gtk) = check_user_config("gtk-4.0/settings.ini", 
232                                          "Settings",
233                                          "gtk-icon-theme-name",
234                                          dir_list_vector.clone()) {
235            return Some(gtk);
236        };
237    }
238    // Default to `hicolor` theme if we can't figure it out
239    let theme_file:PathBuf = match get_theme("hicolor", dir_list_vector).map(|theme| theme.index()) {
240        Some(theme_file) =>theme_file,
241        None =>PathBuf::new(),
242    };
243    if theme_file.exists() {
244        return Some(IconTheme::from_pathbuff(theme_file))
245    }
246    None
247}
248
249/// # Icon Lookup
250/// 
251/// The icon lookup mechanism has two global settings, the list of base directories and the internal name of the current theme. Given these we need to specify how to look up an icon file from the icon name, the nominal size and the scale.
252/// 
253/// The lookup is done first in the current theme, and then recursively in each of the current theme's parents, and finally in the default theme called "hicolor" (implementations may add more default themes before "hicolor", but "hicolor" must be last). As soon as there is an icon of any size that matches in a theme, the search is stopped. Even if there may be an icon with a size closer to the correct one in an inherited theme, we don't want to use it. Doing so may generate an inconsistent change in an icon when you change icon sizes (e.g. zoom in).
254/// 
255/// The lookup inside a theme is done in three phases. First all the directories are scanned for an exact match, e.g. one where the allowed size of the icon files match what was looked up. Then all the directories are scanned for any icon that matches the name. If that fails we finally fall back on un-themed icons. If we fail to find any icon at all it is up to the application to pick a good fallback, as the correct choice depends on the context.
256/// 
257/// The exact algorithm (in rust) is now here:
258// this is our main function used by main.rs to find a single 'named' icon, regardless of type
259pub fn find_icon(icon:String, size:i32, scale:i32) -> Option<PathBuf> {
260    let dir_list_vector = generate_dir_list();
261    let mut theme = user_theme(dir_list_vector.clone());
262    if theme.is_none() {
263        //println!("No user theme");
264        theme = Some(IconTheme::empty());
265    }
266    let theme:IconTheme = theme.unwrap();
267    //println!("theme:{}", theme.clone().name.unwrap().as_str());
268    multiple_find_icon(icon, size, scale, dir_list_vector, theme)
269}
270pub fn multiple_find_icon(icon:String, size:i32, scale:i32, dir_list_vector:Vec<DirList>, theme:IconTheme) -> Option<PathBuf> {
271    // try with the default theme
272    let mut filename:Option<PathBuf> = find_icon_helper(icon.to_owned(), size, scale, theme, dir_list_vector.clone());
273    if filename.is_some(){ return filename }
274
275    // check hi-color a.k.a the "default" theme
276    let theme_file:PathBuf = match get_theme("hicolor", dir_list_vector.clone()).map(|theme| theme.index()) {
277        Some(theme_file) => theme_file,
278        None => PathBuf::new(),
279    };
280    let i_theme_file:String = match theme_file.as_path().to_str() {
281        Some(t) => String::from(t),
282        None => String::from(""),
283    };
284    let hicolor = IconTheme::new(i_theme_file);
285    filename = find_icon_helper(icon.to_owned(), size, scale, hicolor, dir_list_vector);
286    if filename.is_some(){ return filename }
287
288    // just find something already....
289    lookup_fallback_icon(icon)
290}
291
292/// the "helper" function from the free desktop example pseudo code
293pub fn find_icon_helper(icon:String, size:i32, scale:i32, theme:IconTheme, dir_list_vector:Vec<DirList>) -> Option<PathBuf> {
294    let mut filename:Option<PathBuf> = lookup_icon (icon.to_owned(), size, scale, theme.clone(), dir_list_vector.clone());
295    if filename.is_some(){ return filename }
296
297    if let Some(parents) = theme.inherits {
298        for parent in parents {
299        // make a theme from the 'parent'
300            let theme_file:PathBuf = match get_theme(&parent, dir_list_vector.clone()).map(|theme| theme.index()) {
301                Some(theme_file) => theme_file,
302                None => PathBuf::new(),
303            };
304            let i_theme_file:String = match theme_file.as_path().to_str() {
305                Some(t) => String::from(t),
306                None => String::from(""),
307            };
308            let parent_theme = IconTheme::new(i_theme_file.to_owned());
309            // boo recursion :(
310            filename = find_icon_helper (icon.to_owned(), size, scale, parent_theme.clone(), dir_list_vector.clone());
311            if filename.is_some(){ return filename }
312        }
313    }
314    None
315}
316
317/// One of the "following helper functions"
318pub fn lookup_icon (iconname:String, size:i32, scale:i32, theme:IconTheme, dir_list_vector:Vec<DirList>) -> Option<PathBuf> {
319    let list = theme.directories.to_owned();
320    //eprintln!("LIST {:?} {:?}", theme, list);
321    match list.as_ref() {
322        Some(_r) => (),
323        None => {
324            println!("Could not turn list into reference");
325            return None;
326        },
327    };
328
329    let theme_name = theme.name
330                          .unwrap();
331    let mut closest_filename:PathBuf = PathBuf::new();
332    let theme_subdir_list:Vec<Directory> = list.unwrap();
333
334    if let Some(theme) = get_theme(&theme_name, dir_list_vector) {
335        for directory in theme.dirs {
336            // first look check for size matching directories
337            for subdir in theme_subdir_list.clone() {
338                let subdir_name = subdir.name
339                                        .to_owned()
340                                        .unwrap();
341                for extension in EXTENTIONS.iter() {
342                    let mut path = directory.to_owned();
343                    path.push(subdir_name.as_str());
344                    let mut file_name:String = iconname.to_owned();
345                    file_name.push_str(extension);
346                    path.push(file_name.as_str());
347                    //println!("{:?} exists:{:?}", path, path.as_path().is_file());
348                    if directory_matches_size(subdir.to_owned(), size, scale)
349                    && path.as_path().is_file() {
350                        return Some(path)
351                    }
352                }
353            }
354        }
355    }
356    // ok second try lets look through all of them
357    let mut minimal_size:i32 = std::i32::MAX;
358    //TODO
359    for subdir in theme_subdir_list {
360        let subdir_name = subdir.name
361                                .to_owned()
362                                .unwrap();
363        let directory_vec:Vec<PathBuf> = to_pathbuff(icon_dirs_vector());
364        for directory in directory_vec {
365            for extension in EXTENTIONS.iter() {
366                let mut path = directory.to_owned();
367                path.push(theme_name.as_str());
368                path.push(subdir_name.as_str());
369                let mut file_name:String = iconname.to_owned();
370                file_name.push_str(extension);
371                path.push(file_name.as_str());
372                if path.as_path().is_file() && directory_size_distance(subdir.clone(), size, scale) < minimal_size {
373                    closest_filename = path.to_owned();
374                    minimal_size = directory_size_distance(subdir.clone(), size, scale);
375                }
376            }
377        }
378    }
379    if closest_filename.as_path() == Path::new("") {
380        None
381    } else {
382        Some(closest_filename)
383    }
384}
385
386/// Look in the basic icon directories (like /us/share/pixmaps, /usr/share/icons) for anything that matches the icon name!
387pub fn lookup_fallback_icon (iconname:String) ->Option<PathBuf> {
388    let directory_vec:Vec<PathBuf> = to_pathbuff(icon_dirs_vector());
389    for directory in directory_vec {
390        for extension in EXTENTIONS.iter() {
391            let mut path = directory.to_owned();
392            let mut file_name:String = iconname.to_owned();
393            file_name.push_str(extension);
394            path.push(file_name.as_str());
395            if path.as_path().is_file() {
396                return Some(path)
397            }
398        }
399    }
400    None
401}
402
403/// Check to see if the sub directory size is in range
404pub fn directory_matches_size(subdir:Directory, iconsize:i32, iconscale:i32) -> bool {
405    let mut scale = 1;
406    if subdir.scale.is_some() {
407        scale = subdir.scale.unwrap();
408    }
409    // check scale sent in
410    if scale != iconscale {
411        // wrong scale
412        return false
413    }
414    // get our variables
415    let mut d_type = DirectoryType::Threshold;
416    if let Some(d) = subdir.xdg_type {
417        d_type = d; 
418    }
419    let size = subdir.size;
420    // need a default size to check against below
421    if size.is_none() {
422        return false
423    }
424    let size:i32  = size.unwrap();
425    //println!("DirectoryType:{:?}, input size:{} scale:{} vs size:{} scale:{}", d_type, iconsize, iconscale, size, scale);
426    // do we have a minimum?
427    let min_size:i32 = match subdir.min_size {
428            Some(s) => s,
429            None => size,
430    };
431    // do we have a maximum?
432    let max_size:i32  = match subdir.max_size {
433            Some(s) => s,
434            None => size,
435    };
436    // do we have a threshold?
437    let threshold:i32 = match subdir.threshold {
438            Some(s) => s,
439            None => 2,
440    };
441    // what type of directory setting do we have?
442    match d_type {
443        DirectoryType::Fixed => {
444            // is it fixed?
445            return size == iconsize
446        },
447        DirectoryType::Scalable => {
448            // if it scales okay
449            if min_size <= iconsize &&
450               iconsize <= max_size {
451                //println!("scalable");
452                return true
453            }
454        },
455        DirectoryType::Threshold => {
456            // is this in the threshold? 
457            if (size - threshold) <= iconsize &&
458                iconsize <= (size + threshold) {
459                //println!("in threshold");
460                return true
461            }
462        }
463     }
464     // didn't match the icon parameters for this directory in this directory
465     false
466}
467/// You guessed it more pseudo code that turned into rust
468pub fn directory_size_distance(subdir:Directory, iconsize:i32, iconscale:i32) -> i32{
469    // default scale is 1
470    let mut scale = 1;
471    if subdir.scale.is_some() {
472        scale = subdir.scale.unwrap();
473    }
474    // default type is "Threshold"
475    let mut d_type = DirectoryType::Threshold;
476    if subdir.xdg_type.is_some() {
477        d_type = subdir.xdg_type.unwrap();
478    }
479    // we need the size for the defaults later
480    let size = subdir.size;
481    if size.is_none() {return 0}
482    let size:i32  = size.unwrap();
483    // default to sie
484    let mut min_size:i32  = subdir.size.unwrap();
485    if subdir.min_size.is_some() {
486        min_size = subdir.min_size.unwrap();
487    }
488    // efault to size
489    let mut max_size:i32  = subdir.size.unwrap();
490    if subdir.max_size.is_some() {
491        max_size = subdir.max_size.unwrap();
492    }
493    // 2 is the default "threshold"
494    let mut threshold:i32 = 2;
495    if subdir.threshold.is_some() {
496        threshold = subdir.threshold.unwrap();
497    }
498    
499    // now we check our Directory "type"
500    // all this math came directly from the page and is edited to be compatible with Rust
501    match d_type {
502        DirectoryType::Fixed => {
503            let num:i32 = size * scale - iconsize * iconscale;
504            num.abs()
505        },
506        DirectoryType::Scalable => {
507            if iconsize * iconscale < min_size * scale {
508                return min_size * scale - iconsize * iconscale
509            }
510            if iconsize * iconscale > max_size * scale {
511                return iconsize * iconscale - max_size * scale
512            }
513            0
514        },
515        DirectoryType::Threshold => {
516            if iconsize * iconscale < (size - threshold) * scale {
517                return min_size * scale - iconsize * iconscale
518            }
519            if iconsize*iconsize > (size + threshold) * scale {
520                return iconsize * iconsize - max_size * scale
521            }
522            0
523        },
524    }
525}
526
527/// In some cases you don't always want to fall back to an icon in an inherited theme. For instance, sometimes you look for a set of icons, preferring any of them before using an icon from an inherited theme. To support such operations implementations can contain a function that finds the first of a list of icon names in the inheritance hierarchy. This is that function!
528pub fn find_best_icon(icon_list:Vec<String>, size:i32, scale:i32) -> Option<PathBuf> {
529
530    let dir_list_vector = generate_dir_list();
531    let theme:IconTheme = match user_theme(dir_list_vector.clone()){
532        Some(theme) => theme,
533        None => {
534            IconTheme::empty();
535            return None
536        },
537    };
538
539    // Get the filename?
540    let mut filename:Option<PathBuf> = find_best_icon_helper(
541                                          icon_list.clone(),
542                                          size,
543                                          scale,
544                                          theme,
545                                          dir_list_vector.clone()
546                                      );
547    if filename.is_some(){ return filename }
548
549    // check hicolor a.k.a the "default theme"
550
551    let theme_file:PathBuf = match get_theme("hicolor", dir_list_vector.clone()).map(|theme| theme.index()) {
552                Some(theme_file) => theme_file,
553                None => PathBuf::new(),
554    };
555    let i_theme_file:String = match theme_file.as_path().to_str() {
556        Some(t) => String::from(t),
557        None => String::from(""),
558    };
559    let hicolor = IconTheme::new(i_theme_file);
560    filename = find_best_icon_helper(icon_list.clone(), size, scale, hicolor, dir_list_vector);
561
562    if filename.is_some(){ return filename }
563
564    for icon in icon_list {
565        filename = lookup_fallback_icon(icon);
566        if filename.is_some(){ return filename }
567    }
568    None
569}
570
571/// This can be very useful, for example, when handling mime type icons, where there are more and less "specific" versions of icons.
572pub fn find_best_icon_helper(icon_list:Vec<String>, size:i32, scale:i32, theme:IconTheme, dir_list_vector:Vec<DirList>) -> Option<PathBuf> {
573    let mut filename = None;
574    let list = icon_list;
575    let other = list.clone();
576    // look through a list of names to find any icon that is similar
577    for icon in list {
578        filename = lookup_icon(icon, size, scale, theme.clone(), dir_list_vector.clone());
579
580        if filename.is_some(){ return filename }
581    }
582
583    // check the inherits
584    let inherits = theme.inherits;
585    if  let Some(parents) = inherits {
586        for parent in parents {
587            // make a theme from the 'parent'
588            let theme_file:PathBuf = match get_theme(&parent, dir_list_vector.clone()).map(|theme| theme.index()) {
589                Some(theme_file) => theme_file,
590                None => PathBuf::new(),
591            };
592            let i_theme_file:String = match theme_file.as_path().to_str() {
593                Some(t) => String::from(t),
594                None => String::from(""),
595            };
596            let parent_theme = IconTheme::new(i_theme_file);
597            filename = find_best_icon_helper(other.clone(), size, scale, parent_theme.clone(), dir_list_vector.clone());
598
599            if filename.is_some(){ return filename }
600        }
601    }
602    filename
603}