Mime app handling improvements (#1850)

- Fix empty files getting `application/x-zerosize` as their mime types
    - May be tested by creating an empty `test.cpp` file
- Fix the wrong app being selected as a default if that app was set as
default for another mime type
- Include video players (such as cosmic-player) as suggested apps for
audio files
- Do the same for text-based mime types so that cosmic-edit will be
displayed as a suggestion for `.cpp` files
- Allow related apps to be selected in the Open With dropdown in the
properties context drawer.

---
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
This commit is contained in:
Jeremy Soller 2026-06-11 19:09:08 -06:00 committed by GitHub
commit 9a3eb1cdea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 114 additions and 82 deletions

View file

@ -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<MimeApp>, 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::<Vec<_>>();
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<Path>, impl AsRef<Path>)]) -> 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, true);
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, true);
let item_height = 32.0;
let mut displayed_default = false;
let mut last_kind = MimeAppMatch::Exact;
@ -5993,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",

View file

@ -6,12 +6,12 @@ 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};
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};
@ -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,
@ -184,7 +191,7 @@ pub struct MimeApp {
pub exec: Option<String>,
icon_name: Box<str>,
icon: std::sync::OnceLock<widget::icon::Handle>,
is_default: Arc<AtomicBool>,
is_default: Arc<RwLock<FxHashSet<Box<str>>>>,
no_display: Arc<AtomicBool>,
}
@ -199,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 {
@ -245,6 +252,68 @@ impl MimeAppCache {
mime_app_cache
}
pub fn get_apps_for_mime(
&self,
mime_type: &Mime,
include_other: bool,
) -> Vec<(&Arc<MimeApp>, 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)),
);
let include_mime = match mime_type.type_().as_str() {
"audio" => Some("video/mp4".parse::<Mime>().expect("video/mp4 mime")),
"text" => Some(mime_guess::mime::TEXT_PLAIN),
_ => 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 {
results.extend(
self.get(&parent_type)
.iter()
.filter(|&mime_app| dedupe.insert(&mime_app.id))
.map(|mime_app| (mime_app, MimeAppMatch::Related)),
);
}
}
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::<Vec<_>>();
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) {}
@ -280,7 +349,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)),
});
@ -348,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.store(true, atomic::Ordering::Relaxed);
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

View file

@ -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);

View file

@ -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::<Vec<Cow<'static, str>>>(),
mime_apps.iter().position(|x| x.is_default()),
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::<Mime>().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::<Vec<Cow<'static, str>>>(),
mime_apps.iter().position(|x| x.is_default()),
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())
}),
),