feat: improve mime app detection & do it faster
This commit is contained in:
parent
bf01bb31a3
commit
53cfeebe57
3 changed files with 121 additions and 110 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
219
src/mime_app.rs
219
src/mime_app.rs
|
|
@ -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,109 +215,113 @@ 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),
|
||||||
|
icon: {
|
||||||
|
let icon = desktop_entry.icon().unwrap_or_default();
|
||||||
|
if icon.starts_with('/') {
|
||||||
|
cosmic::widget::icon::from_path(PathBuf::from(icon))
|
||||||
|
} else {
|
||||||
|
cosmic::widget::icon::from_name(icon).size(32).handle()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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")
|
||||||
{
|
{
|
||||||
for filename in filenames {
|
self.terminals.push(app.clone());
|
||||||
log::trace!("add {mime}={filename}");
|
}
|
||||||
let apps = self
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove associations
|
||||||
|
for (removed_mime, removed_apps) in &list.removed_associations {
|
||||||
|
for removed_app in removed_apps {
|
||||||
|
if let Some(app) = self
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch defaults and sort apps by their default precedence.
|
||||||
|
for (mime, mut apps) in std::mem::take(&mut self.cache).into_iter() {
|
||||||
|
let defaults = list.default_app_for(&mime);
|
||||||
|
let cache = self
|
||||||
.cache
|
.cache
|
||||||
.entry(mime.clone())
|
.entry(mime.clone())
|
||||||
.or_insert_with(|| Vec::with_capacity(1));
|
.or_insert_with(|| Vec::with_capacity(apps.len()));
|
||||||
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 {
|
|
||||||
log::info!(
|
|
||||||
"failed to add association for {mime:?}: application {filename:?} not found"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (mime, filenames) in list.removed_associations.iter() {
|
// Sort cached apps for this mime by default precedence.
|
||||||
for filename in filenames {
|
for default in defaults.into_iter().flatten() {
|
||||||
log::trace!("remove {mime}={filename}");
|
let default = default.strip_suffix(".desktop").unwrap_or(default.as_ref());
|
||||||
if let Some(apps) = self.cache.get_mut(mime) {
|
apps.retain(|app| {
|
||||||
apps.retain(|x| !filename_eq(&x.path, filename));
|
let found = app.id.as_str() == default;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (mime, filenames) in list.default_apps.iter() {
|
|
||||||
for filename in filenames {
|
|
||||||
log::trace!("default {mime}={filename}");
|
|
||||||
if let Some(apps) = self.cache.get_mut(mime) {
|
|
||||||
let mut found = false;
|
|
||||||
for app in apps.iter_mut() {
|
|
||||||
if filename_eq(&app.path, filename) {
|
|
||||||
app.is_default = true;
|
|
||||||
found = true;
|
|
||||||
} else {
|
|
||||||
app.is_default = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
|
|
||||||
});
|
|
||||||
for apps in self.cache.values_mut() {
|
|
||||||
apps.sort_by(|a, b| match (a.is_default, b.is_default) {
|
|
||||||
(true, false) => Ordering::Less,
|
|
||||||
(false, true) => Ordering::Greater,
|
|
||||||
_ => LANGUAGE_SORTER.compare(&a.name, &b.name),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort remaining apps by name
|
||||||
|
apps.sort_by(|a, b| LANGUAGE_SORTER.compare(&a.name, &b.name));
|
||||||
|
cache.extend_from_slice(&apps);
|
||||||
|
|
||||||
|
tracing::debug!(target: "mime-apps", mime = mime.essence_str(), apps = ?(cache.iter().map(|app| &*app.id).collect::<Vec<&str>>()), "mime defaults found")
|
||||||
|
}
|
||||||
|
|
||||||
// Copy icons to special cache
|
// Copy icons to special cache
|
||||||
//TODO: adjust dropdown API so this is no longer needed
|
//TODO: adjust dropdown API so this is no longer needed
|
||||||
self.icons.extend(self.cache.iter().map(|(mime, apps)| {
|
self.icons.extend(self.cache.iter().map(|(mime, apps)| {
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
14
src/tab.rs
14
src/tab.rs
|
|
@ -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))),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue