diff --git a/src/app.rs b/src/app.rs index 7edc5db..44f4b0c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -448,6 +448,7 @@ pub enum Message { Option>, ), TabView(Option, tab::View), + TerminalDefault(Box), TimeConfigChange(TimeConfig), ToggleContextPage(ContextPage), ToggleFoldersFirst, @@ -559,7 +560,7 @@ pub enum DialogPage { path: PathBuf, mime: mime_guess::Mime, selected: usize, - store_opt: Option, + store_opt: Option>, }, PermanentlyDelete { paths: Box<[PathBuf]>, @@ -2273,7 +2274,7 @@ impl App { .into() } - fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> { + fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&Arc, MimeAppMatch)> { let mut results = Vec::new(); let mut dedupe = FxHashSet::default(); @@ -4793,6 +4794,7 @@ impl Application for App { tab.refresh_cut(&paths); } } + Message::TerminalDefault(command) => {} Message::TimeConfigChange(time_config) => { self.config.tab.military_time = time_config.military_time; return self.update_config(); @@ -5982,7 +5984,7 @@ impl Application for App { widget::button::custom( widget::row::with_children([ icon(app.icon.clone()).size(32).into(), - if app.is_default && !displayed_default { + if app.is_default() && !displayed_default { displayed_default = true; widget::text::body(fl!( "default-app", diff --git a/src/mime_app.rs b/src/mime_app.rs index 8695d79..c830ab7 100644 --- a/src/mime_app.rs +++ b/src/mime_app.rs @@ -7,10 +7,11 @@ use cosmic::desktop; use cosmic::widget; pub use mime_guess::Mime; use rustc_hash::FxHashMap; -use std::cmp::Ordering; 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::time::Instant; use std::{fs, io, process}; @@ -133,7 +134,7 @@ pub struct MimeApp { pub name: String, pub exec: Option, pub icon: widget::icon::Handle, - pub is_default: bool, + is_default: Arc, } impl MimeApp { @@ -146,6 +147,10 @@ impl MimeApp { path_opt, ) } + + pub fn is_default(&self) -> bool { + self.is_default.load(atomic::Ordering::SeqCst) + } } // 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()), }, - is_default: false, + is_default: Arc::new(AtomicBool::new(false)), } } } -#[cfg(feature = "desktop")] -fn filename_eq(path_opt: &Option, filename: &str) -> bool { - path_opt - .as_ref() - .and_then(|path| path.file_name()) - .is_some_and(|x| x == filename) -} - pub struct MimeAppCache { - apps: Vec, - cache: FxHashMap>, + apps: Vec>, + cache: FxHashMap>>, icons: FxHashMap>, - terminals: Vec, + terminals: Vec>, } impl MimeAppCache { @@ -204,10 +201,12 @@ impl MimeAppCache { #[cfg(not(feature = "desktop"))] 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")] pub fn reload(&mut self) { use crate::localize::LANGUAGE_SORTER; + use cosmic::desktop::fde; + use std::borrow::Cow; let start = Instant::now(); @@ -216,107 +215,111 @@ impl MimeAppCache { self.icons.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 paths = cosmic_mime_apps::list_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 - .added_associations - .iter() - .chain(list.default_apps.iter()) - { - for filename in filenames { - log::trace!("add {mime}={filename}"); - let apps = self - .cache - .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)); + let app = Arc::new(MimeApp { + id: desktop_entry.appid.clone(), + path: Some(desktop_entry.path.clone()), + 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 { - log::info!( - "failed to add association for {mime:?}: application {filename:?} not found" - ); + 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") + { + 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::().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() { - for filename in filenames { - log::trace!("remove {mime}={filename}"); - if let Some(apps) = self.cache.get_mut(mime) { - apps.retain(|x| !filename_eq(&x.path, filename)); + // 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); } } } - 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; - } - } + // 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 + .entry(mime.clone()) + .or_insert_with(|| Vec::with_capacity(apps.len())); + + // 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()); + apps.retain(|app| { + let found = app.id.as_str() == default; 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 - 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), - }); + !found + }); + } + + // 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::>()), "mime defaults found") } // Copy icons to special cache @@ -329,14 +332,14 @@ impl MimeAppCache { })); 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] { &self.apps } - pub fn get(&self, key: &Mime) -> &[MimeApp] { + pub fn get(&self, key: &Mime) -> &[Arc] { self.cache.get(key).map_or(&[], Vec::as_slice) } @@ -359,7 +362,7 @@ impl MimeAppCache { .map(|string| string.trim().replace(".desktop", "")) } - pub fn terminal(&self) -> Option<&MimeApp> { + pub fn terminal(&self) -> Option<&Arc> { //TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec // The current approach works but might not adhere to the spec (yet) diff --git a/src/tab.rs b/src/tab.rs index 7d0c8fd..185a258 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -2374,8 +2374,11 @@ impl Item { widget::settings::item::builder(fl!("open-with")).control( Element::from( widget::dropdown( - mime_apps, - mime_apps.iter().position(|x| x.is_default), + mime_apps + .iter() + .map(|app| Cow::Owned(app.name.clone())) + .collect::>>(), + mime_apps.iter().position(|x| x.is_default()), move |index| index, ) .icons(Cow::Borrowed(mime_app_cache.icons(&self.mime))), @@ -6455,8 +6458,11 @@ impl Tab { widget::settings::item::builder(fl!("open-with")).control( Element::from( widget::dropdown( - mime_apps, - mime_apps.iter().position(|x| x.is_default), + mime_apps + .iter() + .map(|app| Cow::Owned(app.name.clone())) + .collect::>>(), + mime_apps.iter().position(|x| x.is_default()), move |index| (index, mime_closure.clone()), ) .icons(Cow::Borrowed(mime_app_cache.icons(&mime))),