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

406
src/core/wayland.rs Normal file
View File

@@ -0,0 +1,406 @@
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq)]
pub enum WaylandStatus {
/// Native Wayland support detected (GTK4, Qt6+Wayland, Electron 38+)
Native,
/// Will run under XWayland compatibility layer
XWayland,
/// Toolkit supports Wayland but plugins may be missing or env vars needed
Possible,
/// X11-only toolkit with no Wayland path (GTK2, old Electron, Java)
X11Only,
/// Could not determine (uncommon toolkit, static binary, etc.)
Unknown,
}
impl WaylandStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Native => "native",
Self::XWayland => "xwayland",
Self::Possible => "possible",
Self::X11Only => "x11_only",
Self::Unknown => "unknown",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"native" => Self::Native,
"xwayland" => Self::XWayland,
"possible" => Self::Possible,
"x11_only" => Self::X11Only,
_ => Self::Unknown,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Native => "Native Wayland",
Self::XWayland => "XWayland",
Self::Possible => "Wayland possible",
Self::X11Only => "X11 only",
Self::Unknown => "Unknown",
}
}
pub fn badge_class(&self) -> &'static str {
match self {
Self::Native => "success",
Self::XWayland | Self::Possible => "warning",
Self::X11Only => "error",
Self::Unknown => "neutral",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DetectedToolkit {
Gtk4,
Gtk3Wayland,
Gtk3X11Only,
Gtk2,
Qt6Wayland,
Qt6X11Only,
Qt5Wayland,
Qt5X11Only,
ElectronNative(u32), // version >= 38
ElectronFlagged(u32), // version 28-37 with ozone flag
ElectronLegacy(u32), // version < 28
JavaSwing,
Flutter,
Unknown,
}
impl DetectedToolkit {
pub fn label(&self) -> String {
match self {
Self::Gtk4 => "GTK4".to_string(),
Self::Gtk3Wayland => "GTK3 (Wayland)".to_string(),
Self::Gtk3X11Only => "GTK3 (X11)".to_string(),
Self::Gtk2 => "GTK2".to_string(),
Self::Qt6Wayland => "Qt6 (Wayland)".to_string(),
Self::Qt6X11Only => "Qt6 (X11)".to_string(),
Self::Qt5Wayland => "Qt5 (Wayland)".to_string(),
Self::Qt5X11Only => "Qt5 (X11)".to_string(),
Self::ElectronNative(v) => format!("Electron {} (native Wayland)", v),
Self::ElectronFlagged(v) => format!("Electron {} (Wayland with flags)", v),
Self::ElectronLegacy(v) => format!("Electron {} (X11)", v),
Self::JavaSwing => "Java/Swing".to_string(),
Self::Flutter => "Flutter".to_string(),
Self::Unknown => "Unknown".to_string(),
}
}
pub fn wayland_status(&self) -> WaylandStatus {
match self {
Self::Gtk4 | Self::Gtk3Wayland | Self::Qt6Wayland | Self::Qt5Wayland
| Self::ElectronNative(_) | Self::Flutter => WaylandStatus::Native,
Self::ElectronFlagged(_) => WaylandStatus::Possible,
Self::Gtk3X11Only | Self::Qt6X11Only | Self::Qt5X11Only => WaylandStatus::XWayland,
Self::Gtk2 | Self::ElectronLegacy(_) | Self::JavaSwing => WaylandStatus::X11Only,
Self::Unknown => WaylandStatus::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub struct WaylandAnalysis {
pub status: WaylandStatus,
pub toolkit: DetectedToolkit,
pub libraries_found: Vec<String>,
}
/// Analyze an AppImage's Wayland compatibility by inspecting its bundled libraries.
/// Uses unsquashfs to list files inside the squashfs.
pub fn analyze_appimage(appimage_path: &Path) -> WaylandAnalysis {
let libs = list_bundled_libraries(appimage_path);
let toolkit = detect_toolkit(&libs);
let status = toolkit.wayland_status();
WaylandAnalysis {
status,
toolkit,
libraries_found: libs,
}
}
/// List shared libraries bundled inside the AppImage squashfs.
fn list_bundled_libraries(appimage_path: &Path) -> Vec<String> {
// First get the squashfs offset
let offset_output = Command::new(appimage_path)
.arg("--appimage-offset")
.env("APPIMAGE_EXTRACT_AND_RUN", "1")
.output();
let offset = match offset_output {
Ok(out) if out.status.success() => {
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
_ => return Vec::new(),
};
// Use unsquashfs to list files (just filenames, no extraction)
let output = Command::new("unsquashfs")
.args(["-o", &offset, "-l", "-no-progress"])
.arg(appimage_path)
.output();
match output {
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
stdout
.lines()
.filter(|line| line.contains(".so"))
.map(|line| {
// unsquashfs -l output format: "squashfs-root/usr/lib/libfoo.so.1"
// Extract just the filename
line.rsplit('/').next().unwrap_or(line).to_string()
})
.collect()
}
_ => Vec::new(),
}
}
/// Detect the UI toolkit from the bundled library list.
fn detect_toolkit(libs: &[String]) -> DetectedToolkit {
let has = |pattern: &str| -> bool {
libs.iter().any(|l| l.contains(pattern))
};
// Check for GTK4 (always Wayland-native)
if has("libgtk-4") || has("libGdk-4") {
return DetectedToolkit::Gtk4;
}
// Check for Flutter (GTK backend, Wayland-native)
if has("libflutter_linux_gtk") {
return DetectedToolkit::Flutter;
}
// Check for Java/Swing (X11 only)
if has("libjvm.so") || has("libjava.so") || has("libawt.so") {
return DetectedToolkit::JavaSwing;
}
// Check for Electron (version-dependent)
if has("libElectron") || has("electron") || has("libnode.so") || has("libchromium") {
let version = detect_electron_version(libs);
if let Some(v) = version {
if v >= 38 {
return DetectedToolkit::ElectronNative(v);
} else if v >= 28 {
return DetectedToolkit::ElectronFlagged(v);
} else {
return DetectedToolkit::ElectronLegacy(v);
}
}
// Can't determine version - assume modern enough for XWayland at minimum
return DetectedToolkit::ElectronFlagged(0);
}
// Check for Qt6
if has("libQt6Core") || has("libQt6Gui") {
if has("libQt6WaylandClient") || has("libqwayland") {
return DetectedToolkit::Qt6Wayland;
}
return DetectedToolkit::Qt6X11Only;
}
// Check for Qt5
if has("libQt5Core") || has("libQt5Gui") {
if has("libQt5WaylandClient") || has("libqwayland") {
return DetectedToolkit::Qt5Wayland;
}
return DetectedToolkit::Qt5X11Only;
}
// Check for GTK3
if has("libgtk-3") || has("libGdk-3") {
if has("libwayland-client") {
return DetectedToolkit::Gtk3Wayland;
}
return DetectedToolkit::Gtk3X11Only;
}
// Check for GTK2 (X11 only, forever)
if has("libgtk-x11-2") || has("libgdk-x11-2") {
return DetectedToolkit::Gtk2;
}
DetectedToolkit::Unknown
}
/// Try to detect Electron version from bundled files.
fn detect_electron_version(libs: &[String]) -> Option<u32> {
for lib in libs {
// Look for version patterns in Electron-related files
if lib.contains("electron") {
// Try to extract version number from filenames like "electron-v28.0.0"
for part in lib.split(&['-', '_', 'v'][..]) {
if let Some(major) = part.split('.').next() {
if let Ok(v) = major.parse::<u32>() {
if v > 0 && v < 200 {
return Some(v);
}
}
}
}
}
}
None
}
/// Detect the current desktop session type.
#[derive(Debug, Clone, PartialEq)]
pub enum SessionType {
Wayland,
X11,
Unknown,
}
impl SessionType {
pub fn label(&self) -> &'static str {
match self {
Self::Wayland => "Wayland",
Self::X11 => "X11",
Self::Unknown => "Unknown",
}
}
}
pub fn detect_session_type() -> SessionType {
// Check XDG_SESSION_TYPE first (most reliable)
if let Ok(session) = std::env::var("XDG_SESSION_TYPE") {
return match session.as_str() {
"wayland" => SessionType::Wayland,
"x11" => SessionType::X11,
_ => SessionType::Unknown,
};
}
// Check WAYLAND_DISPLAY
if std::env::var("WAYLAND_DISPLAY").is_ok() {
return SessionType::Wayland;
}
// Check DISPLAY (X11 fallback)
if std::env::var("DISPLAY").is_ok() {
return SessionType::X11;
}
SessionType::Unknown
}
/// Get desktop environment info string.
pub fn detect_desktop_environment() -> String {
let de = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let session = std::env::var("DESKTOP_SESSION").unwrap_or_default();
if !de.is_empty() {
de
} else if !session.is_empty() {
session
} else {
"Unknown".to_string()
}
}
/// Check if XWayland is available on the system.
pub fn has_xwayland() -> bool {
// Check if Xwayland process is running
Command::new("pgrep")
.arg("Xwayland")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wayland_status_roundtrip() {
let statuses = [
WaylandStatus::Native,
WaylandStatus::XWayland,
WaylandStatus::Possible,
WaylandStatus::X11Only,
WaylandStatus::Unknown,
];
for status in &statuses {
assert_eq!(&WaylandStatus::from_str(status.as_str()), status);
}
}
#[test]
fn test_detect_toolkit_gtk4() {
let libs = vec!["libgtk-4.so.1".to_string(), "libglib-2.0.so.0".to_string()];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Gtk4));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
}
#[test]
fn test_detect_toolkit_qt5_wayland() {
let libs = vec![
"libQt5Core.so.5".to_string(),
"libQt5Gui.so.5".to_string(),
"libQt5WaylandClient.so.5".to_string(),
];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Qt5Wayland));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
}
#[test]
fn test_detect_toolkit_qt5_x11() {
let libs = vec![
"libQt5Core.so.5".to_string(),
"libQt5Gui.so.5".to_string(),
];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Qt5X11Only));
assert_eq!(toolkit.wayland_status(), WaylandStatus::XWayland);
}
#[test]
fn test_detect_toolkit_gtk2() {
let libs = vec!["libgtk-x11-2.0.so.0".to_string()];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Gtk2));
assert_eq!(toolkit.wayland_status(), WaylandStatus::X11Only);
}
#[test]
fn test_detect_toolkit_gtk3_with_wayland() {
let libs = vec![
"libgtk-3.so.0".to_string(),
"libwayland-client.so.0".to_string(),
];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Gtk3Wayland));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
}
#[test]
fn test_detect_toolkit_unknown() {
let libs = vec!["libfoo.so.1".to_string()];
let toolkit = detect_toolkit(&libs);
assert!(matches!(toolkit, DetectedToolkit::Unknown));
assert_eq!(toolkit.wayland_status(), WaylandStatus::Unknown);
}
#[test]
fn test_badge_classes() {
assert_eq!(WaylandStatus::Native.badge_class(), "success");
assert_eq!(WaylandStatus::XWayland.badge_class(), "warning");
assert_eq!(WaylandStatus::X11Only.badge_class(), "error");
assert_eq!(WaylandStatus::Unknown.badge_class(), "neutral");
}
}