fix(default-apps): missing and wrong associations

* update the cosmic-mime-apps crate with fixes to app association detection
* actually use cosmic-mime-apps for significantly faster  association lookups
This commit is contained in:
Michael Aaron Murphy 2026-05-26 17:30:37 +02:00 committed by Michael Murphy
parent 81912bed6c
commit 2e83e56c09
3 changed files with 91 additions and 61 deletions

6
Cargo.lock generated
View file

@ -1470,12 +1470,14 @@ dependencies = [
[[package]]
name = "cosmic-mime-apps"
version = "0.1.0"
source = "git+https://github.com/pop-os/cosmic-mime-apps#673f3758920a7b20d307d356d586deef714df6b0"
version = "0.2.0"
source = "git+https://github.com/pop-os/cosmic-mime-apps#a95c8043a7b41ca3c143a1fad5b3b8a5a3411008"
dependencies = [
"freedesktop-desktop-entry",
"futures-util",
"mime 0.3.17",
"quick-xml 0.38.4",
"tokio",
"xdg",
]

View file

@ -63,7 +63,8 @@ libcosmic.workspace = true
iced_winit.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
sysinfo = { version = "=0.38.0", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true }
# mime-apps = { package = "cosmic-mime-apps", path = "../../../cosmic-mime-apps", features = ["tokio"], optional = true }
notify = "8.2.0"
regex = "1.12.3"
ron = "0.12"

View file

@ -2,11 +2,6 @@
// Copyright 2024 bbb651 <bar.ye651@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, dropdown, icon, settings};
use cosmic::{Apply, Element, Task, surface};
@ -17,8 +12,16 @@ use cosmic_settings_page::{self as page, Section, section};
use freedesktop_desktop_entry::{
DesktopEntry, Iter as DesktopEntryIter, default_paths, get_languages_from_env,
};
use mime::Mime;
use mime_apps::App;
use slotmap::SlotMap;
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::io::SeekFrom;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
const DROPDOWN_WEB_BROWSER: usize = 0;
const DROPDOWN_FILE_MANAGER: usize = 1;
@ -47,7 +50,7 @@ pub enum Category {
#[derive(Clone, Debug)]
pub enum Message {
SetDefault(Category, usize),
Update(CachedMimeApps),
Update(Arc<CachedMimeApps>),
Surface(surface::Action),
}
@ -63,13 +66,11 @@ impl From<Message> for crate::pages::Message {
}
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct CachedMimeApps {
pub list: mime_apps::List,
pub local_list: mime_apps::List,
pub apps: Vec<AppMeta>,
pub known_mimes: BTreeSet<mime::Mime>,
pub config_path: Box<Path>,
local_list: Option<(mime_apps::List, tokio::fs::File)>,
apps: Vec<AppMeta>,
known_mimes: BTreeSet<mime::Mime>,
}
#[derive(Clone, Debug)]
@ -80,11 +81,12 @@ pub struct AppMeta {
icons: Vec<icon::Handle>,
}
#[derive(Clone, Debug, Default)]
#[derive(Debug, Default)]
pub struct Page {
on_enter_handle: Option<cosmic::iced::task::Handle>,
mime_apps: Option<CachedMimeApps>,
shortcuts_config: Option<cosmic_config::Config>,
update_config: Option<tokio::task::JoinHandle<()>>,
}
impl page::AutoBind<crate::pages::Message> for Page {}
@ -108,46 +110,45 @@ impl page::Page<crate::pages::Message> for Page {
handle.abort();
}
let config_update_handle = self.update_config.take();
if self.shortcuts_config.is_none() {
self.shortcuts_config = cosmic_settings_config::shortcuts::context().ok();
}
let (task, on_enter_handle) = Task::future(async move {
let mut list = mime_apps::List::default();
list.load_from_paths(&mime_apps::list_paths());
let mut local_list = mime_apps::List::default();
if let Some(path) = mime_apps::local_list_path()
&& let Ok(buffer) = std::fs::read_to_string(&path)
{
local_list.load_from(&buffer);
// Wait for previous copy operation to complete.
if let Some(handle) = config_update_handle {
_ = handle.await;
}
let assocs = mime_apps::associations::by_app();
let local_list = mime_apps::load_user_mimeapps().await.ok();
let mut list = mime_apps::List::default();
list.load_from_paths(&mime_apps::list_paths());
let assocs = mime_apps::associations::by_app(&list);
let apps = vec![
load_defaults(&assocs, &["x-scheme-handler/http"]).await,
load_defaults(&assocs, &["inode/directory"]).await,
load_defaults(&assocs, &["x-scheme-handler/mailto"]).await,
load_defaults(&assocs, &["audio/mp3", "application/ogg", "video/mp4"]).await,
load_defaults(&assocs, &["video/mp4"]).await,
load_defaults(&assocs, &["image/png"]).await,
load_defaults(&assocs, &["text/calendar"]).await,
load_defaults(&list, &assocs, &["x-scheme-handler/http"]),
load_defaults(&list, &assocs, &["inode/directory"]),
load_defaults(&list, &assocs, &["x-scheme-handler/mailto"]),
load_defaults(
&list,
&assocs,
&["audio/mpeg", "application/ogg", "audio/x-flac"],
),
load_defaults(&list, &assocs, &["video/mp4"]),
load_defaults(&list, &assocs, &["image/png"]),
load_defaults(&list, &assocs, &["text/calendar"]),
load_terminal_apps(&assocs).await,
load_defaults(&assocs, &["text/plain"]).await,
load_defaults(&list, &assocs, &["text/plain"]),
];
Message::Update(CachedMimeApps {
Message::Update(Arc::new(CachedMimeApps {
apps,
list,
local_list,
known_mimes: mime_apps::mime_info::mime_types(),
config_path: dirs::config_dir()
.expect("config dir not found")
.join("mimeapps.list")
.into(),
})
}))
.into()
})
.abortable();
@ -188,6 +189,7 @@ impl Page {
"application/ogg",
"application/x-cue",
"application/x-ogg",
"audio/mpeg",
"audio/mp3",
"x-content/audio-cdda",
])
@ -238,7 +240,7 @@ impl Page {
let meta = &mut mime_apps.apps[category_id];
if meta.selected != Some(id) {
if meta.selected.is_none_or(|selected| selected != id) {
meta.selected = Some(id);
let appid = &meta.app_ids[id];
@ -249,24 +251,40 @@ impl Page {
assign_default_terminal(config, appid);
}
for mime in mime_types {
if let Ok(mime) = mime.parse() {
mime_apps
.local_list
.set_default_app(mime, [appid, ".desktop"].concat());
};
if let Some((local_list, local_file)) = mime_apps.local_list.as_mut() {
for mime in mime_types {
if let Ok(mime) = mime.parse() {
tracing::info!(target: "default-apps", ?mime, appid, "setting default for mime");
local_list.set_default_app(mime, [appid, ".desktop"].concat());
};
}
if let Some(config_update_handle) = self.update_config.take() {
config_update_handle.abort();
}
let mut buffer = local_list.to_string();
buffer.push('\n');
if let Ok(mut local_file) =
futures::executor::block_on(local_file.try_clone())
{
self.update_config = Some(tokio::spawn(async move {
tracing::debug!(target: "default-apps", buffer, "writing to mimeapps config");
_ = local_file.seek(SeekFrom::Start(0)).await;
_ = local_file.set_len(buffer.len() as u64).await;
_ = local_file.write_all(buffer.as_bytes()).await;
_ = local_file.flush().await;
_ = local_file.seek(SeekFrom::Start(0)).await;
_ = tokio::process::Command::new("update-desktop-database")
.status()
.await;
}));
}
}
let mut buffer = mime_apps.local_list.to_string();
buffer.push('\n');
_ = std::fs::write(&mime_apps.config_path, buffer);
_ = std::process::Command::new("update-desktop-database").status();
}
}
Message::Update(mime_apps) => {
self.mime_apps = Some(mime_apps);
}
Message::Update(mime_apps) => self.mime_apps = Arc::into_inner(mime_apps),
Message::Surface(a) => {
return cosmic::task::message(crate::app::Message::Surface(a));
}
@ -286,7 +304,7 @@ fn app_item(meta: &AppMeta, label: String, category: Category) -> widget::FlexRo
} else {
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
Some(meta.selected.unwrap_or(0)),
move |id| Message::SetDefault(category, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
@ -393,12 +411,16 @@ fn assign_default_terminal(config: &cosmic_config::Config, appid: &str) {
}
}
async fn load_defaults(assocs: &BTreeMap<Arc<str>, Arc<App>>, for_mimes: &[&str]) -> AppMeta {
fn load_defaults(
list: &mime_apps::List,
assocs: &BTreeMap<Arc<str>, Arc<App>>,
for_mimes: &[&str],
) -> AppMeta {
let mut unsorted = Vec::new();
let mut current_app = None;
for for_mime in for_mimes {
let Ok(mime) = for_mime.parse() else {
let Ok(mime) = Mime::from_str(for_mime) else {
return AppMeta {
selected: None,
app_ids: Vec::new(),
@ -407,7 +429,12 @@ async fn load_defaults(assocs: &BTreeMap<Arc<str>, Arc<App>>, for_mimes: &[&str]
};
};
let current_app_entry = xdg_mime_query_default(for_mime).await;
let current_app_entry = dbg!(
list.default_app_for(&mime)
.and_then(|entries| entries.first())
);
tracing::info!(target: "default-apps", ?mime, ?current_app_entry, "entry for mime");
let current_appid = current_app_entry
.as_ref()
.and_then(|entry| entry.strip_suffix(".desktop"));