git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit224e31d04351f423ea790cb1508f3fe564e11de2
parente8295f963c
authorLucas Galante <[email protected]>
date2026-09-08 15:14
default apps: one xdg-mime call per pick; report a default only when every type agrees

Picks spawned one xdg-mime process per MIME type in parallel. xdg-mime
rewrites ~/.config/mimeapps.list through a shared mimeapps.list.new temp
file, so concurrent runs clobber each other and drop writes: a browser
pick left text/html unset, which fell through to cce-browser via
mimeinfo.cache. Chrome's own check (xdg-settings check default-web-browser)
needs http, https and text/html to all agree, so Chrome kept saying it
was not the default while this page - which read only the first type -
said it was.

Apply a pick with ONE xdg-mime invocation naming every type, awaited and
logged on failure; and query every type when refreshing, showing 'not
set' when they disagree so a partial set is visible and re-picking
repairs it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/pages/default_apps.rs | 59 ++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 48 insertions(+), 11 deletions(-)

diff --git a/src/pages/default_apps.rs b/src/pages/default_apps.rs
index 07d25bc..f8e0053 100644
--- a/src/pages/default_apps.rs
+++ b/src/pages/default_apps.rs
@@ -1,7 +1,8 @@
 //! XDG default applications: curated categories over `~/.config/mimeapps.list`.
 //! Candidates come from the installed `.desktop` entries that claim the
-//! category's MIME types; picks are applied through `xdg-mime default`, one
-//! call per type, so a browser pick covers http/https/text-html at once.
+//! category's MIME types; picks are applied through ONE `xdg-mime default`
+//! call naming every type, so a browser pick covers http/https/text-html at
+//! once (and atomically — parallel calls clobber each other's writes).
 //!
 //! The Terminal row is the one non-MIME category: terminals have no MIME type,
 //! so candidates come from entries declaring `Categories=TerminalEmulator`,
@@ -163,11 +164,31 @@ pub fn update(state: &mut DefaultAppsState, msg: DefaultAppsMessage) {
             log::info!("[default_apps] applying {:?} -> {id}", entry.label);
             match entry.kind {
                 CategoryKind::Mime(mimes) => {
-                    for mime in mimes {
-                        let _ = tokio::process::Command::new("xdg-mime")
-                            .args(["default", &id, mime])
-                            .spawn();
-                    }
+                    // ONE xdg-mime invocation for every type. Parallel
+                    // invocations race on the shared `mimeapps.list.new`
+                    // temp file and drop each other's writes: a browser pick
+                    // left text/html unset (falling through to whatever
+                    // mimeinfo.cache lists first), so Chrome's
+                    // `xdg-settings check default-web-browser` said "no"
+                    // while this page — reading only the first type — said
+                    // Chrome.
+                    let id = id.clone();
+                    tokio::spawn(async move {
+                        let out = tokio::process::Command::new("xdg-mime")
+                            .arg("default")
+                            .arg(&id)
+                            .args(mimes)
+                            .output()
+                            .await;
+                        match out {
+                            Ok(o) if o.status.success() => {}
+                            Ok(o) => log::error!(
+                                "[default_apps] xdg-mime default {id} failed: {}",
+                                String::from_utf8_lossy(&o.stderr).trim()
+                            ),
+                            Err(e) => log::error!("[default_apps] xdg-mime spawn failed: {e}"),
+                        }
+                    });
                 }
                 CategoryKind::Terminal => set_default_terminal(&id),
             }
@@ -318,14 +339,30 @@ pub async fn fetch_default_apps() -> DefaultAppsInfo {
     DefaultAppsInfo(cats)
 }
 
-async fn fetch_mime_category(apps: &HashMap<String, DesktopApp>, mimes: &[&str]) -> CategoryInfo {
-    let current = tokio::process::Command::new("xdg-mime")
-        .args(["query", "default", mimes[0]])
+async fn query_default(mime: &str) -> Option<String> {
+    tokio::process::Command::new("xdg-mime")
+        .args(["query", "default", mime])
         .output()
         .await
         .ok()
         .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
-        .filter(|s| !s.is_empty());
+        .filter(|s| !s.is_empty())
+}
+
+async fn fetch_mime_category(apps: &HashMap<String, DesktopApp>, mimes: &[&str]) -> CategoryInfo {
+    // The category's default is only real if EVERY type agrees — a partial
+    // set (one type lost to the write race above, or set by hand) shows as
+    // "not set" so re-picking repairs it, instead of reporting an app the
+    // other types don't actually resolve to.
+    let mut current = query_default(mimes[0]).await;
+    for mime in &mimes[1..] {
+        if current.is_none() {
+            break;
+        }
+        if query_default(mime).await != current {
+            current = None;
+        }
+    }
 
     let mut candidates: Vec<(String, String)> = apps
         .iter()