diff --git a/Cargo.lock b/Cargo.lock index a82f88f..49d57f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index afee51c..69125d9 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -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" diff --git a/cosmic-settings/src/pages/applications/default_apps.rs b/cosmic-settings/src/pages/applications/default_apps.rs index 8512ab9..51da084 100644 --- a/cosmic-settings/src/pages/applications/default_apps.rs +++ b/cosmic-settings/src/pages/applications/default_apps.rs @@ -2,11 +2,6 @@ // Copyright 2024 bbb651 // 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), Surface(surface::Action), } @@ -63,13 +66,11 @@ impl From 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, - pub known_mimes: BTreeSet, - pub config_path: Box, + local_list: Option<(mime_apps::List, tokio::fs::File)>, + apps: Vec, + known_mimes: BTreeSet, } #[derive(Clone, Debug)] @@ -80,11 +81,12 @@ pub struct AppMeta { icons: Vec, } -#[derive(Clone, Debug, Default)] +#[derive(Debug, Default)] pub struct Page { on_enter_handle: Option, mime_apps: Option, shortcuts_config: Option, + update_config: Option>, } impl page::AutoBind for Page {} @@ -108,46 +110,45 @@ impl page::Page 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>, for_mimes: &[&str]) -> AppMeta { +fn load_defaults( + list: &mime_apps::List, + assocs: &BTreeMap, Arc>, + 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>, 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"));