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:
lashman
2026-02-27 17:16:41 +02:00
parent a7ed3742fb
commit 423323d5a9
51 changed files with 10583 additions and 481 deletions

View File

@@ -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(&copy_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(&copy_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,
}
}