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, } /// 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 { // Get the squashfs offset using binary scan (never execute the AppImage - // some apps like Affinity have custom AppRun scripts that ignore flags) let offset = match crate::core::inspector::find_squashfs_offset_for(appimage_path) { Some(o) => o.to_string(), None => 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 { 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::() { 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() } } /// Result of analyzing a running process for Wayland usage. #[derive(Debug, Clone)] pub struct RuntimeAnalysis { pub pid: u32, pub has_wayland_socket: bool, pub has_x11_connection: bool, pub env_vars: Vec<(String, String)>, } impl RuntimeAnalysis { /// Human-readable status label. pub fn status_label(&self) -> &'static str { match (self.has_wayland_socket, self.has_x11_connection) { (true, false) => "Native Wayland", (true, true) => "Wayland + X11 fallback", (false, true) => "X11 / XWayland", (false, false) => "Unknown", } } /// Machine-readable status string for database storage. pub fn as_status_str(&self) -> &'static str { match (self.has_wayland_socket, self.has_x11_connection) { (true, false) => "native", (true, true) => "native", (false, true) => "xwayland", (false, false) => "unknown", } } } /// Analyze a running process to determine its actual Wayland/X11 usage. /// Inspects /proc//fd for Wayland and X11 sockets, and reads /// relevant environment variables from /proc//environ. pub fn analyze_running_process(pid: u32) -> Result { let proc_path = format!("/proc/{}", pid); if !std::path::Path::new(&proc_path).exists() { return Err(format!("Process {} not found", pid)); } // Check file descriptors for Wayland and X11 sockets let fd_dir = format!("{}/fd", proc_path); let mut has_wayland_socket = false; let mut has_x11_connection = false; if let Ok(entries) = std::fs::read_dir(&fd_dir) { for entry in entries.flatten() { if let Ok(target) = std::fs::read_link(entry.path()) { let target_str = target.to_string_lossy(); if target_str.contains("wayland") { has_wayland_socket = true; } if target_str.contains("/tmp/.X11-unix/") || target_str.contains("@/tmp/.X11") { has_x11_connection = true; } } } } // Read relevant environment variables let environ_path = format!("{}/environ", proc_path); let mut env_vars = Vec::new(); let relevant_vars = [ "WAYLAND_DISPLAY", "DISPLAY", "GDK_BACKEND", "QT_QPA_PLATFORM", "XDG_SESSION_TYPE", "SDL_VIDEODRIVER", "CLUTTER_BACKEND", ]; if let Ok(data) = std::fs::read(&environ_path) { for entry in data.split(|&b| b == 0) { if let Ok(s) = std::str::from_utf8(entry) { if let Some((key, value)) = s.split_once('=') { if relevant_vars.contains(&key) { env_vars.push((key.to_string(), value.to_string())); } } } } } // Also check env vars for hints if fd inspection was inconclusive if !has_wayland_socket { has_wayland_socket = env_vars.iter().any(|(k, v)| { (k == "GDK_BACKEND" && v.contains("wayland")) || (k == "QT_QPA_PLATFORM" && v.contains("wayland")) }); } Ok(RuntimeAnalysis { pid, has_wayland_socket, has_x11_connection, env_vars, }) } /// 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"); } }