1use std::collections::BTreeMap;
23use std::path::PathBuf;
24
25#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct Icons {
28 own: BTreeMap<String, String>,
29 generic: BTreeMap<String, String>,
30}
31
32impl Icons {
33 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 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 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 #[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 #[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 #[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}