feat: improve mime app detection & do it faster

This commit is contained in:
Michael Aaron Murphy 2026-05-18 17:23:43 +02:00 committed by Ashley Wulber
parent bf01bb31a3
commit 53cfeebe57
3 changed files with 121 additions and 110 deletions

View file

@ -448,6 +448,7 @@ pub enum Message {
Option<Vec<PathBuf>>, Option<Vec<PathBuf>>,
), ),
TabView(Option<Entity>, tab::View), TabView(Option<Entity>, tab::View),
TerminalDefault(Box<str>),
TimeConfigChange(TimeConfig), TimeConfigChange(TimeConfig),
ToggleContextPage(ContextPage), ToggleContextPage(ContextPage),
ToggleFoldersFirst, ToggleFoldersFirst,
@ -559,7 +560,7 @@ pub enum DialogPage {
path: PathBuf, path: PathBuf,
mime: mime_guess::Mime, mime: mime_guess::Mime,
selected: usize, selected: usize,
store_opt: Option<MimeApp>, store_opt: Option<Arc<MimeApp>>,
}, },
PermanentlyDelete { PermanentlyDelete {
paths: Box<[PathBuf]>, paths: Box<[PathBuf]>,
@ -2273,7 +2274,7 @@ impl App {
.into() .into()
} }
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> { fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&Arc<MimeApp>, MimeAppMatch)> {
let mut results = Vec::new(); let mut results = Vec::new();
let mut dedupe = FxHashSet::default(); let mut dedupe = FxHashSet::default();
@ -4793,6 +4794,7 @@ impl Application for App {
tab.refresh_cut(&paths); tab.refresh_cut(&paths);
} }
} }
Message::TerminalDefault(command) => {}
Message::TimeConfigChange(time_config) => { Message::TimeConfigChange(time_config) => {
self.config.tab.military_time = time_config.military_time; self.config.tab.military_time = time_config.military_time;
return self.update_config(); return self.update_config();
@ -5982,7 +5984,7 @@ impl Application for App {
widget::button::custom( widget::button::custom(
widget::row::with_children([ widget::row::with_children([
icon(app.icon.clone()).size(32).into(), icon(app.icon.clone()).size(32).into(),
if app.is_default && !displayed_default { if app.is_default() && !displayed_default {
displayed_default = true; displayed_default = true;
widget::text::body(fl!( widget::text::body(fl!(
"default-app", "default-app",

View file

@ -7,10 +7,11 @@ use cosmic::desktop;
use cosmic::widget; use cosmic::widget;
pub use mime_guess::Mime; pub use mime_guess::Mime;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::cmp::Ordering;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, atomic};
use std::time::Instant; use std::time::Instant;
use std::{fs, io, process}; use std::{fs, io, process};
@ -133,7 +134,7 @@ pub struct MimeApp {
pub name: String, pub name: String,
pub exec: Option<String>, pub exec: Option<String>,
pub icon: widget::icon::Handle, pub icon: widget::icon::Handle,
pub is_default: bool, is_default: Arc<AtomicBool>,
} }
impl MimeApp { impl MimeApp {
@ -146,6 +147,10 @@ impl MimeApp {
path_opt, path_opt,
) )
} }
pub fn is_default(&self) -> bool {
self.is_default.load(atomic::Ordering::SeqCst)
}
} }
// This allows usage of MimeApp in a dropdown // This allows usage of MimeApp in a dropdown
@ -169,24 +174,16 @@ impl From<&desktop::DesktopEntryData> for MimeApp {
} }
desktop::fde::IconSource::Path(path) => widget::icon::from_path(path.clone()), desktop::fde::IconSource::Path(path) => widget::icon::from_path(path.clone()),
}, },
is_default: false, is_default: Arc::new(AtomicBool::new(false)),
} }
} }
} }
#[cfg(feature = "desktop")]
fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
path_opt
.as_ref()
.and_then(|path| path.file_name())
.is_some_and(|x| x == filename)
}
pub struct MimeAppCache { pub struct MimeAppCache {
apps: Vec<MimeApp>, apps: Vec<Arc<MimeApp>>,
cache: FxHashMap<Mime, Vec<MimeApp>>, cache: FxHashMap<Mime, Vec<Arc<MimeApp>>>,
icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>, icons: FxHashMap<Mime, Box<[widget::icon::Handle]>>,
terminals: Vec<MimeApp>, terminals: Vec<Arc<MimeApp>>,
} }
impl MimeAppCache { impl MimeAppCache {
@ -204,10 +201,12 @@ impl MimeAppCache {
#[cfg(not(feature = "desktop"))] #[cfg(not(feature = "desktop"))]
pub fn reload(&mut self) {} pub fn reload(&mut self) {}
// Only available when using desktop feature of libcosmic, which only works on Unix-likes /// Reload mime types and their known app associations and defaults.
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
pub fn reload(&mut self) { pub fn reload(&mut self) {
use crate::localize::LANGUAGE_SORTER; use crate::localize::LANGUAGE_SORTER;
use cosmic::desktop::fde;
use std::borrow::Cow;
let start = Instant::now(); let start = Instant::now();
@ -216,107 +215,111 @@ impl MimeAppCache {
self.icons.clear(); self.icons.clear();
self.terminals.clear(); self.terminals.clear();
//TODO: get proper locale?
let locale = &[];
// Load desktop applications by supported mime types
//TODO: hashmap for all apps by id?
let all_apps: Box<[_]> = desktop::load_applications(locale, false, None).collect();
for app in &all_apps {
//TODO: just collect apps that can be executed with a file argument?
if !app.mime_types.is_empty() {
self.apps.push(MimeApp::from(app));
}
for mime in &app.mime_types {
let apps = self
.cache
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| x.id == app.id) {
apps.push(MimeApp::from(app));
}
}
for category in &app.categories {
if category == "TerminalEmulator" {
self.terminals.push(MimeApp::from(app));
break;
}
}
}
let mut list = cosmic_mime_apps::List::default(); let mut list = cosmic_mime_apps::List::default();
let paths = cosmic_mime_apps::list_paths(); let paths = cosmic_mime_apps::list_paths();
list.load_from_paths(&paths); list.load_from_paths(&paths);
let locales = fde::get_languages_from_env();
for desktop_entry in fde::Iter::new(fde::default_paths()).entries(Some(&locales)) {
let name = desktop_entry
.name(&locales)
.unwrap_or_else(|| Cow::Borrowed(desktop_entry.id()));
for (mime, filenames) in list let app = Arc::new(MimeApp {
.added_associations id: desktop_entry.appid.clone(),
.iter() path: Some(desktop_entry.path.clone()),
.chain(list.default_apps.iter()) name: name.into(),
{ exec: desktop_entry.exec().map(String::from),
for filename in filenames { icon: {
log::trace!("add {mime}={filename}"); let icon = desktop_entry.icon().unwrap_or_default();
let apps = self if icon.starts_with('/') {
.cache cosmic::widget::icon::from_path(PathBuf::from(icon))
.entry(mime.clone())
.or_insert_with(|| Vec::with_capacity(1));
if !apps.iter().any(|x| filename_eq(&x.path, filename)) {
if let Some(app) = all_apps.iter().find(|&x| filename_eq(&x.path, filename)) {
apps.push(MimeApp::from(app));
} else { } else {
log::info!( cosmic::widget::icon::from_name(icon).size(32).handle()
"failed to add association for {mime:?}: application {filename:?} not found" }
); },
is_default: Arc::new(AtomicBool::new(false)),
});
tracing::info!(target: "mime-apps", id = app.id, "detected desktop entry");
self.apps.push(app.clone());
if desktop_entry
.categories()
.into_iter()
.flatten()
.any(|c| c == "TerminalEmulator")
{
self.terminals.push(app.clone());
}
// Cache associations defined by the desktop entry.
let mime_types = desktop_entry.mime_type().unwrap_or_else(Vec::new);
for mime in mime_types.iter().filter_map(|m| m.parse::<Mime>().ok()) {
let apps = self.cache.entry(mime.clone()).or_default();
if apps.iter().all(|cached_app| cached_app.id != app.id) {
apps.push(app.clone());
}
}
}
// Cache added associations from mimeapps lists.
for (added_mime, added_apps) in &list.added_associations {
for added_app in added_apps {
if let Some(app) = self
.apps
.iter()
.find(|cached| cached.id.as_str() == added_app.as_ref())
{
let apps = self.cache.entry(added_mime.clone()).or_default();
if apps.iter().all(|cached_app| cached_app.id != app.id) {
apps.push(app.clone());
} }
} }
} }
} }
for (mime, filenames) in list.removed_associations.iter() { // Remove associations
for filename in filenames { for (removed_mime, removed_apps) in &list.removed_associations {
log::trace!("remove {mime}={filename}"); for removed_app in removed_apps {
if let Some(apps) = self.cache.get_mut(mime) { if let Some(app) = self
apps.retain(|x| !filename_eq(&x.path, filename)); .apps
.iter()
.find(|cached| cached.id.as_str() == removed_app.as_ref())
&& let Some(apps) = self.cache.get_mut(removed_mime)
{
apps.retain(|cached_app| cached_app.id != app.id);
} }
} }
} }
for (mime, filenames) in list.default_apps.iter() { // Fetch defaults and sort apps by their default precedence.
for filename in filenames { for (mime, mut apps) in std::mem::take(&mut self.cache).into_iter() {
log::trace!("default {mime}={filename}"); let defaults = list.default_app_for(&mime);
if let Some(apps) = self.cache.get_mut(mime) { let cache = self
let mut found = false; .cache
for app in apps.iter_mut() { .entry(mime.clone())
if filename_eq(&app.path, filename) { .or_insert_with(|| Vec::with_capacity(apps.len()));
app.is_default = true;
found = true; // Sort cached apps for this mime by default precedence.
} else { for default in defaults.into_iter().flatten() {
app.is_default = false; let default = default.strip_suffix(".desktop").unwrap_or(default.as_ref());
} apps.retain(|app| {
} let found = app.id.as_str() == default;
if found { if found {
break; app.is_default.store(true, atomic::Ordering::Relaxed);
cache.push(app.clone());
} }
log::debug!(
"failed to set default for {mime:?}: application {filename:?} not found"
);
}
}
}
// Sort apps by name !found
self.apps });
.sort_by(|a, b| match (a.is_default, b.is_default) { }
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater, // Sort remaining apps by name
_ => LANGUAGE_SORTER.compare(&a.name, &b.name), apps.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.name, &b.name));
}); cache.extend_from_slice(&apps);
for apps in self.cache.values_mut() {
apps.sort_by(|a, b| match (a.is_default, b.is_default) { tracing::debug!(target: "mime-apps", mime = mime.essence_str(), apps = ?(cache.iter().map(|app| &*app.id).collect::<Vec<&str>>()), "mime defaults found")
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
});
} }
// Copy icons to special cache // Copy icons to special cache
@ -329,14 +332,14 @@ impl MimeAppCache {
})); }));
let elapsed = start.elapsed(); let elapsed = start.elapsed();
log::info!("loaded mime app cache in {elapsed:?}"); tracing::info!(target: "mime-apps", "loaded mime app cache in {elapsed:?}");
} }
pub fn apps(&self) -> &[MimeApp] { pub fn apps(&self) -> &[Arc<MimeApp>] {
&self.apps &self.apps
} }
pub fn get(&self, key: &Mime) -> &[MimeApp] { pub fn get(&self, key: &Mime) -> &[Arc<MimeApp>] {
self.cache.get(key).map_or(&[], Vec::as_slice) self.cache.get(key).map_or(&[], Vec::as_slice)
} }
@ -359,7 +362,7 @@ impl MimeAppCache {
.map(|string| string.trim().replace(".desktop", "")) .map(|string| string.trim().replace(".desktop", ""))
} }
pub fn terminal(&self) -> Option<&MimeApp> { pub fn terminal(&self) -> Option<&Arc<MimeApp>> {
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
// The current approach works but might not adhere to the spec (yet) // The current approach works but might not adhere to the spec (yet)

View file

@ -2374,8 +2374,11 @@ impl Item {
widget::settings::item::builder(fl!("open-with")).control( widget::settings::item::builder(fl!("open-with")).control(
Element::from( Element::from(
widget::dropdown( widget::dropdown(
mime_apps, mime_apps
mime_apps.iter().position(|x| x.is_default), .iter()
.map(|app| Cow::Owned(app.name.clone()))
.collect::<Vec<Cow<'static, str>>>(),
mime_apps.iter().position(|x| x.is_default()),
move |index| index, move |index| index,
) )
.icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))), .icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))),
@ -6455,8 +6458,11 @@ impl Tab {
widget::settings::item::builder(fl!("open-with")).control( widget::settings::item::builder(fl!("open-with")).control(
Element::from( Element::from(
widget::dropdown( widget::dropdown(
mime_apps, mime_apps
mime_apps.iter().position(|x| x.is_default), .iter()
.map(|app| Cow::Owned(app.name.clone()))
.collect::<Vec<Cow<'static, str>>>(),
mime_apps.iter().position(|x| x.is_default()),
move |index| (index, mime_closure.clone()), move |index| (index, mime_closure.clone()),
) )
.icons(Cow::Borrowed(mime_app_cache.icons(&mime))), .icons(Cow::Borrowed(mime_app_cache.icons(&mime))),