cosmic_files/
mime_icon.rs

1// SPDX-License-Identifier: GPL-3.0-only
2
3use cosmic::widget::icon;
4use mime_guess::Mime;
5use rustc_hash::FxHashMap;
6use std::fs;
7use std::path::Path;
8use std::sync::{LazyLock, Mutex};
9
10pub const FALLBACK_MIME_ICON: &str = "text-x-generic";
11
12#[derive(Debug, Eq, Hash, PartialEq)]
13struct MimeIconKey {
14    mime: Mime,
15    size: u16,
16}
17
18#[derive(Default)]
19pub struct MimeIconCache {
20    #[allow(dead_code)]
21    cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
22    #[cfg(unix)]
23    pub shared_mime_info: xdg_mime::SharedMimeInfo,
24}
25
26impl MimeIconCache {
27    #[cfg(not(unix))]
28    pub fn get(&mut self, _key: MimeIconKey) -> Option<icon::Handle> {
29        None
30    }
31
32    #[cfg(unix)]
33    fn get(&mut self, key: MimeIconKey) -> Option<icon::Handle> {
34        self.cache
35            .entry(key)
36            .or_insert_with_key(|key| {
37                #[cfg(unix)]
38                let mut icon_names = self.shared_mime_info.lookup_icon_names(&key.mime);
39                #[cfg(not(unix))]
40                let mut icon_names = match guess_generic_icon_name(&key.mime) {
41                    Some(name) => vec![name],
42                    None => vec![],
43                };
44                if icon_names.is_empty() {
45                    return None;
46                }
47                let icon_name = icon_names.remove(0);
48                let mut named = icon::from_name(icon_name).prefer_svg(true).size(key.size);
49                if !icon_names.is_empty() {
50                    let fallback_names =
51                        icon_names.into_iter().map(std::borrow::Cow::from).collect();
52                    named = named.fallback(Some(icon::IconFallback::Names(fallback_names)));
53                }
54                Some(named.handle())
55            })
56            .clone()
57    }
58}
59
60pub static MIME_ICON_CACHE: LazyLock<Mutex<MimeIconCache>> =
61    LazyLock::new(|| Mutex::new(MimeIconCache::default()));
62
63#[cfg(not(unix))]
64pub fn mime_for_path(
65    path: impl AsRef<Path>,
66    _metadata_opt: Option<&fs::Metadata>,
67    _remote: bool,
68) -> Mime {
69    mime_guess::from_path(path).first_or_octet_stream()
70}
71
72#[cfg(unix)]
73pub fn mime_for_path(
74    path: impl AsRef<Path>,
75    _metadata_opt: Option<&fs::Metadata>,
76    _remote: bool,
77) -> Mime {
78    let path = path.as_ref();
79
80    #[cfg(unix)]
81    {
82        let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
83        // Try the shared mime info cache first
84        let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type();
85        gb.zero_size(false);
86    if remote {
87            if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) {
88                gb.file_name(file_name);
89            }
90        } else {
91            gb.path(path);
92        }
93        if let Some(metadata) = metadata_opt {
94            gb.metadata(metadata.clone());
95        }
96        let guess = gb.guess();
97        let guessed_mime = guess.mime_type();
98
99        /// Checks if the `Mime` is a special variant returned by `xdg-mime`.
100        /// This includes directories, symlinks and zerosize files, which are returned as uncertain.
101        fn is_special_mime(mime: &Mime) -> bool {
102            *mime == "inode/directory"
103                || *mime == "inode/symlink"
104                || *mime == "application/x-zerosize"
105        }
106
107        // `xdg-mime-rs` sets the guess to uncertain if it returns special mime types.
108        // The guess could also be uncertain on platforms without shared-mime-info.
109        // Try mime_guess, but only if it is not one of the special mime types.
110        if guess.uncertain() && (remote || !is_special_mime(guessed_mime)) {
111            // If uncertain, try mime_guess. This could happen on platforms without shared-mime-info
112            mime_guess::from_path(path).first_or_octet_stream()
113        } else {
114            guessed_mime.clone()
115        }
116    }
117    #[cfg(not(unix))]
118    {
119        mime_guess::from_path(path).first_or_octet_stream()
120    }
121}
122
123#[allow(dead_code)]
124fn guess_generic_icon_name(mime: &Mime) -> Option<&'static str> {
125    let ty = mime.type_().as_str();
126    let sub = mime.subtype().as_str();
127
128    match ty {
129        "text" => Some("text-x-generic"),
130        "image" => Some("image-x-generic"),
131        "audio" => Some("audio-x-generic"),
132        "video" => Some("video-x-generic"),
133        "application" => match sub {
134            "pdf" => Some("application-pdf"),
135            "zip" | "x-7z-compressed" | "x-rar-compressed" | "x-xz" | "x-bzip2" => {
136                Some("package-x-generic")
137            }
138            "json" | "xml" | "x-yaml" => Some("text-x-generic"),
139            _ => Some("application-x-executable"), // or "application-x-generic"
140        },
141        _ => None,
142    }
143}
144
145pub fn mime_icon(mime: Mime, size: u16) -> icon::Handle {
146    let mut mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
147    match mime_icon_cache.get(MimeIconKey { mime, size }) {
148        Some(handle) => handle,
149        None => icon::from_name(FALLBACK_MIME_ICON)
150            .prefer_svg(true)
151            .size(size)
152            .handle(),
153    }
154}
155
156#[cfg(not(unix))]
157pub fn parent_mime_types(_mime: &Mime) -> Option<Vec<Mime>> {
158    None
159}
160
161#[cfg(unix)]
162pub fn parent_mime_types(mime: &Mime) -> Option<Vec<Mime>> {
163    let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap();
164    mime_icon_cache.shared_mime_info.get_parents_aliased(mime)
165}