Add Phase 5 enhancements: security, i18n, analysis, backup, notifications
- Database v8 migration: tags, pinned, avg_startup_ms columns - Security scanning with CVE matching and batch scan - Bundled library extraction and vulnerability reports - Desktop notification system for security alerts - Backup/restore system for AppImage configurations - i18n framework with gettext support - Runtime analysis and Wayland compatibility detection - AppStream metadata and Flatpak-style build support - File watcher module for live directory monitoring - Preferences panel with GSettings integration - CLI interface for headless operation - Detail view: tabbed layout with ViewSwitcher in title bar, health score, sandbox controls, changelog links - Library view: sort dropdown, context menu enhancements - Dashboard: system status, disk usage, launch history - Security report page with scan and export - Packaging: meson build, PKGBUILD, metainfo
This commit is contained in:
@@ -22,32 +22,15 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
// Toast overlay for copy actions
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
|
||||
// Main content container
|
||||
let content = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
|
||||
// Hero banner (always visible at top)
|
||||
let banner = build_banner(record);
|
||||
content.append(&banner);
|
||||
|
||||
// ViewSwitcher (tab bar) - inline style, between banner and tab content
|
||||
// ViewStack for tabbed content
|
||||
let view_stack = adw::ViewStack::new();
|
||||
|
||||
let switcher = adw::ViewSwitcher::builder()
|
||||
.stack(&view_stack)
|
||||
.policy(adw::ViewSwitcherPolicy::Wide)
|
||||
.build();
|
||||
switcher.add_css_class("inline");
|
||||
switcher.add_css_class("detail-view-switcher");
|
||||
content.append(&switcher);
|
||||
|
||||
// Build tab pages
|
||||
let overview_page = build_overview_tab(record, db);
|
||||
view_stack.add_titled(&overview_page, Some("overview"), "Overview");
|
||||
view_stack.page(&overview_page).set_icon_name(Some("info-symbolic"));
|
||||
|
||||
let system_page = build_system_tab(record, db);
|
||||
let system_page = build_system_tab(record, db, &toast_overlay);
|
||||
view_stack.add_titled(&system_page, Some("system"), "System");
|
||||
view_stack.page(&system_page).set_icon_name(Some("system-run-symbolic"));
|
||||
|
||||
@@ -59,18 +42,31 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
view_stack.add_titled(&storage_page, Some("storage"), "Storage");
|
||||
view_stack.page(&storage_page).set_icon_name(Some("drive-harddisk-symbolic"));
|
||||
|
||||
// Scrollable area for tab content
|
||||
// Scrollable view stack
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.child(&view_stack)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
// Main vertical layout: banner + scrolled tabs
|
||||
let content = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
content.append(&build_banner(record));
|
||||
content.append(&scrolled);
|
||||
|
||||
toast_overlay.set_child(Some(&content));
|
||||
|
||||
// Header bar with per-app actions
|
||||
// Header bar with ViewSwitcher as title widget (standard GNOME pattern)
|
||||
let header = adw::HeaderBar::new();
|
||||
|
||||
let switcher = adw::ViewSwitcher::builder()
|
||||
.stack(&view_stack)
|
||||
.policy(adw::ViewSwitcherPolicy::Wide)
|
||||
.build();
|
||||
header.set_title_widget(Some(&switcher));
|
||||
|
||||
// Launch button
|
||||
let launch_button = gtk::Button::builder()
|
||||
.label("Launch")
|
||||
.tooltip_text("Launch this AppImage")
|
||||
@@ -97,11 +93,9 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
let pid = child.id();
|
||||
log::info!("Launched AppImage: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||
|
||||
// Run post-launch Wayland runtime analysis after a short delay
|
||||
let db_wayland = db_launch.clone();
|
||||
let path_clone = path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
// Wait 3 seconds for the process to initialize
|
||||
glib::timeout_future(std::time::Duration::from_secs(3)).await;
|
||||
|
||||
let analysis_result = gio::spawn_blocking(move || {
|
||||
@@ -165,7 +159,10 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Rich banner at top: large icon + app name + version + badges
|
||||
// ---------------------------------------------------------------------------
|
||||
// Banner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_banner(record: &AppImageRecord) -> gtk::Box {
|
||||
let banner = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
@@ -178,7 +175,7 @@ fn build_banner(record: &AppImageRecord) -> gtk::Box {
|
||||
|
||||
let name = record.app_name.as_deref().unwrap_or(&record.filename);
|
||||
|
||||
// Large icon (96x96) with drop shadow
|
||||
// Large icon with drop shadow
|
||||
let icon = widgets::app_icon(record.icon_path.as_deref(), name, 96);
|
||||
icon.set_valign(gtk::Align::Start);
|
||||
icon.add_css_class("icon-dropshadow");
|
||||
@@ -259,7 +256,10 @@ fn build_banner(record: &AppImageRecord) -> gtk::Box {
|
||||
banner
|
||||
}
|
||||
|
||||
/// Tab 1: Overview - most commonly needed info at a glance
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 1: Overview - updates, usage, basic file info
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
let tab = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
@@ -283,6 +283,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
// Updates section
|
||||
let updates_group = adw::PreferencesGroup::builder()
|
||||
.title("Updates")
|
||||
.description("Keep this app up to date by checking for new versions.")
|
||||
.build();
|
||||
|
||||
if let Some(ref update_type) = record.update_type {
|
||||
@@ -291,15 +292,31 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.unwrap_or("Unknown format");
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Update method")
|
||||
.subtitle(display_label)
|
||||
.subtitle(&format!(
|
||||
"This app checks for updates using: {}",
|
||||
display_label
|
||||
))
|
||||
.tooltip_text(
|
||||
"AppImages can include built-in update information that tells Driftwood \
|
||||
where to check for newer versions. Common methods include GitHub releases, \
|
||||
zsync (efficient delta updates), and direct download URLs."
|
||||
)
|
||||
.build();
|
||||
updates_group.add(&row);
|
||||
} else {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Update method")
|
||||
.subtitle("This app cannot check for updates automatically")
|
||||
.subtitle(
|
||||
"This app does not include update information. \
|
||||
You will need to check for new versions manually."
|
||||
)
|
||||
.tooltip_text(
|
||||
"AppImages can include built-in update information that tells Driftwood \
|
||||
where to check for newer versions. This one doesn't have any, so you'll \
|
||||
need to download updates yourself from wherever you got the app."
|
||||
)
|
||||
.build();
|
||||
let badge = widgets::status_badge("None", "neutral");
|
||||
let badge = widgets::status_badge("Manual only", "neutral");
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
row.add_suffix(&badge);
|
||||
updates_group.add(&row);
|
||||
@@ -314,9 +331,9 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
|
||||
if is_newer {
|
||||
let subtitle = format!(
|
||||
"{} -> {}",
|
||||
"A newer version is available: {} (you have {})",
|
||||
latest,
|
||||
record.app_version.as_deref().unwrap_or("unknown"),
|
||||
latest
|
||||
);
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Update available")
|
||||
@@ -328,8 +345,8 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
updates_group.add(&row);
|
||||
} else {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Status")
|
||||
.subtitle("Up to date")
|
||||
.title("Version status")
|
||||
.subtitle("You are running the latest version.")
|
||||
.build();
|
||||
let badge = widgets::status_badge("Latest", "success");
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
@@ -375,20 +392,29 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.build();
|
||||
|
||||
let type_str = match record.appimage_type {
|
||||
Some(1) => "Type 1",
|
||||
Some(2) => "Type 2",
|
||||
_ => "Unknown",
|
||||
Some(1) => "Type 1 (ISO 9660) - older format, still widely supported",
|
||||
Some(2) => "Type 2 (SquashFS) - modern format, most common today",
|
||||
_ => "Unknown type",
|
||||
};
|
||||
let type_row = adw::ActionRow::builder()
|
||||
.title("AppImage type")
|
||||
.title("AppImage format")
|
||||
.subtitle(type_str)
|
||||
.tooltip_text("Type 1 uses ISO9660, Type 2 uses SquashFS")
|
||||
.tooltip_text(
|
||||
"AppImages come in two formats. Type 1 uses an ISO 9660 filesystem \
|
||||
(older, simpler). Type 2 uses SquashFS (modern, compressed, smaller \
|
||||
files). Type 2 is the standard today and is what most AppImage tools \
|
||||
produce."
|
||||
)
|
||||
.build();
|
||||
info_group.add(&type_row);
|
||||
|
||||
let exec_row = adw::ActionRow::builder()
|
||||
.title("Executable")
|
||||
.subtitle(if record.is_executable { "Yes" } else { "No" })
|
||||
.subtitle(if record.is_executable {
|
||||
"Yes - this file has execute permission"
|
||||
} else {
|
||||
"No - execute permission is missing. It will be set automatically when launched."
|
||||
})
|
||||
.build();
|
||||
info_group.add(&exec_row);
|
||||
|
||||
@@ -420,8 +446,11 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
tab
|
||||
}
|
||||
|
||||
/// Tab 2: System - integration, compatibility, sandboxing
|
||||
fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 2: System - integration, compatibility, sandboxing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>, toast_overlay: &adw::ToastOverlay) -> gtk::Box {
|
||||
let tab = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(24)
|
||||
@@ -444,13 +473,21 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
// Desktop Integration group
|
||||
let integration_group = adw::PreferencesGroup::builder()
|
||||
.title("Desktop Integration")
|
||||
.description("Add this app to your application menu")
|
||||
.description(
|
||||
"Show this app in your Activities menu and app launcher, \
|
||||
just like a regular installed application."
|
||||
)
|
||||
.build();
|
||||
|
||||
let switch_row = adw::SwitchRow::builder()
|
||||
.title("Add to application menu")
|
||||
.subtitle("Creates a .desktop file and installs the icon")
|
||||
.subtitle("Creates a .desktop entry and installs the app icon")
|
||||
.active(record.integrated)
|
||||
.tooltip_text(
|
||||
"Desktop integration makes this AppImage appear in your Activities menu \
|
||||
and app launcher, just like a regular installed app. It creates a .desktop \
|
||||
file (a shortcut) and copies the app's icon to your system icon folder."
|
||||
)
|
||||
.build();
|
||||
|
||||
let record_id = record.id;
|
||||
@@ -501,8 +538,11 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
|
||||
// Runtime Compatibility group
|
||||
let compat_group = adw::PreferencesGroup::builder()
|
||||
.title("Runtime Compatibility")
|
||||
.description("Wayland support and FUSE status")
|
||||
.title("Compatibility")
|
||||
.description(
|
||||
"How well this app works with your display server and filesystem. \
|
||||
Most issues here can be resolved with a small package install."
|
||||
)
|
||||
.build();
|
||||
|
||||
let wayland_status = record
|
||||
@@ -512,20 +552,31 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.unwrap_or(WaylandStatus::Unknown);
|
||||
|
||||
let wayland_row = adw::ActionRow::builder()
|
||||
.title("Wayland")
|
||||
.subtitle(wayland_description(&wayland_status))
|
||||
.tooltip_text("Display protocol for Linux desktops")
|
||||
.title("Wayland display")
|
||||
.subtitle(wayland_user_explanation(&wayland_status))
|
||||
.tooltip_text(
|
||||
"Wayland is the modern display system used by GNOME and most Linux desktops. \
|
||||
It replaced the older X11 system. Apps built for X11 still work through \
|
||||
a compatibility layer called XWayland, but native Wayland apps look \
|
||||
sharper and perform better, especially on high-resolution screens."
|
||||
)
|
||||
.build();
|
||||
let wayland_badge = widgets::status_badge(wayland_status.label(), wayland_status.badge_class());
|
||||
wayland_badge.set_valign(gtk::Align::Center);
|
||||
wayland_row.add_suffix(&wayland_badge);
|
||||
compat_group.add(&wayland_row);
|
||||
|
||||
// Wayland analyze button
|
||||
// Analyze toolkit button
|
||||
let analyze_row = adw::ActionRow::builder()
|
||||
.title("Analyze toolkit")
|
||||
.subtitle("Inspect bundled libraries to detect UI toolkit")
|
||||
.subtitle("Inspect bundled libraries to detect which UI toolkit this app uses")
|
||||
.activatable(true)
|
||||
.tooltip_text(
|
||||
"UI toolkits (like GTK, Qt, Electron) are the frameworks apps use to \
|
||||
draw their windows and buttons. Knowing the toolkit helps predict Wayland \
|
||||
compatibility - GTK4 apps are natively Wayland, while older Qt or Electron \
|
||||
apps may need XWayland."
|
||||
)
|
||||
.build();
|
||||
let analyze_icon = gtk::Image::from_icon_name("system-search-symbolic");
|
||||
analyze_icon.set_valign(gtk::Align::Center);
|
||||
@@ -552,12 +603,12 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
let toolkit_label = analysis.toolkit.label();
|
||||
let lib_count = analysis.libraries_found.len();
|
||||
row_clone.set_subtitle(&format!(
|
||||
"Toolkit: {} ({} libraries scanned)",
|
||||
"Detected: {} ({} libraries scanned)",
|
||||
toolkit_label, lib_count,
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
row_clone.set_subtitle("Analysis failed");
|
||||
row_clone.set_subtitle("Analysis failed - the AppImage may not be mountable");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -567,8 +618,11 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
// Runtime Wayland status (from post-launch analysis)
|
||||
if let Some(ref runtime_status) = record.runtime_wayland_status {
|
||||
let runtime_row = adw::ActionRow::builder()
|
||||
.title("Runtime display protocol")
|
||||
.subtitle(runtime_status)
|
||||
.title("Last observed protocol")
|
||||
.subtitle(&format!(
|
||||
"When this app was last launched, it used: {}",
|
||||
runtime_status
|
||||
))
|
||||
.build();
|
||||
if let Some(ref checked) = record.runtime_wayland_checked {
|
||||
let info = gtk::Label::builder()
|
||||
@@ -581,6 +635,7 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
compat_group.add(&runtime_row);
|
||||
}
|
||||
|
||||
// FUSE status
|
||||
let fuse_system = fuse::detect_system_fuse();
|
||||
let fuse_status = record
|
||||
.fuse_status
|
||||
@@ -589,9 +644,14 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.unwrap_or(fuse_system.status.clone());
|
||||
|
||||
let fuse_row = adw::ActionRow::builder()
|
||||
.title("FUSE")
|
||||
.subtitle(fuse_description(&fuse_status))
|
||||
.tooltip_text("Filesystem in Userspace - required for mounting AppImages")
|
||||
.title("FUSE (filesystem)")
|
||||
.subtitle(fuse_user_explanation(&fuse_status))
|
||||
.tooltip_text(
|
||||
"FUSE (Filesystem in Userspace) lets AppImages mount themselves as \
|
||||
virtual drives so they can run directly without extracting. Without it, \
|
||||
AppImages still work but need to extract to a temp folder first, which \
|
||||
is slower. Most systems have FUSE already, but some need libfuse2 installed."
|
||||
)
|
||||
.build();
|
||||
let fuse_badge = widgets::status_badge_with_icon(
|
||||
if fuse_status.is_functional() { "emblem-ok-symbolic" } else { "dialog-warning-symbolic" },
|
||||
@@ -600,14 +660,29 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
);
|
||||
fuse_badge.set_valign(gtk::Align::Center);
|
||||
fuse_row.add_suffix(&fuse_badge);
|
||||
if let Some(cmd) = fuse_install_command(&fuse_status) {
|
||||
let copy_btn = widgets::copy_button(cmd, Some(toast_overlay));
|
||||
copy_btn.set_valign(gtk::Align::Center);
|
||||
copy_btn.set_tooltip_text(Some(&format!("Copy '{}' to clipboard", cmd)));
|
||||
fuse_row.add_suffix(©_btn);
|
||||
}
|
||||
compat_group.add(&fuse_row);
|
||||
|
||||
// Per-app FUSE launch method
|
||||
// Per-app launch method
|
||||
let appimage_path = std::path::Path::new(&record.path);
|
||||
let app_fuse_status = fuse::determine_app_fuse_status(&fuse_system, appimage_path);
|
||||
let launch_method_row = adw::ActionRow::builder()
|
||||
.title("Launch method")
|
||||
.subtitle(app_fuse_status.label())
|
||||
.subtitle(&format!(
|
||||
"This app will launch using: {}",
|
||||
app_fuse_status.label()
|
||||
))
|
||||
.tooltip_text(
|
||||
"AppImages can launch two ways: 'FUSE mount' mounts the image as a \
|
||||
virtual drive (fast, instant startup), or 'extract' unpacks to a temp \
|
||||
folder first (slower, but works everywhere). The method is chosen \
|
||||
automatically based on your system's FUSE support."
|
||||
)
|
||||
.build();
|
||||
let launch_badge = widgets::status_badge(
|
||||
fuse_system.status.as_str(),
|
||||
@@ -621,7 +696,10 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
// Sandboxing group
|
||||
let sandbox_group = adw::PreferencesGroup::builder()
|
||||
.title("Sandboxing")
|
||||
.description("Isolate this app with Firejail")
|
||||
.description(
|
||||
"Isolate this app for extra security. Sandboxing limits what \
|
||||
the app can access on your system."
|
||||
)
|
||||
.build();
|
||||
|
||||
let current_mode = record
|
||||
@@ -633,17 +711,25 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
let firejail_available = launcher::has_firejail();
|
||||
|
||||
let sandbox_subtitle = if firejail_available {
|
||||
format!("Current mode: {}", current_mode.label())
|
||||
format!(
|
||||
"Isolate this app using Firejail. Current mode: {}",
|
||||
current_mode.display_label()
|
||||
)
|
||||
} else {
|
||||
"Firejail is not installed".to_string()
|
||||
"Firejail is not installed. Use the row below to copy the install command.".to_string()
|
||||
};
|
||||
|
||||
let firejail_row = adw::SwitchRow::builder()
|
||||
.title("Firejail sandbox")
|
||||
.subtitle(&sandbox_subtitle)
|
||||
.tooltip_text("Linux application sandboxing tool")
|
||||
.active(current_mode == SandboxMode::Firejail)
|
||||
.sensitive(firejail_available)
|
||||
.tooltip_text(
|
||||
"Sandboxing restricts what an app can access on your system - files, \
|
||||
network, devices, etc. This adds a security layer so that even if an \
|
||||
app is compromised, it cannot freely access your personal data. Firejail \
|
||||
is a lightweight Linux sandboxing tool."
|
||||
)
|
||||
.build();
|
||||
|
||||
let record_id = record.id;
|
||||
@@ -661,13 +747,18 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
sandbox_group.add(&firejail_row);
|
||||
|
||||
if !firejail_available {
|
||||
let firejail_cmd = "sudo apt install firejail";
|
||||
let info_row = adw::ActionRow::builder()
|
||||
.title("Install Firejail")
|
||||
.subtitle("sudo apt install firejail")
|
||||
.subtitle(firejail_cmd)
|
||||
.build();
|
||||
let badge = widgets::status_badge("Missing", "warning");
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
info_row.add_suffix(&badge);
|
||||
let copy_btn = widgets::copy_button(firejail_cmd, Some(toast_overlay));
|
||||
copy_btn.set_valign(gtk::Align::Center);
|
||||
copy_btn.set_tooltip_text(Some(&format!("Copy '{}' to clipboard", firejail_cmd)));
|
||||
info_row.add_suffix(©_btn);
|
||||
sandbox_group.add(&info_row);
|
||||
}
|
||||
inner.append(&sandbox_group);
|
||||
@@ -677,7 +768,10 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
tab
|
||||
}
|
||||
|
||||
/// Tab 3: Security - vulnerability scanning and integrity
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 3: Security - vulnerability scanning and integrity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
let tab = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
@@ -700,7 +794,10 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
|
||||
let group = adw::PreferencesGroup::builder()
|
||||
.title("Vulnerability Scanning")
|
||||
.description("Check bundled libraries for known CVEs")
|
||||
.description(
|
||||
"Scan the libraries bundled inside this AppImage for known \
|
||||
security vulnerabilities (CVEs)."
|
||||
)
|
||||
.build();
|
||||
|
||||
let libs = db.get_bundled_libraries(record.id).unwrap_or_default();
|
||||
@@ -709,7 +806,10 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
if libs.is_empty() {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Security scan")
|
||||
.subtitle("Not yet scanned for vulnerabilities")
|
||||
.subtitle(
|
||||
"This app has not been scanned yet. Use the button below \
|
||||
to check for known vulnerabilities."
|
||||
)
|
||||
.build();
|
||||
let badge = widgets::status_badge("Not scanned", "neutral");
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
@@ -718,14 +818,17 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
} else {
|
||||
let lib_row = adw::ActionRow::builder()
|
||||
.title("Bundled libraries")
|
||||
.subtitle(&libs.len().to_string())
|
||||
.subtitle(&format!(
|
||||
"{} libraries detected inside this AppImage",
|
||||
libs.len()
|
||||
))
|
||||
.build();
|
||||
group.add(&lib_row);
|
||||
|
||||
if summary.total() == 0 {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Vulnerabilities")
|
||||
.subtitle("No known vulnerabilities")
|
||||
.subtitle("No known security issues found in the bundled libraries.")
|
||||
.build();
|
||||
let badge = widgets::status_badge("Clean", "success");
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
@@ -734,7 +837,11 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
} else {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("Vulnerabilities")
|
||||
.subtitle(&format!("{} found", summary.total()))
|
||||
.subtitle(&format!(
|
||||
"{} known issue{} found. Consider updating this app if a newer version is available.",
|
||||
summary.total(),
|
||||
if summary.total() == 1 { "" } else { "s" },
|
||||
))
|
||||
.build();
|
||||
let badge = widgets::status_badge(summary.max_severity(), summary.badge_class());
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
@@ -745,9 +852,16 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
|
||||
// Scan button
|
||||
let scan_row = adw::ActionRow::builder()
|
||||
.title("Scan this AppImage")
|
||||
.subtitle("Check bundled libraries for known CVEs")
|
||||
.title("Run security scan")
|
||||
.subtitle("Check bundled libraries against known CVE databases")
|
||||
.activatable(true)
|
||||
.tooltip_text(
|
||||
"CVE stands for Common Vulnerabilities and Exposures - a public list \
|
||||
of known security bugs in software. AppImages bundle their own copies \
|
||||
of system libraries, which may contain outdated versions with known \
|
||||
vulnerabilities. This scan checks those bundled libraries against the \
|
||||
OSV.dev database to find any known issues."
|
||||
)
|
||||
.build();
|
||||
let scan_icon = gtk::Image::from_icon_name("security-medium-symbolic");
|
||||
scan_icon.set_valign(gtk::Align::Center);
|
||||
@@ -758,7 +872,7 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
scan_row.connect_activated(move |row| {
|
||||
row.set_sensitive(false);
|
||||
row.update_state(&[gtk::accessible::State::Busy(true)]);
|
||||
row.set_subtitle("Scanning...");
|
||||
row.set_subtitle("Scanning - this may take a moment...");
|
||||
let row_clone = row.clone();
|
||||
let path = record_path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
@@ -775,15 +889,17 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
Ok(scan_result) => {
|
||||
let total = scan_result.total_cves();
|
||||
if total == 0 {
|
||||
row_clone.set_subtitle("No vulnerabilities found");
|
||||
row_clone.set_subtitle("No vulnerabilities found - looking good!");
|
||||
} else {
|
||||
row_clone.set_subtitle(&format!(
|
||||
"Found {} CVE{}", total, if total == 1 { "" } else { "s" }
|
||||
"Found {} known issue{}. Check for app updates.",
|
||||
total,
|
||||
if total == 1 { "" } else { "s" },
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
row_clone.set_subtitle("Scan failed");
|
||||
row_clone.set_subtitle("Scan failed - the AppImage may not be mountable");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -795,6 +911,7 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
if record.sha256.is_some() {
|
||||
let integrity_group = adw::PreferencesGroup::builder()
|
||||
.title("Integrity")
|
||||
.description("Verify that the file has not been modified or corrupted.")
|
||||
.build();
|
||||
|
||||
if let Some(ref hash) = record.sha256 {
|
||||
@@ -802,7 +919,12 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.title("SHA256 checksum")
|
||||
.subtitle(hash)
|
||||
.subtitle_selectable(true)
|
||||
.tooltip_text("Cryptographic hash for verifying file integrity")
|
||||
.tooltip_text(
|
||||
"A SHA256 checksum is a unique fingerprint of the file. If even one \
|
||||
byte changes, the checksum changes completely. You can compare this \
|
||||
against the developer's published checksum to verify the file hasn't \
|
||||
been tampered with or corrupted during download."
|
||||
)
|
||||
.build();
|
||||
hash_row.add_css_class("property");
|
||||
integrity_group.add(&hash_row);
|
||||
@@ -815,7 +937,10 @@ fn build_security_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
tab
|
||||
}
|
||||
|
||||
/// Tab 4: Storage - disk usage and data discovery
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab 4: Storage - disk usage, data paths, file location
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_storage_tab(
|
||||
record: &AppImageRecord,
|
||||
db: &Rc<Database>,
|
||||
@@ -843,12 +968,16 @@ fn build_storage_tab(
|
||||
// Disk usage group
|
||||
let size_group = adw::PreferencesGroup::builder()
|
||||
.title("Disk Usage")
|
||||
.description(
|
||||
"Disk space used by this app, including any configuration, \
|
||||
cache, or data files it may have created."
|
||||
)
|
||||
.build();
|
||||
|
||||
let fp = footprint::get_footprint(db, record.id, record.size_bytes as u64);
|
||||
|
||||
let appimage_row = adw::ActionRow::builder()
|
||||
.title("AppImage file size")
|
||||
.title("AppImage file")
|
||||
.subtitle(&widgets::format_size(record.size_bytes))
|
||||
.build();
|
||||
size_group.add(&appimage_row);
|
||||
@@ -857,9 +986,9 @@ fn build_storage_tab(
|
||||
let data_total = fp.data_total();
|
||||
if data_total > 0 {
|
||||
let total_row = adw::ActionRow::builder()
|
||||
.title("Total disk footprint")
|
||||
.title("Total disk usage")
|
||||
.subtitle(&format!(
|
||||
"{} (AppImage) + {} (data) = {}",
|
||||
"{} (AppImage) + {} (app data) = {}",
|
||||
widgets::format_size(record.size_bytes),
|
||||
widgets::format_size(data_total as i64),
|
||||
widgets::format_size(fp.total_size() as i64),
|
||||
@@ -872,14 +1001,14 @@ fn build_storage_tab(
|
||||
|
||||
// Data paths group
|
||||
let paths_group = adw::PreferencesGroup::builder()
|
||||
.title("Data Paths")
|
||||
.description("Config, data, and cache directories for this app")
|
||||
.title("App Data")
|
||||
.description("Config, cache, and data directories this app may have created.")
|
||||
.build();
|
||||
|
||||
// Discover button
|
||||
let discover_row = adw::ActionRow::builder()
|
||||
.title("Discover data paths")
|
||||
.subtitle("Search for config, data, and cache directories")
|
||||
.title("Find app data")
|
||||
.subtitle("Search for config, cache, and data directories")
|
||||
.activatable(true)
|
||||
.build();
|
||||
let discover_icon = gtk::Image::from_icon_name("folder-saved-search-symbolic");
|
||||
@@ -890,7 +1019,7 @@ fn build_storage_tab(
|
||||
let record_id = record.id;
|
||||
discover_row.connect_activated(move |row| {
|
||||
row.set_sensitive(false);
|
||||
row.set_subtitle("Discovering...");
|
||||
row.set_subtitle("Searching...");
|
||||
let row_clone = row.clone();
|
||||
let rec = record_clone.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
@@ -906,10 +1035,10 @@ fn build_storage_tab(
|
||||
Ok(fp) => {
|
||||
let count = fp.paths.len();
|
||||
if count == 0 {
|
||||
row_clone.set_subtitle("No associated paths found");
|
||||
row_clone.set_subtitle("No associated data directories found");
|
||||
} else {
|
||||
row_clone.set_subtitle(&format!(
|
||||
"Found {} path{} ({})",
|
||||
"Found {} path{} using {}",
|
||||
count,
|
||||
if count == 1 { "" } else { "s" },
|
||||
widgets::format_size(fp.data_total() as i64),
|
||||
@@ -917,14 +1046,14 @@ fn build_storage_tab(
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
row_clone.set_subtitle("Discovery failed");
|
||||
row_clone.set_subtitle("Search failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
paths_group.add(&discover_row);
|
||||
|
||||
// Individual discovered paths with type icons and confidence badges
|
||||
// Individual discovered paths
|
||||
for dp in &fp.paths {
|
||||
if dp.exists {
|
||||
let row = adw::ActionRow::builder()
|
||||
@@ -998,22 +1127,51 @@ fn build_storage_tab(
|
||||
tab
|
||||
}
|
||||
|
||||
fn wayland_description(status: &WaylandStatus) -> &'static str {
|
||||
// ---------------------------------------------------------------------------
|
||||
// User-friendly explanations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn wayland_user_explanation(status: &WaylandStatus) -> &'static str {
|
||||
match status {
|
||||
WaylandStatus::Native => "Runs natively on Wayland",
|
||||
WaylandStatus::XWayland => "Runs via XWayland compatibility layer",
|
||||
WaylandStatus::Possible => "May run on Wayland with additional flags",
|
||||
WaylandStatus::X11Only => "X11 only - no Wayland support",
|
||||
WaylandStatus::Unknown => "Could not determine Wayland compatibility",
|
||||
WaylandStatus::Native =>
|
||||
"Runs natively on Wayland - the best experience on modern Linux desktops.",
|
||||
WaylandStatus::XWayland =>
|
||||
"Uses XWayland for display. Works fine, but may appear slightly \
|
||||
blurry on high-resolution screens.",
|
||||
WaylandStatus::Possible =>
|
||||
"Might work on Wayland with the right settings. Try launching it to find out.",
|
||||
WaylandStatus::X11Only =>
|
||||
"Designed for X11 only. It will run through XWayland automatically, \
|
||||
but you may notice minor display quirks.",
|
||||
WaylandStatus::Unknown =>
|
||||
"Not yet determined. Launch the app or use 'Analyze toolkit' below to check.",
|
||||
}
|
||||
}
|
||||
|
||||
fn fuse_description(status: &FuseStatus) -> &'static str {
|
||||
fn fuse_user_explanation(status: &FuseStatus) -> &'static str {
|
||||
match status {
|
||||
FuseStatus::FullyFunctional => "FUSE mount available - native AppImage launch",
|
||||
FuseStatus::Fuse3Only => "Only FUSE3 installed - may need libfuse2",
|
||||
FuseStatus::NoFusermount => "fusermount binary not found",
|
||||
FuseStatus::NoDevFuse => "/dev/fuse device not available",
|
||||
FuseStatus::MissingLibfuse2 => "libfuse2 not installed",
|
||||
FuseStatus::FullyFunctional =>
|
||||
"FUSE is working - AppImages mount directly for fast startup.",
|
||||
FuseStatus::Fuse3Only =>
|
||||
"Only FUSE 3 found. Some AppImages need FUSE 2. \
|
||||
Click the copy button to get the install command.",
|
||||
FuseStatus::NoFusermount =>
|
||||
"FUSE tools not found. The app will still work by extracting to a \
|
||||
temporary folder, but startup will be slower.",
|
||||
FuseStatus::NoDevFuse =>
|
||||
"/dev/fuse not available. FUSE may not be configured on your system. \
|
||||
Apps will extract to a temp folder instead.",
|
||||
FuseStatus::MissingLibfuse2 =>
|
||||
"libfuse2 is missing. Click the copy button to get the install command.",
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an install command for a FUSE status that needs one, or None.
|
||||
fn fuse_install_command(status: &FuseStatus) -> Option<&'static str> {
|
||||
match status {
|
||||
FuseStatus::Fuse3Only | FuseStatus::MissingLibfuse2 => {
|
||||
Some("sudo apt install libfuse2")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user