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:
commit
9a3eb1cdea
4 changed files with 114 additions and 82 deletions
61
src/app.rs
61
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<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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
38
src/tab.rs
38
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::<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())
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue