Skip to main content

hyprforge_mime/
icons.rs

1//! Which icon *name* a type has — the half of "draw this file's icon"
2//! that is the MIME database's. Which *file* that name is belongs to the
3//! icon theme, and to `hyprforge-icons`.
4//!
5//! The shared MIME database specification gives three sources, most
6//! specific first, and two more plain files beside `globs2`:
7//!
8//! - `icons` — `type:icon`, a type's own icon when it is not the
9//!   obvious one (`application/x-godot-scene:x-godot-scene`).
10//! - the type itself with `/` turned into `-` — `image/png` is
11//!   `image-png`. This is the one almost every type uses.
12//! - `generic-icons` — `type:icon`, the family it belongs to
13//!   (`application/xhtml+xml:text-html`); and failing that,
14//!   `<media>-x-generic`.
15//!
16//! Between the second and the third, this also offers each *parent*
17//! type's own name — `text/markdown` is a kind of `text/plain`, so
18//! `text-plain` is a better answer than a generic page. Not in the
19//! specification's list, and harmless beside it: a theme that has none
20//! of these names still reaches the generic ones after.
21
22use std::collections::BTreeMap;
23use std::path::PathBuf;
24
25/// The two icon files, read once.
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct Icons {
28    own: BTreeMap<String, String>,
29    generic: BTreeMap<String, String>,
30}
31
32impl Icons {
33    /// Reads `mime/icons` and `mime/generic-icons` from every data
34    /// directory given. The first to name a type keeps it, so a user's
35    /// own database beats the system's.
36    pub fn load_from(data_dirs: &[PathBuf]) -> Icons {
37        let mut icons = Icons::default();
38        for dir in data_dirs {
39            let mime = dir.join("mime");
40            if let Ok(text) = std::fs::read_to_string(mime.join("icons")) {
41                add(&mut icons.own, &text);
42            }
43            if let Ok(text) = std::fs::read_to_string(mime.join("generic-icons")) {
44                add(&mut icons.generic, &text);
45            }
46        }
47        icons
48    }
49
50    /// Every icon name worth trying for `mime`, best first. `parents` is
51    /// what the type is a kind of, nearest first.
52    pub fn names_for(&self, mime: &str, parents: &[String]) -> Vec<String> {
53        let mut names: Vec<String> = Vec::new();
54        let mut push = |name: String| {
55            if !names.contains(&name) {
56                names.push(name);
57            }
58        };
59        if let Some(own) = self.own.get(mime) {
60            push(own.clone());
61        }
62        push(mime.replace('/', "-"));
63        // `application/octet-stream` is every type's last ancestor and
64        // its icon means "unknown", which is no better than the generic
65        // family a more specific type still has.
66        for parent in parents.iter().filter(|p| *p != "application/octet-stream") {
67            push(parent.replace('/', "-"));
68        }
69        if let Some(generic) = self.generic.get(mime) {
70            push(generic.clone());
71        }
72        if let Some((media, _)) = mime.split_once('/') {
73            push(format!("{media}-x-generic"));
74        }
75        names
76    }
77}
78
79fn add(into: &mut BTreeMap<String, String>, text: &str) {
80    for line in text.lines().map(str::trim) {
81        if line.starts_with('#') {
82            continue;
83        }
84        if let Some((mime, icon)) = line.split_once(':') {
85            into.entry(mime.trim().to_string()).or_insert_with(|| icon.trim().to_string());
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    fn icons(own: &str, generic: &str) -> Icons {
95        let mut icons = Icons::default();
96        add(&mut icons.own, own);
97        add(&mut icons.generic, generic);
98        icons
99    }
100
101    /// The ordinary case: the type's own dashed name, then its family.
102    #[test]
103    fn a_type_is_named_by_itself_first_and_its_media_last() {
104        let names = icons("", "").names_for("image/png", &[]);
105        assert_eq!(names, ["image-png", "image-x-generic"]);
106    }
107
108    /// An `icons` entry overrides the obvious name, and `generic-icons`
109    /// overrides the `<media>-x-generic` guess without removing it.
110    #[test]
111    fn the_databases_own_answers_come_before_the_guesses() {
112        let db = icons("application/x-godot-scene:x-godot-scene", "application/xhtml+xml:text-html");
113        assert_eq!(db.names_for("application/x-godot-scene", &[])[0], "x-godot-scene");
114        assert_eq!(
115            db.names_for("application/xhtml+xml", &[]),
116            ["application-xhtml+xml", "text-html", "application-x-generic"]
117        );
118    }
119
120    /// A parent's name sits between the type's own and the generic ones —
121    /// and the universal ancestor is not offered, since its icon means
122    /// "unknown".
123    #[test]
124    fn a_parent_type_is_tried_before_the_generic_family() {
125        let parents = ["text/plain".to_string(), "application/octet-stream".to_string()];
126        let names = icons("", "").names_for("text/markdown", &parents);
127        assert_eq!(names, ["text-markdown", "text-plain", "text-x-generic"]);
128    }
129}