use std::path::{Path, PathBuf}; use std::process::Command; use crate::{PackageInfo, CartoError}; use super::{PackageManager, PackageSource}; pub struct FlatpakManager; impl PackageManager for FlatpakManager { fn name(&self) -> &'static str { "flatpak" } fn source_type(&self) -> PackageSource { PackageSource::Flatpak } fn is_available(&self) -> bool { Path::new("/usr/bin/flatpak").exists() || Command::new("flatpak").arg("--version").output().is_ok() } fn get_installed_packages(&self) -> Result, CartoError> { let output = Command::new("flatpak") .args(["list", "--columns=application,name,version,branch,installation"]) .output()?; if !output.status.success() { return Err(CartoError::CommandFailed("flatpak list failed".to_string())); } let stdout = String::from_utf8(output.stdout)?; let mut packages = Vec::new(); for line in stdout.lines().skip(1) { // Skip header let parts: Vec<&str> = line.split('\t').collect(); if parts.len() >= 4 { let app_id = parts[0]; let name = if parts[1].is_empty() { app_id } else { parts[1] }; let version = parts[2]; let installation = parts.get(4).unwrap_or(&"system"); // Skip runtimes - we only want applications if !app_id.starts_with("org.freedesktop.Platform") && !app_id.starts_with("org.gnome.Platform") && !app_id.starts_with("org.kde.Platform") { packages.push(PackageInfo { name: format!("{} ({})", name, app_id), version: version.to_string(), source: PackageSource::Flatpak, install_time: self.get_install_time(app_id, installation).unwrap_or(0), size: self.get_package_size(app_id, installation).unwrap_or(0), }); } } } Ok(packages) } fn get_package_files(&self, package: &str) -> Result, CartoError> { // Extract app ID from package name (format: "Name (app.id)") let app_id = if package.contains('(') && package.contains(')') { package.split('(').nth(1) .and_then(|s| s.split(')').next()) .unwrap_or(package) } else { package }; let mut files = Vec::new(); // Get installation location let location = self.get_app_location(app_id)?; if let Some(location_path) = location { files.push(location_path); } // Add common Flatpak locations files.extend(self.get_flatpak_system_files(app_id)); // Add user data directories if they exist if let Some(home) = std::env::var("HOME").ok() { let user_data = PathBuf::from(format!("{}/.var/app/{}", home, app_id)); if user_data.exists() { files.push(user_data); } } // Filter to only existing files Ok(files.into_iter().filter(|p| p.exists()).collect()) } fn get_file_owner(&self, path: &Path) -> Result, CartoError> { let path_str = path.to_string_lossy(); // Check if it's in a Flatpak directory if path_str.contains("/var/lib/flatpak/app/") { // Extract app ID from path like /var/lib/flatpak/app/org.signal.Signal/... if let Some(app_part) = path_str.split("/var/lib/flatpak/app/").nth(1) { if let Some(app_id) = app_part.split('/').next() { return Ok(Some(app_id.to_string())); } } } // Check user Flatpak directories if path_str.contains("/.local/share/flatpak/app/") { if let Some(app_part) = path_str.split("/.local/share/flatpak/app/").nth(1) { if let Some(app_id) = app_part.split('/').next() { return Ok(Some(app_id.to_string())); } } } // Check user data directories (~/.var/app/*) if path_str.contains("/.var/app/") { if let Some(app_part) = path_str.split("/.var/app/").nth(1) { if let Some(app_id) = app_part.split('/').next() { return Ok(Some(app_id.to_string())); } } } Ok(None) } fn get_package_info(&self, package: &str) -> Result, CartoError> { // Extract app ID from package name let app_id = if package.contains('(') && package.contains(')') { package.split('(').nth(1) .and_then(|s| s.split(')').next()) .unwrap_or(package) } else { package }; let output = Command::new("flatpak") .args(["info", app_id]) .output()?; if !output.status.success() { return Ok(None); } let stdout = String::from_utf8(output.stdout)?; let mut name = String::new(); let mut version = String::new(); let mut size = 0u64; for line in stdout.lines() { if line.starts_with("Name: ") { name = line.strip_prefix("Name: ").unwrap_or("").to_string(); } else if line.starts_with("Version: ") { version = line.strip_prefix("Version: ").unwrap_or("").to_string(); } else if line.starts_with("Installed size: ") { let size_str = line.strip_prefix("Installed size: ").unwrap_or(""); size = self.parse_size(size_str); } } if !name.is_empty() || !app_id.is_empty() { Ok(Some(PackageInfo { name: if name.is_empty() { format!("{} ({})", app_id, app_id) } else { format!("{} ({})", name, app_id) }, version: if version.is_empty() { "unknown".to_string() } else { version }, source: PackageSource::Flatpak, install_time: self.get_install_time(app_id, "system").unwrap_or(0), size, })) } else { Ok(None) } } } impl FlatpakManager { fn get_app_location(&self, app_id: &str) -> Result, CartoError> { let output = Command::new("flatpak") .args(["info", "--show-location", app_id]) .output()?; if output.status.success() { let location = String::from_utf8(output.stdout)?; let path = PathBuf::from(location.trim()); if path.exists() { return Ok(Some(path)); } } Ok(None) } fn get_flatpak_system_files(&self, app_id: &str) -> Vec { let mut files = Vec::new(); // Common Flatpak locations let locations = [ format!("/var/lib/flatpak/app/{}", app_id), format!("/var/lib/flatpak/exports/share/applications/{}.desktop", app_id), format!("/var/lib/flatpak/exports/share/icons/hicolor/scalable/apps/{}.svg", app_id), ]; for location in &locations { let path = PathBuf::from(location); if path.exists() { files.push(path); } } files } fn get_install_time(&self, _app_id: &str, _installation: &str) -> Option { // Flatpak doesn't easily expose install times, so we return None // Could potentially parse from filesystem timestamps None } fn get_package_size(&self, app_id: &str, _installation: &str) -> Option { if let Ok(output) = Command::new("flatpak") .args(["info", app_id]) .output() { if let Ok(stdout) = String::from_utf8(output.stdout) { for line in stdout.lines() { if line.starts_with("Installed size: ") { let size_str = line.strip_prefix("Installed size: ").unwrap_or(""); return Some(self.parse_size(size_str)); } } } } None } fn parse_size(&self, size_str: &str) -> u64 { // Parse sizes like "1.2 GB", "500 MB", "1,234 kB" let cleaned = size_str.replace(',', ""); let parts: Vec<&str> = cleaned.split_whitespace().collect(); if parts.len() >= 2 { if let Ok(num) = parts[0].parse::() { let multiplier: i64 = match parts[1].to_uppercase().as_str() { "B" | "BYTES" => 1, "KB" | "K" => 1_000, "MB" | "M" => 1_000_000, "GB" | "G" => 1_000_000_000, "TB" | "T" => 1_000_000_000_000, _ => 1, }; return (num * multiplier as f64) as u64; } } 0 } }