Add FUSE fix wizard with pkexec installation
Dashboard shows a banner when FUSE is not functional. The wizard detects the distro, shows the install command, and runs it via pkexec. Verifies FUSE status after installation completes.
This commit is contained in:
@@ -307,6 +307,53 @@ fn extract_os_field(content: &str, key: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the package install command suitable for running via pkexec (no sudo prefix).
|
||||
pub fn get_fuse_install_command() -> Option<String> {
|
||||
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<String> {
|
||||
let output = Command::new("dpkg")
|
||||
|
||||
@@ -26,6 +26,18 @@ pub fn build_dashboard_page(db: &Rc<Database>) -> 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());
|
||||
|
||||
|
||||
177
src/ui/fuse_wizard.rs
Normal file
177
src/ui/fuse_wizard.rs
Normal file
@@ -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<gtk::Widget>) {
|
||||
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));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user