diff --git a/src/core/fuse.rs b/src/core/fuse.rs index 2693060..f641c51 100644 --- a/src/core/fuse.rs +++ b/src/core/fuse.rs @@ -307,6 +307,53 @@ fn extract_os_field(content: &str, key: &str) -> Option { None } +/// Get the package install command suitable for running via pkexec (no sudo prefix). +pub fn get_fuse_install_command() -> Option { + let content = std::fs::read_to_string("/etc/os-release").ok()?; + let id = extract_os_field(&content, "ID"); + let version_id = extract_os_field(&content, "VERSION_ID"); + let id_like = extract_os_field(&content, "ID_LIKE"); + + match id.as_deref() { + Some("ubuntu") => { + let ver: f64 = version_id + .as_deref() + .and_then(|v| v.parse().ok()) + .unwrap_or(0.0); + if ver >= 24.04 { + Some("apt install -y libfuse2t64".to_string()) + } else { + Some("apt install -y libfuse2".to_string()) + } + } + Some("debian") => Some("apt install -y libfuse2".to_string()), + Some("fedora") => Some("dnf install -y fuse-libs".to_string()), + Some("arch") | Some("manjaro") | Some("endeavouros") => { + Some("pacman -S --noconfirm fuse2".to_string()) + } + Some("opensuse-tumbleweed") | Some("opensuse-leap") => { + Some("zypper install -y libfuse2".to_string()) + } + _ => { + if let Some(like) = id_like.as_deref() { + if like.contains("ubuntu") || like.contains("debian") { + return Some("apt install -y libfuse2".to_string()); + } + if like.contains("fedora") { + return Some("dnf install -y fuse-libs".to_string()); + } + if like.contains("arch") { + return Some("pacman -S --noconfirm fuse2".to_string()); + } + if like.contains("suse") { + return Some("zypper install -y libfuse2".to_string()); + } + } + None + } + } +} + /// Check if AppImageLauncher is installed (known conflicts with new runtime). pub fn detect_appimagelauncher() -> Option { let output = Command::new("dpkg") diff --git a/src/ui/dashboard.rs b/src/ui/dashboard.rs index e16b482..9732134 100644 --- a/src/ui/dashboard.rs +++ b/src/ui/dashboard.rs @@ -26,6 +26,18 @@ pub fn build_dashboard_page(db: &Rc) -> adw::NavigationPage { .margin_end(18) .build(); + // FUSE warning banner if not functional + let fuse_info = fuse::detect_system_fuse(); + if !fuse_info.status.is_functional() { + let banner = adw::Banner::builder() + .title("FUSE is not working - some AppImages may not launch") + .button_label("Fix Now") + .revealed(true) + .build(); + banner.set_action_name(Some("win.fix-fuse")); + content.append(&banner); + } + // Section 1: System Status content.append(&build_system_status_group()); diff --git a/src/ui/fuse_wizard.rs b/src/ui/fuse_wizard.rs new file mode 100644 index 0000000..af1a876 --- /dev/null +++ b/src/ui/fuse_wizard.rs @@ -0,0 +1,177 @@ +use adw::prelude::*; +use gtk::gio; + +use crate::core::fuse; +use crate::i18n::i18n; + +/// Show a FUSE installation wizard dialog. +pub fn show_fuse_wizard(parent: &impl IsA) { + let system_info = fuse::detect_system_fuse(); + if system_info.status.is_functional() { + let dialog = adw::AlertDialog::builder() + .heading(&i18n("FUSE is working")) + .body(&i18n("libfuse2 is installed and functional. No action needed.")) + .build(); + dialog.add_response("ok", &i18n("OK")); + dialog.present(Some(parent)); + return; + } + + let install_cmd = match fuse::get_fuse_install_command() { + Some(cmd) => cmd, + None => { + let dialog = adw::AlertDialog::builder() + .heading(&i18n("Unknown distribution")) + .body(&i18n( + "Could not detect your Linux distribution. Please install libfuse2 manually using your package manager.", + )) + .build(); + dialog.add_response("ok", &i18n("OK")); + dialog.present(Some(parent)); + return; + } + }; + + let dialog = adw::Dialog::builder() + .title(&i18n("Install FUSE")) + .content_width(480) + .content_height(350) + .build(); + + let toolbar = adw::ToolbarView::new(); + let header = adw::HeaderBar::new(); + toolbar.add_top_bar(&header); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(18) + .margin_start(24) + .margin_end(24) + .margin_top(18) + .margin_bottom(18) + .build(); + + let title = gtk::Label::builder() + .label(&i18n("FUSE is required")) + .xalign(0.0) + .build(); + title.add_css_class("title-3"); + content.append(&title); + + let explanation = gtk::Label::builder() + .label(&i18n( + "Most AppImages require libfuse2 to mount and run. \ + Without it, apps will use a slower extract-and-run fallback or may not launch at all.", + )) + .wrap(true) + .xalign(0.0) + .build(); + content.append(&explanation); + + // Show the detected issue + let issue_label = gtk::Label::builder() + .label(&format!("Status: {}", system_info.status.label())) + .xalign(0.0) + .build(); + issue_label.add_css_class("heading"); + content.append(&issue_label); + + // Show the command + let cmd_label = gtk::Label::builder() + .label(&format!("Command: {}", install_cmd)) + .xalign(0.0) + .selectable(true) + .build(); + cmd_label.add_css_class("monospace"); + content.append(&cmd_label); + + // Status label for result + let status_label = gtk::Label::builder() + .xalign(0.0) + .wrap(true) + .visible(false) + .build(); + content.append(&status_label); + + // Install button + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(12) + .halign(gtk::Align::End) + .margin_top(12) + .build(); + + let install_btn = gtk::Button::builder() + .label(&i18n("Install via pkexec")) + .build(); + install_btn.add_css_class("suggested-action"); + install_btn.add_css_class("pill"); + + button_box.append(&install_btn); + content.append(&button_box); + + toolbar.set_content(Some(&content)); + dialog.set_child(Some(&toolbar)); + + let cmd = install_cmd.clone(); + let status_ref = status_label.clone(); + let btn_ref = install_btn.clone(); + install_btn.connect_clicked(move |_| { + btn_ref.set_sensitive(false); + status_ref.set_visible(true); + status_ref.set_label(&i18n("Running installation...")); + + let cmd = cmd.clone(); + let status = status_ref.clone(); + let btn = btn_ref.clone(); + glib::spawn_future_local(async move { + let result = gio::spawn_blocking(move || { + // Split command into program and args + let parts: Vec<&str> = cmd.split_whitespace().collect(); + if parts.is_empty() { + return Err("Empty command".to_string()); + } + let program = parts[0]; + let args = &parts[1..]; + + std::process::Command::new("pkexec") + .arg(program) + .args(args) + .status() + .map_err(|e| format!("Failed to run pkexec: {}", e)) + }) + .await; + + match result { + Ok(Ok(exit)) if exit.success() => { + // Verify FUSE is now working + let fuse_info = fuse::detect_system_fuse(); + if fuse_info.status.is_functional() { + status.set_label(&i18n("FUSE installed successfully! AppImages should now mount natively.")); + status.add_css_class("success"); + } else { + status.set_label(&i18n("Installation completed but FUSE still not detected. A reboot may be required.")); + status.add_css_class("warning"); + } + } + Ok(Ok(_)) => { + status.set_label(&i18n("Installation was cancelled or failed.")); + status.add_css_class("error"); + btn.set_sensitive(true); + } + Ok(Err(e)) => { + status.set_label(&format!("Error: {}", e)); + status.add_css_class("error"); + btn.set_sensitive(true); + } + Err(_) => { + status.set_label(&i18n("Task failed unexpectedly.")); + status.add_css_class("error"); + btn.set_sensitive(true); + } + } + }); + }); + + dialog.present(Some(parent)); +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 34aca71..28edc39 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,6 +5,7 @@ pub mod dashboard; pub mod detail_view; pub mod drop_dialog; pub mod duplicate_dialog; +pub mod fuse_wizard; pub mod integration_dialog; pub mod library_view; pub mod preferences; diff --git a/src/window.rs b/src/window.rs index 7f09242..f5ffe4b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -677,6 +677,16 @@ impl DriftwoodWindow { } self.add_action(&update_all_action); + let fix_fuse_action = gio::SimpleAction::new("fix-fuse", None); + { + let window_weak = self.downgrade(); + fix_fuse_action.connect_activate(move |_, _| { + let Some(window) = window_weak.upgrade() else { return }; + crate::ui::fuse_wizard::show_fuse_wizard(&window); + }); + } + self.add_action(&fix_fuse_action); + // --- Context menu actions (parameterized with record ID) --- let param_type = Some(glib::VariantTy::INT64);