Implement Driftwood AppImage manager - Phases 1 and 2

Phase 1 - Application scaffolding:
- GTK4/libadwaita application window with AdwNavigationView
- GSettings-backed window state persistence
- GResource-compiled CSS and schema
- Library view with grid/list toggle, search, sorting, filtering
- Detail view with file info, desktop integration controls
- Preferences window with scan directories, theme, behavior settings
- CLI with list, scan, integrate, remove, clean, inspect commands
- AppImage discovery, metadata extraction, desktop integration
- Orphaned desktop entry detection and cleanup
- AppImage packaging script

Phase 2 - Intelligence layer:
- Database schema v2 with migration for status tracking columns
- FUSE detection engine (libfuse2/3, fusermount, /dev/fuse, AppImageLauncher)
- Wayland awareness engine (session type, toolkit detection, XWayland)
- Update info parsing from AppImage ELF sections (.upd_info)
- GitHub/GitLab Releases API integration for update checking
- Update download with progress tracking and atomic apply
- Launch wrapper with FUSE auto-detection and usage tracking
- Duplicate and multi-version detection with recommendations
- Dashboard with system health, library stats, disk usage
- Update check dialog (single and batch)
- Duplicate resolution dialog
- Status badges on library cards and detail view
- Extended CLI: status, check-updates, duplicates, launch commands

49 tests passing across all modules.
This commit is contained in:
lashman
2026-02-26 23:04:27 +02:00
parent 588b1b1525
commit fa28955919
33 changed files with 10401 additions and 0 deletions

166
src/core/launcher.rs Normal file
View File

@@ -0,0 +1,166 @@
use std::path::Path;
use std::process::{Child, Command, Stdio};
use super::database::Database;
use super::fuse::{detect_system_fuse, determine_app_fuse_status, AppImageFuseStatus};
/// Launch method used for the AppImage.
#[derive(Debug, Clone, PartialEq)]
pub enum LaunchMethod {
/// Direct execution via FUSE mount
Direct,
/// Extract-and-run fallback (APPIMAGE_EXTRACT_AND_RUN=1)
ExtractAndRun,
/// Via firejail sandbox
Sandboxed,
}
impl LaunchMethod {
pub fn as_str(&self) -> &'static str {
match self {
Self::Direct => "direct",
Self::ExtractAndRun => "extract_and_run",
Self::Sandboxed => "sandboxed",
}
}
}
/// Result of a launch attempt.
#[derive(Debug)]
pub enum LaunchResult {
/// Successfully spawned the process.
Started {
child: Child,
method: LaunchMethod,
},
/// Failed to launch.
Failed(String),
}
/// Launch an AppImage, recording the event in the database.
/// Automatically selects the best launch method based on FUSE status.
pub fn launch_appimage(
db: &Database,
record_id: i64,
appimage_path: &Path,
source: &str,
extra_args: &[String],
extra_env: &[(&str, &str)],
) -> LaunchResult {
// Determine launch method based on FUSE status
let fuse_info = detect_system_fuse();
let fuse_status = determine_app_fuse_status(&fuse_info, appimage_path);
let method = match fuse_status {
AppImageFuseStatus::NativeFuse | AppImageFuseStatus::StaticRuntime => LaunchMethod::Direct,
AppImageFuseStatus::ExtractAndRun => LaunchMethod::ExtractAndRun,
AppImageFuseStatus::CannotLaunch => {
return LaunchResult::Failed(
"Cannot launch: FUSE is not available and extract-and-run is not supported".into(),
);
}
};
let result = execute_appimage(appimage_path, &method, extra_args, extra_env);
// Record the launch event regardless of success
if let Err(e) = db.record_launch(record_id, source) {
log::warn!("Failed to record launch event: {}", e);
}
result
}
/// Launch an AppImage without database tracking (for standalone use).
pub fn launch_appimage_simple(
appimage_path: &Path,
extra_args: &[String],
) -> LaunchResult {
let fuse_info = detect_system_fuse();
let fuse_status = determine_app_fuse_status(&fuse_info, appimage_path);
let method = match fuse_status {
AppImageFuseStatus::NativeFuse | AppImageFuseStatus::StaticRuntime => LaunchMethod::Direct,
AppImageFuseStatus::ExtractAndRun => LaunchMethod::ExtractAndRun,
AppImageFuseStatus::CannotLaunch => {
return LaunchResult::Failed(
"Cannot launch: FUSE is not available and this AppImage doesn't support extract-and-run".into(),
);
}
};
execute_appimage(appimage_path, &method, extra_args, &[])
}
/// Execute the AppImage process with the given method.
fn execute_appimage(
appimage_path: &Path,
method: &LaunchMethod,
args: &[String],
extra_env: &[(&str, &str)],
) -> LaunchResult {
let mut cmd = match method {
LaunchMethod::Direct => {
let mut c = Command::new(appimage_path);
c.args(args);
c
}
LaunchMethod::ExtractAndRun => {
let mut c = Command::new(appimage_path);
c.env("APPIMAGE_EXTRACT_AND_RUN", "1");
c.args(args);
c
}
LaunchMethod::Sandboxed => {
let mut c = Command::new("firejail");
c.arg("--appimage");
c.arg(appimage_path);
c.args(args);
c
}
};
// Apply extra environment variables
for (key, value) in extra_env {
cmd.env(key, value);
}
// Detach from our process group so the app runs independently
cmd.stdin(Stdio::null());
match cmd.spawn() {
Ok(child) => LaunchResult::Started {
child,
method: method.clone(),
},
Err(e) => LaunchResult::Failed(format!("Failed to spawn process: {}", e)),
}
}
/// Check if firejail is available for sandboxed launches.
pub fn has_firejail() -> bool {
Command::new("firejail")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
/// Get launch statistics for an AppImage from the database.
#[derive(Debug, Clone)]
pub struct LaunchStats {
pub total_launches: u64,
pub last_launched: Option<String>,
}
pub fn get_launch_stats(db: &Database, record_id: i64) -> LaunchStats {
let total_launches = db.get_launch_count(record_id).unwrap_or(0) as u64;
let last_launched = db.get_last_launched(record_id).unwrap_or(None);
LaunchStats {
total_launches,
last_launched,
}
}