From c6220578302f974d8198f1e8858a9a442386cae4 Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 28 Feb 2026 00:10:39 +0200 Subject: [PATCH] Add default application selector for browsers, email, and file managers --- src/core/integrator.rs | 140 +++++++++++++++++++++++++++++++++++++++++ src/ui/detail_view.rs | 56 +++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/src/core/integrator.rs b/src/core/integrator.rs index 6a22a9c..b3baaae 100644 --- a/src/core/integrator.rs +++ b/src/core/integrator.rs @@ -332,6 +332,28 @@ pub fn undo_all_modifications(db: &Database, appimage_id: i64) -> Result<(), Str .status(); } } + "default_app" => { + if let Some(ref prev) = m.previous_value { + match m.file_path.as_str() { + "web-browser" => { + let _ = Command::new("xdg-settings") + .args(["set", "default-web-browser", prev]) + .status(); + } + "email-client" => { + let _ = Command::new("xdg-settings") + .args(["set", "default-url-scheme-handler", "mailto", prev]) + .status(); + } + "file-manager" => { + let _ = Command::new("xdg-mime") + .args(["default", prev, "inode/directory"]) + .status(); + } + _ => {} + } + } + } "system_desktop" | "system_icon" | "system_binary" => { let _ = Command::new("pkexec") .args(["rm", "-f", &m.file_path]) @@ -395,6 +417,124 @@ pub fn set_mime_default( Ok(()) } +/// A system-level default application type that an AppImage can serve as. +#[derive(Debug, Clone, PartialEq)] +pub enum DefaultAppType { + WebBrowser, + EmailClient, + FileManager, +} + +impl DefaultAppType { + pub fn label(&self) -> &'static str { + match self { + Self::WebBrowser => "Web browser", + Self::EmailClient => "Email client", + Self::FileManager => "File manager", + } + } +} + +/// Detect which system-default roles an AppImage can fill based on its categories. +pub fn detect_default_capabilities(categories: &str) -> Vec { + let mut caps = Vec::new(); + if categories.contains("WebBrowser") { + caps.push(DefaultAppType::WebBrowser); + } + if categories.contains("Email") { + caps.push(DefaultAppType::EmailClient); + } + if categories.contains("FileManager") { + caps.push(DefaultAppType::FileManager); + } + caps +} + +/// Set an AppImage as a system-default application for the given role. +/// Stores the previous default in system_modifications for reversal. +pub fn set_default_app( + db: &Database, + appimage_id: i64, + app_id: &str, + cap: &DefaultAppType, +) -> Result<(), String> { + let desktop_filename = format!("driftwood-{}.desktop", app_id); + + match cap { + DefaultAppType::WebBrowser => { + let prev = Command::new("xdg-settings") + .args(["get", "default-web-browser"]) + .output() + .ok() + .and_then(|o| { + if o.status.success() { + let s = String::from_utf8_lossy(&o.stdout).trim().to_string(); + if s.is_empty() { None } else { Some(s) } + } else { + None + } + }); + + let status = Command::new("xdg-settings") + .args(["set", "default-web-browser", &desktop_filename]) + .status() + .map_err(|e| format!("xdg-settings failed: {}", e))?; + if !status.success() { + return Err("xdg-settings returned non-zero".to_string()); + } + db.register_modification(appimage_id, "default_app", "web-browser", prev.as_deref()).ok(); + } + DefaultAppType::EmailClient => { + let prev = Command::new("xdg-settings") + .args(["get", "default-url-scheme-handler", "mailto"]) + .output() + .ok() + .and_then(|o| { + if o.status.success() { + let s = String::from_utf8_lossy(&o.stdout).trim().to_string(); + if s.is_empty() { None } else { Some(s) } + } else { + None + } + }); + + let status = Command::new("xdg-settings") + .args(["set", "default-url-scheme-handler", "mailto", &desktop_filename]) + .status() + .map_err(|e| format!("xdg-settings failed: {}", e))?; + if !status.success() { + return Err("xdg-settings returned non-zero".to_string()); + } + db.register_modification(appimage_id, "default_app", "email-client", prev.as_deref()).ok(); + } + DefaultAppType::FileManager => { + let prev = Command::new("xdg-mime") + .args(["query", "default", "inode/directory"]) + .output() + .ok() + .and_then(|o| { + if o.status.success() { + let s = String::from_utf8_lossy(&o.stdout).trim().to_string(); + if s.is_empty() { None } else { Some(s) } + } else { + None + } + }); + + let status = Command::new("xdg-mime") + .args(["default", &desktop_filename, "inode/directory"]) + .status() + .map_err(|e| format!("xdg-mime failed: {}", e))?; + if !status.success() { + return Err("xdg-mime returned non-zero".to_string()); + } + db.register_modification(appimage_id, "default_app", "file-manager", prev.as_deref()).ok(); + } + } + + Ok(()) +} + fn update_desktop_database() { let apps_dir = applications_dir(); Command::new("update-desktop-database") diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index b7f6ea1..d1457a6 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -1123,6 +1123,62 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: & } } + // Default Application group + if let Some(ref cats) = record.categories { + let caps = integrator::detect_default_capabilities(cats); + if !caps.is_empty() && record.integrated { + let default_group = adw::PreferencesGroup::builder() + .title("Default Application") + .description( + "Set this app as your system default for tasks it can handle." + ) + .build(); + + let app_id = integrator::make_app_id( + record.app_name.as_deref().unwrap_or(&record.filename), + ); + + for cap in &caps { + let row = adw::ActionRow::builder() + .title(cap.label()) + .build(); + + let set_btn = gtk::Button::builder() + .label("Set Default") + .valign(gtk::Align::Center) + .build(); + set_btn.add_css_class("flat"); + + let db_def = db.clone(); + let record_id = record.id; + let app_id_clone = app_id.clone(); + let cap_clone = cap.clone(); + let toast_def = toast_overlay.clone(); + set_btn.connect_clicked(move |btn| { + match integrator::set_default_app( + &db_def, record_id, &app_id_clone, &cap_clone, + ) { + Ok(()) => { + toast_def.add_toast(adw::Toast::new( + &format!("Set as default {}", cap_clone.label().to_lowercase()), + )); + btn.set_sensitive(false); + btn.set_label("Default"); + } + Err(e) => { + log::error!("Failed to set default app: {}", e); + toast_def.add_toast(adw::Toast::new("Failed to set default")); + } + } + }); + + row.add_suffix(&set_btn); + default_group.add(&row); + } + inner.append(&default_group); + } + } + // Runtime Compatibility group let compat_group = adw::PreferencesGroup::builder() .title("Compatibility")