cosmic_files/
thumbnailer.rs1#[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 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 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}