From 64998b37a1ff84a4430101e3dfb2191dc7d6f428 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 11 Jun 2026 15:52:30 +0200 Subject: [PATCH 1/6] fix: do not use `application/x-zerosize` as mime type for empty files --- src/mime_icon.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mime_icon.rs b/src/mime_icon.rs index 64f007f..847b550 100644 --- a/src/mime_icon.rs +++ b/src/mime_icon.rs @@ -78,6 +78,7 @@ pub fn mime_for_path( let mime_icon_cache = MIME_ICON_CACHE.lock().unwrap(); // Try the shared mime info cache first let mut gb = mime_icon_cache.shared_mime_info.guess_mime_type(); + gb.zero_size(false); if remote { if let Some(file_name) = path.file_name().and_then(std::ffi::OsStr::to_str) { gb.file_name(file_name); From 3cd0d0c00a867933b1eef954757400b56e391d03 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 11 Jun 2026 16:36:30 +0200 Subject: [PATCH 2/6] refactor: move mime app logic to mime_app module --- src/app.rs | 59 +++---------------------------------------------- src/mime_app.rs | 51 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/app.rs b/src/app.rs index 292bb83..10b8c76 100644 --- a/src/app.rs +++ b/src/app.rs @@ -67,7 +67,7 @@ use crate::config::{ use crate::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings}; use crate::key_bind::key_binds; use crate::localize::LANGUAGE_SORTER; -use crate::mime_app::{self, MimeApp, MimeAppCache}; +use crate::mime_app::{self, MimeApp, MimeAppCache, MimeAppMatch}; use crate::mounter::{ MOUNTERS, MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, }; @@ -656,13 +656,6 @@ impl DialogPages { pub struct FavoriteIndex(usize); -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum MimeAppMatch { - Exact, - Related, - Other, -} - pub struct MounterData(MounterKey, MounterItem); #[derive(Clone, Debug)] @@ -2274,52 +2267,6 @@ impl App { .into() } - fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&Arc, MimeAppMatch)> { - let mut results = Vec::new(); - - let mut dedupe = FxHashSet::default(); - - // start with exact matches - results.extend( - self.mime_app_cache - .get(mime_type) - .iter() - .filter(|&mime_app| dedupe.insert(&mime_app.id)) - .map(|mime_app| (mime_app, MimeAppMatch::Exact)), - ); - - // grab matches based off of subclass / parent mime type - if let Some(parent_types) = mime_icon::parent_mime_types(mime_type) { - for parent_type in parent_types { - results.extend( - self.mime_app_cache - .get(&parent_type) - .iter() - .filter(|&mime_app| dedupe.insert(&mime_app.id)) - .map(|mime_app| (mime_app, MimeAppMatch::Related)), - ); - } - } - - // Add other apps - results.extend({ - let mut apps = self - .mime_app_cache - .apps() - .iter() - .filter(|mime_app| !mime_app.no_display()) - .filter(|&mime_app| dedupe.insert(&mime_app.id)) - .map(|mime_app| (mime_app, MimeAppMatch::Other)) - .collect::>(); - apps.sort_by(|(a, _), (b, _)| { - crate::localize::LANGUAGE_SORTER.compare(&a.name, &b.name) - }); - apps - }); - - results - } - // Update favorites based on renaming or moving dirs. fn update_favorites(&mut self, path_changes: &[(impl AsRef, impl AsRef)]) -> bool { let mut favorites_changed = false; @@ -3233,7 +3180,7 @@ impl Application for App { selected, .. } => { - let available_apps = self.get_apps_for_mime(&mime); + let available_apps = self.mime_app_cache.get_apps_for_mime(&mime); if let Some((app, _)) = available_apps.get(selected) { if let Some(mut command) = @@ -5971,7 +5918,7 @@ impl Application for App { }; let mut column = widget::list_column(); - let available_apps = self.get_apps_for_mime(mime); + let available_apps = self.mime_app_cache.get_apps_for_mime(mime); let item_height = 32.0; let mut displayed_default = false; let mut last_kind = MimeAppMatch::Exact; diff --git a/src/mime_app.rs b/src/mime_app.rs index f1f643c..a4956f8 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -6,7 +6,7 @@ use cosmic::widget; pub use mime_guess::Mime; #[cfg(feature = "desktop")] use notify_debouncer_full::notify; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; @@ -176,6 +176,13 @@ pub fn exec_to_command( Some(commands) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MimeAppMatch { + Exact, + Related, + Other, +} + #[derive(Clone, Debug)] pub struct MimeApp { pub id: String, @@ -245,6 +252,48 @@ impl MimeAppCache { mime_app_cache } + pub fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&Arc, MimeAppMatch)> { + let mut results = Vec::new(); + + let mut dedupe = FxHashSet::default(); + + // start with exact matches + results.extend( + self.get(mime_type) + .iter() + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Exact)), + ); + + // grab matches based off of subclass / parent mime type + if let Some(parent_types) = crate::mime_icon::parent_mime_types(mime_type) { + for parent_type in parent_types { + results.extend( + self.get(&parent_type) + .iter() + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Related)), + ); + } + } + + results.extend({ + let mut apps = self + .apps() + .iter() + .filter(|mime_app| !mime_app.no_display()) + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Other)) + .collect::>(); + apps.sort_by(|(a, _), (b, _)| { + crate::localize::LANGUAGE_SORTER.compare(&a.name, &b.name) + }); + apps + }); + + results + } + #[cfg(not(feature = "desktop"))] pub fn reload(&mut self) {} From 256a6cba19b2bf7667d271fe27c106449bf6c30c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 11 Jun 2026 17:15:37 +0200 Subject: [PATCH 3/6] feat(mime_app): include video players for audio mime types --- src/mime_app.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/mime_app.rs b/src/mime_app.rs index a4956f8..e1f1ca2 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -254,7 +254,6 @@ impl MimeAppCache { pub fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&Arc, MimeAppMatch)> { let mut results = Vec::new(); - let mut dedupe = FxHashSet::default(); // start with exact matches @@ -265,6 +264,20 @@ impl MimeAppCache { .map(|mime_app| (mime_app, MimeAppMatch::Exact)), ); + let include_mime = match mime_type.type_().as_str() { + "audio" => Some("video/mp4".parse::().expect("video/mp4 mime")), + _ => None, + }; + + if let Some(mime) = include_mime { + results.extend( + self.get(&mime) + .iter() + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Exact)), + ); + } + // grab matches based off of subclass / parent mime type if let Some(parent_types) = crate::mime_icon::parent_mime_types(mime_type) { for parent_type in parent_types { From 6d4af69318e5a5ee5fdfd2000383d9a199247f6f Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 11 Jun 2026 17:16:26 +0200 Subject: [PATCH 4/6] feat(mime_app): include text editors for text-based mime types --- src/mime_app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mime_app.rs b/src/mime_app.rs index e1f1ca2..a76a84c 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -266,6 +266,7 @@ impl MimeAppCache { let include_mime = match mime_type.type_().as_str() { "audio" => Some("video/mp4".parse::().expect("video/mp4 mime")), + "text" => Some(mime_guess::mime::TEXT_PLAIN), _ => None, }; From eda8291678dac99fd082e778c5260754ab26f8f6 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 11 Jun 2026 17:54:12 +0200 Subject: [PATCH 5/6] fix(mime_app): track mime type app default by mime --- src/app.rs | 2 +- src/mime_app.rs | 12 ++++++------ src/tab.rs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app.rs b/src/app.rs index 10b8c76..58ac81f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5940,7 +5940,7 @@ impl Application for App { widget::button::custom( widget::row::with_children([ icon(app.icon()).size(32).into(), - if app.is_default() && !displayed_default { + if app.is_default(mime) && !displayed_default { displayed_default = true; widget::text::body(fl!( "default-app", diff --git a/src/mime_app.rs b/src/mime_app.rs index a76a84c..bb84f80 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -11,7 +11,7 @@ use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicBool; -use std::sync::{Arc, atomic}; +use std::sync::{Arc, RwLock, atomic}; use std::time::{self, Instant}; use std::{fs, io, process}; @@ -191,7 +191,7 @@ pub struct MimeApp { pub exec: Option, icon_name: Box, icon: std::sync::OnceLock, - is_default: Arc, + is_default: Arc>>>, no_display: Arc, } @@ -206,8 +206,8 @@ impl MimeApp { ) } - pub fn is_default(&self) -> bool { - self.is_default.load(atomic::Ordering::Relaxed) + pub fn is_default(&self, mime: &Mime) -> bool { + self.is_default.read().unwrap().contains(mime.essence_str()) } pub fn no_display(&self) -> bool { @@ -343,7 +343,7 @@ impl MimeAppCache { exec: desktop_entry.exec().map(String::from), icon_name: desktop_entry.icon().unwrap_or_default().into(), icon: std::sync::OnceLock::new(), - is_default: Arc::new(AtomicBool::new(false)), + is_default: Arc::new(RwLock::default()), no_display: Arc::new(AtomicBool::new(false)), }); @@ -414,7 +414,7 @@ impl MimeAppCache { apps.retain(|app| { let found = app.id.as_str() == default; if found { - app.is_default.store(true, atomic::Ordering::Relaxed); + app.is_default.write().unwrap().insert(default.into()); cache.push(app.clone()); } diff --git a/src/tab.rs b/src/tab.rs index 0aaaddc..d5deaaf 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -2378,7 +2378,7 @@ impl Item { .iter() .map(|app| Cow::Owned(app.name.clone())) .collect::>>(), - mime_apps.iter().position(|x| x.is_default()), + mime_apps.iter().position(|x| x.is_default(&self.mime)), move |index| index, ) .icons(Cow::Owned(mime_app_cache.icons(&self.mime))), @@ -6462,7 +6462,7 @@ impl Tab { .iter() .map(|app| Cow::Owned(app.name.clone())) .collect::>>(), - mime_apps.iter().position(|x| x.is_default()), + mime_apps.iter().position(|x| x.is_default(&mime)), move |index| (index, mime_closure.clone()), ) .icons(Cow::Owned(mime_app_cache.icons(&mime))), From e8fdb28a24ab11f249a9a39b1ea04bb245924466 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 11 Jun 2026 18:45:49 +0200 Subject: [PATCH 6/6] feat(mime_app): show related apps in open with dropdown in preview --- src/app.rs | 4 ++-- src/mime_app.rs | 47 +++++++++++++++++++++++++++++++++-------------- src/tab.rs | 38 ++++++++++++++++++++------------------ 3 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/app.rs b/src/app.rs index 58ac81f..ac0996a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3180,7 +3180,7 @@ impl Application for App { selected, .. } => { - let available_apps = self.mime_app_cache.get_apps_for_mime(&mime); + let available_apps = self.mime_app_cache.get_apps_for_mime(&mime, true); if let Some((app, _)) = available_apps.get(selected) { if let Some(mut command) = @@ -5918,7 +5918,7 @@ impl Application for App { }; let mut column = widget::list_column(); - let available_apps = self.mime_app_cache.get_apps_for_mime(mime); + let available_apps = self.mime_app_cache.get_apps_for_mime(mime, true); let item_height = 32.0; let mut displayed_default = false; let mut last_kind = MimeAppMatch::Exact; diff --git a/src/mime_app.rs b/src/mime_app.rs index bb84f80..5a3b421 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -252,7 +252,11 @@ impl MimeAppCache { mime_app_cache } - pub fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&Arc, MimeAppMatch)> { + pub fn get_apps_for_mime( + &self, + mime_type: &Mime, + include_other: bool, + ) -> Vec<(&Arc, MimeAppMatch)> { let mut results = Vec::new(); let mut dedupe = FxHashSet::default(); @@ -291,19 +295,21 @@ impl MimeAppCache { } } - results.extend({ - let mut apps = self - .apps() - .iter() - .filter(|mime_app| !mime_app.no_display()) - .filter(|&mime_app| dedupe.insert(&mime_app.id)) - .map(|mime_app| (mime_app, MimeAppMatch::Other)) - .collect::>(); - apps.sort_by(|(a, _), (b, _)| { - crate::localize::LANGUAGE_SORTER.compare(&a.name, &b.name) + if include_other { + results.extend({ + let mut apps = self + .apps() + .iter() + .filter(|mime_app| !mime_app.no_display()) + .filter(|&mime_app| dedupe.insert(&mime_app.id)) + .map(|mime_app| (mime_app, MimeAppMatch::Other)) + .collect::>(); + apps.sort_by(|(a, _), (b, _)| { + crate::localize::LANGUAGE_SORTER.compare(&a.name, &b.name) + }); + apps }); - apps - }); + } results } @@ -411,15 +417,28 @@ impl MimeAppCache { // Sort cached apps for this mime by default precedence. for default in defaults.into_iter().flatten() { let default = default.strip_suffix(".desktop").unwrap_or(default.as_ref()); + let mut found_any = false; apps.retain(|app| { let found = app.id.as_str() == default; if found { - app.is_default.write().unwrap().insert(default.into()); + app.is_default + .write() + .unwrap() + .insert(mime.essence_str().into()); cache.push(app.clone()); + found_any = true; } !found }); + + if !found_any && let Some(app) = self.apps.iter().find(|app| app.id == default) { + app.is_default + .write() + .unwrap() + .insert(mime.essence_str().into()); + cache.push(app.clone()); + } } // Sort remaining apps by name diff --git a/src/tab.rs b/src/tab.rs index d5deaaf..6afcfce 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -2368,23 +2368,24 @@ impl Item { ))); let mut settings = Vec::new(); if let Some(mime_app_cache) = mime_app_cache_opt { - let mime_apps = mime_app_cache.get(&self.mime); + let mime_apps = mime_app_cache.get_apps_for_mime(&self.mime, false); if !mime_apps.is_empty() { + let (names, icons) = mime_apps + .iter() + .map(|(app, _)| (Cow::Owned(app.name.clone()), app.icon())) + .collect::<(Vec<_>, Vec<_>)>(); settings.push( widget::settings::item::builder(fl!("open-with")).control( Element::from( widget::dropdown( - mime_apps - .iter() - .map(|app| Cow::Owned(app.name.clone())) - .collect::>>(), - mime_apps.iter().position(|x| x.is_default(&self.mime)), + names, + mime_apps.iter().position(|(x, _)| x.is_default(&self.mime)), move |index| index, ) - .icons(Cow::Owned(mime_app_cache.icons(&self.mime))), + .icons(Cow::Owned(icons)), ) - .map(|index| { - let mime_app = &mime_apps[index]; + .map(move |index| { + let mime_app = &mime_apps[index].0; Message::SetOpenWith(self.mime.clone(), mime_app.id.clone()) }), ), @@ -6451,24 +6452,25 @@ impl Tab { .and_then(|(mime, _)| mime.parse::().ok()) && let Some(mime_app_cache) = mime_app_cache_opt { - let mime_apps = mime_app_cache.get(&mime); + let mime_apps = mime_app_cache.get_apps_for_mime(&mime, false); if !mime_apps.is_empty() { let mime_closure = mime.clone(); + let (names, icons) = mime_apps + .iter() + .map(|(app, _)| (Cow::Owned(app.name.clone()), app.icon())) + .collect::<(Vec<_>, Vec<_>)>(); settings.push( widget::settings::item::builder(fl!("open-with")).control( Element::from( widget::dropdown( - mime_apps - .iter() - .map(|app| Cow::Owned(app.name.clone())) - .collect::>>(), - mime_apps.iter().position(|x| x.is_default(&mime)), + names, + mime_apps.iter().position(|(x, _)| x.is_default(&mime)), move |index| (index, mime_closure.clone()), ) - .icons(Cow::Owned(mime_app_cache.icons(&mime))), + .icons(Cow::Owned(icons)), ) - .map(|(index, mime)| { - let mime_app = &mime_apps[index]; + .map(move |(index, mime)| { + let mime_app = &mime_apps[index].0; Message::SetOpenWith(mime, mime_app.id.clone()) }), ),