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:
406
src/core/wayland.rs
Normal file
406
src/core/wayland.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user