cosmic_files/
thumbnailer.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: GPL-3.0-only
3
4#[cfg(feature = "desktop")]
5use cosmic::desktop::fde::GenericEntry;
6use mime_guess::Mime;
7use rustc_hash::FxHashMap;
8use std::path::Path;
9use std::sync::{LazyLock, Mutex};
10use std::process;
11#[cfg(not(windows))]
12use std::time::Instant;
13#[cfg(not(windows))]
14use std::fs;
15
16#[derive(Clone, Debug)]
17pub struct Thumbnailer {
18    pub exec: String,
19}
20
21impl Thumbnailer {
22    pub fn command(
23        &self,
24        input: &Path,
25        output: &Path,
26        thumbnail_size: u32,
27    ) -> Option<process::Command> {
28        let args_vec: Vec<String> = shlex::split(&self.exec)?;
29        let mut args = args_vec.iter();
30        let mut command = process::Command::new(args.next()?);
31        for arg in args {
32            if arg.starts_with('%') {
33                match arg.as_str() {
34                    "%i" | "%u" => {
35                        command.arg(input);
36                    }
37                    "%o" => {
38                        command.arg(output);
39                    }
40                    "%s" => {
41                        command.arg(format!("{thumbnail_size}"));
42                    }
43                    _ => {
44                        log::warn!(
45                            "unsupported thumbnailer Exec code {:?} in {:?}",
46                            arg,
47                            self.exec
48                        );
49                        return None;
50                    }
51                }
52            } else {
53                command.arg(arg);
54            }
55        }
56        Some(command)
57    }
58}
59
60pub struct ThumbnailerCache {
61    cache: FxHashMap<Mime, Vec<Thumbnailer>>,
62}
63
64impl ThumbnailerCache {
65    pub fn new() -> Self {
66        let mut thumbnailer_cache = Self {
67            cache: FxHashMap::default(),
68        };
69        thumbnailer_cache.reload();
70        thumbnailer_cache
71    }
72
73    #[cfg(windows)]
74    pub fn reload(&mut self) {}
75
76    #[cfg(not(windows))]
77    pub fn reload(&mut self) {
78        let start = Instant::now();
79
80        self.cache.clear();
81
82        let mut search_dirs = Vec::new();
83        let xdg_dirs = xdg::BaseDirectories::new();
84
85        if let Some(mut data_home) = xdg_dirs.get_data_home() {
86            data_home.push("thumbnailers");
87            search_dirs.push(data_home);
88        }
89        search_dirs.extend(xdg_dirs.get_data_dirs().into_iter().map(|mut data_dir| {
90            data_dir.push("thumbnailers");
91            data_dir
92        }));
93
94        let mut thumbnailer_paths = Vec::new();
95        for dir in search_dirs {
96            log::trace!("looking for thumbnailers in {}", dir.display());
97            match fs::read_dir(&dir) {
98                Ok(entries) => {
99                    thumbnailer_paths.extend(entries.filter_map(|entry_res| {
100                        entry_res
101                            .inspect_err(|err| {
102                                log::warn!(
103                                    "failed to read entry in directory {}: {}",
104                                    dir.display(),
105                                    err
106                                )
107                            })
108                            .ok()
109                            .map(|entry| entry.path())
110                    }));
111                }
112                Err(err) => {
113                    log::warn!("failed to read directory {}: {}", dir.display(), err);
114                }
115            }
116        }
117
118        //TODO: handle directory specific behavior
119        for path in thumbnailer_paths {
120            let entry = match GenericEntry::from_path(&path) {
121                Ok(ok) => ok,
122                Err(err) => {
123                    log::warn!("failed to parse {}: {}", path.display(), err);
124                    continue;
125                }
126            };
127
128            //TODO: use TryExec?
129            let Some(section) = entry.group("Thumbnailer Entry") else {
130                log::warn!(
131                    "missing Thumbnailer Entry section for thumbnailer {}",
132                    path.display()
133                );
134                continue;
135            };
136            let Some(exec) = section.entry("Exec") else {
137                log::warn!("missing Exec attribute for thumbnailer {}", path.display());
138                continue;
139            };
140            let Some(mime_types) = section.entry("MimeType") else {
141                log::warn!(
142                    "missing MimeType attribute for thumbnailer {}",
143                    path.display()
144                );
145                continue;
146            };
147
148            for mime_type in mime_types.split_terminator(';') {
149                if let Ok(mime) = mime_type.parse::<Mime>() {
150                    log::trace!("thumbnailer {}={}", mime, path.display());
151                    let apps = self
152                        .cache
153                        .entry(mime)
154                        .or_insert_with(|| Vec::with_capacity(1));
155                    apps.push(Thumbnailer {
156                        exec: exec.to_string(),
157                    });
158                }
159            }
160        }
161
162        let elapsed = start.elapsed();
163        log::info!("loaded thumbnailer cache in {elapsed:?}");
164    }
165
166    pub fn get(&self, key: &Mime) -> Vec<Thumbnailer> {
167        self.cache.get(key).map_or_else(Vec::new, Vec::clone)
168    }
169}
170
171static THUMBNAILER_CACHE: LazyLock<Mutex<ThumbnailerCache>> =
172    LazyLock::new(|| Mutex::new(ThumbnailerCache::new()));
173
174pub fn thumbnailer(mime: &Mime) -> Vec<Thumbnailer> {
175    let thumbnailer_cache = THUMBNAILER_CACHE.lock().unwrap();
176    thumbnailer_cache.get(mime)
177}