use clap::{Parser, Subcommand}; use glib::ExitCode; use gtk::prelude::*; use std::time::Instant; use crate::core::database::Database; use crate::core::discovery; use crate::core::duplicates; use crate::core::fuse; use crate::core::inspector; use crate::core::integrator; use crate::core::launcher; use crate::core::orphan; use crate::core::updater; use crate::core::verification; use crate::core::wayland; #[derive(Parser)] #[command(name = "driftwood", version, about = "Modern AppImage manager for GNOME desktops")] pub struct Cli { #[command(subcommand)] pub command: Option, } #[derive(Subcommand)] pub enum Commands { /// List all known AppImages List { /// Output format: table or json #[arg(long, default_value = "table")] format: String, }, /// Scan for AppImages in configured directories Scan, /// Integrate an AppImage (create .desktop and install icon) Integrate { /// Path to the AppImage path: String, }, /// Remove integration for an AppImage Remove { /// Path to the AppImage path: String, }, /// Clean orphaned desktop entries Clean, /// Inspect an AppImage and show its metadata Inspect { /// Path to the AppImage path: String, }, /// Show system status (FUSE, Wayland, desktop environment) Status, /// Check all AppImages for updates CheckUpdates, /// Find duplicate and multi-version AppImages Duplicates, /// Launch an AppImage (with tracking and FUSE detection) Launch { /// Path to the AppImage path: String, }, /// Update all AppImages that have available updates UpdateAll, /// Enable or disable autostart for an AppImage Autostart { /// Path to the AppImage path: String, /// Enable autostart #[arg(long, conflicts_with = "disable")] enable: bool, /// Disable autostart #[arg(long)] disable: bool, }, /// Remove all system modifications made by Driftwood Purge, /// Verify an AppImage's integrity (signature or SHA256) Verify { /// Path to the AppImage path: String, /// Expected SHA256 hash (if not provided, checks embedded signature) #[arg(long)] sha256: Option, }, /// Export app library to a JSON file Export { /// Output file path (default: stdout) #[arg(long)] output: Option, }, /// Import app library from a JSON file Import { /// Path to the JSON file to import file: String, }, } pub fn run_command(command: Commands) -> ExitCode { let db = match Database::open() { Ok(db) => db, Err(e) => { eprintln!("Error: Failed to open database: {}", e); return ExitCode::FAILURE; } }; match command { Commands::List { format } => cmd_list(&db, &format), Commands::Scan => cmd_scan(&db), Commands::Integrate { path } => cmd_integrate(&db, &path), Commands::Remove { path } => cmd_remove(&db, &path), Commands::Clean => cmd_clean(), Commands::Inspect { path } => cmd_inspect(&path), Commands::Status => cmd_status(), Commands::CheckUpdates => cmd_check_updates(&db), Commands::Duplicates => cmd_duplicates(&db), Commands::Launch { path } => cmd_launch(&db, &path), Commands::UpdateAll => cmd_update_all(&db), Commands::Autostart { path, enable, disable } => cmd_autostart(&db, &path, enable, disable), Commands::Purge => cmd_purge(&db), Commands::Verify { path, sha256 } => cmd_verify(&path, sha256.as_deref()), Commands::Export { output } => cmd_export(&db, output.as_deref()), Commands::Import { file } => cmd_import(&db, &file), } } fn cmd_list(db: &Database, format: &str) -> ExitCode { let records = match db.get_all_appimages() { Ok(r) => r, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; if records.is_empty() { println!("No AppImages found. Run 'driftwood scan' first."); return ExitCode::SUCCESS; } if format == "json" { let items: Vec = records .iter() .map(|r| { serde_json::json!({ "name": r.app_name.as_deref().unwrap_or(&r.filename), "version": r.app_version.as_deref().unwrap_or(""), "path": r.path, "size": r.size_bytes, "integrated": r.integrated, }) }) .collect(); println!("{}", serde_json::to_string_pretty(&items).unwrap_or_else(|_| "[]".into())); return ExitCode::SUCCESS; } // Table output let name_width = records .iter() .map(|r| r.app_name.as_deref().unwrap_or(&r.filename).len()) .max() .unwrap_or(4) .max(4) .min(30); let ver_width = records .iter() .map(|r| r.app_version.as_deref().unwrap_or("").len()) .max() .unwrap_or(7) .max(7) .min(15); println!( " {:10} {}", "Name", "Version", "Size", "Integrated", name_w = name_width, ver_w = ver_width, ); println!( " {:-10} ----------", "", "", "", name_w = name_width, ver_w = ver_width, ); let mut integrated_count = 0; for r in &records { let name = r.app_name.as_deref().unwrap_or(&r.filename); let display_name = if name.len() > name_width { &name[..name_width] } else { name }; let version = r.app_version.as_deref().unwrap_or(""); let size = humansize::format_size(r.size_bytes as u64, humansize::BINARY); let status = if r.integrated { "Yes" } else { "No" }; if r.integrated { integrated_count += 1; } println!( " {:10} {}", display_name, version, size, status, name_w = name_width, ver_w = ver_width, ); } println!(); println!( " {} AppImages found, {} integrated", records.len(), integrated_count, ); ExitCode::SUCCESS } fn cmd_scan(db: &Database) -> ExitCode { let settings = gtk::gio::Settings::new(crate::config::APP_ID); let dirs: Vec = settings .strv("scan-directories") .iter() .map(|s| s.to_string()) .collect(); println!("Scanning directories:"); for d in &dirs { let expanded = discovery::expand_tilde(d); println!(" {}", expanded.display()); } let start = Instant::now(); let discovered = discovery::scan_directories(&dirs); let total = discovered.len(); let mut new_count = 0; for d in &discovered { let existing = db .get_appimage_by_path(&d.path.to_string_lossy()) .ok() .flatten(); let modified = d.modified_time .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|dur| { chrono::DateTime::from_timestamp(dur.as_secs() as i64, 0) .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) }); let id = db.upsert_appimage( &d.path.to_string_lossy(), &d.filename, Some(d.appimage_type.as_i32()), d.size_bytes as i64, d.is_executable, modified.as_deref(), ).unwrap_or(0); if existing.is_none() { new_count += 1; println!(" [NEW] {}", d.filename); } let needs_metadata = existing .as_ref() .map(|r| r.app_name.is_none()) .unwrap_or(true); if needs_metadata { print!(" Inspecting {}... ", d.filename); match inspector::inspect_appimage(&d.path, &d.appimage_type) { Ok(metadata) => { let categories = if metadata.categories.is_empty() { None } else { Some(metadata.categories.join(";")) }; db.update_metadata( id, metadata.app_name.as_deref(), metadata.app_version.as_deref(), metadata.description.as_deref(), metadata.developer.as_deref(), categories.as_deref(), metadata.architecture.as_deref(), metadata.cached_icon_path.as_ref().map(|p| p.to_string_lossy()).as_deref(), Some(&metadata.desktop_entry_content), ).ok(); println!( "{}", metadata.app_name.as_deref().unwrap_or("(no name)") ); } Err(e) => { println!("failed: {}", e); } } } } let duration = start.elapsed(); db.log_scan( "cli", &dirs, total as i32, new_count, 0, duration.as_millis() as i64, ).ok(); println!(); println!( "Scan complete: {} found, {} new ({:.1}s)", total, new_count, duration.as_secs_f64(), ); ExitCode::SUCCESS } fn cmd_integrate(db: &Database, path: &str) -> ExitCode { let record = match db.get_appimage_by_path(path) { Ok(Some(r)) => r, Ok(None) => { eprintln!("Error: '{}' is not in the database. Run 'driftwood scan' first.", path); return ExitCode::FAILURE; } Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; if record.integrated { println!("{} is already integrated.", record.app_name.as_deref().unwrap_or(&record.filename)); return ExitCode::SUCCESS; } match integrator::integrate_tracked(&record, &db) { Ok(result) => { db.set_integrated( record.id, true, Some(&result.desktop_file_path.to_string_lossy()), ).ok(); println!( "Integrated {} -> {}", record.app_name.as_deref().unwrap_or(&record.filename), result.desktop_file_path.display(), ); ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); ExitCode::FAILURE } } } fn cmd_remove(db: &Database, path: &str) -> ExitCode { let record = match db.get_appimage_by_path(path) { Ok(Some(r)) => r, Ok(None) => { eprintln!("Error: '{}' is not in the database.", path); return ExitCode::FAILURE; } Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; if !record.integrated { println!("{} is not integrated.", record.app_name.as_deref().unwrap_or(&record.filename)); return ExitCode::SUCCESS; } integrator::undo_all_modifications(&db, record.id).ok(); match integrator::remove_integration(&record) { Ok(()) => { db.set_integrated(record.id, false, None).ok(); println!( "Removed integration for {}", record.app_name.as_deref().unwrap_or(&record.filename), ); ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); ExitCode::FAILURE } } } fn cmd_clean() -> ExitCode { let orphans = orphan::detect_orphans(); if orphans.is_empty() { println!("No orphaned desktop entries found."); return ExitCode::SUCCESS; } println!("Found {} orphaned entries:", orphans.len()); for entry in &orphans { println!( " {} (was: {})", entry.app_name.as_deref().unwrap_or("Unknown"), entry.original_appimage_path, ); } match orphan::clean_all_orphans() { Ok(summary) => { println!( "Cleaned {} desktop entries, {} icons", summary.entries_removed, summary.icons_removed, ); ExitCode::SUCCESS } Err(e) => { eprintln!("Error during cleanup: {}", e); ExitCode::FAILURE } } } fn cmd_inspect(path: &str) -> ExitCode { let file_path = std::path::Path::new(path); if !file_path.exists() { eprintln!("Error: file not found: {}", path); return ExitCode::FAILURE; } // Detect AppImage type let discovered = discovery::scan_directories(&[path.to_string()]); if discovered.is_empty() { // Try scanning the parent directory and finding by path let parent = file_path.parent().unwrap_or(std::path::Path::new(".")); let all = discovery::scan_directories(&[parent.to_string_lossy().to_string()]); let found = all.iter().find(|d| d.path == file_path); if let Some(d) = found { return do_inspect(file_path, &d.appimage_type); } eprintln!("Error: '{}' does not appear to be an AppImage", path); return ExitCode::FAILURE; } let d = &discovered[0]; do_inspect(file_path, &d.appimage_type) } fn cmd_status() -> ExitCode { println!("System Status"); println!("============="); println!(); // Display server let session = wayland::detect_session_type(); println!(" Display server: {}", session.label()); // Desktop environment let de = wayland::detect_desktop_environment(); println!(" Desktop: {}", de); // XWayland println!( " XWayland: {}", if wayland::has_xwayland() { "running" } else { "not detected" } ); println!(); // FUSE let fuse_info = fuse::detect_system_fuse(); println!(" FUSE status: {}", fuse_info.status.label()); println!(" libfuse2: {}", if fuse_info.has_libfuse2 { "yes" } else { "no" }); println!(" libfuse3: {}", if fuse_info.has_libfuse3 { "yes" } else { "no" }); println!(" fusermount: {}", fuse_info.fusermount_path.as_deref().unwrap_or("not found")); println!(" /dev/fuse: {}", if fuse_info.has_dev_fuse { "present" } else { "missing" }); if let Some(ref hint) = fuse_info.install_hint { println!(); println!(" Fix: {}", hint); } // AppImageLauncher if let Some(version) = fuse::detect_appimagelauncher() { println!(); println!(" WARNING: AppImageLauncher v{} detected (may conflict)", version); } println!(); // AppImageUpdate tool println!( " AppImageUpdate: {}", if updater::has_appimage_update_tool() { "available (delta updates enabled)" } else { "not found (full downloads only)" } ); ExitCode::SUCCESS } fn cmd_check_updates(db: &Database) -> ExitCode { let records = match db.get_all_appimages() { Ok(r) => r, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; if records.is_empty() { println!("No AppImages found. Run 'driftwood scan' first."); return ExitCode::SUCCESS; } println!("Checking {} AppImages for updates...", records.len()); println!(); let mut updates_found = 0; for record in &records { let name = record.app_name.as_deref().unwrap_or(&record.filename); let appimage_path = std::path::Path::new(&record.path); if !appimage_path.exists() { continue; } print!(" {} ... ", name); let (type_label, raw_info, check_result) = updater::check_appimage_for_update( appimage_path, record.app_version.as_deref(), ); // Store update info if raw_info.is_some() || type_label.is_some() { db.update_update_info(record.id, raw_info.as_deref(), type_label.as_deref()).ok(); } match check_result { Some(result) if result.update_available => { let latest = result.latest_version.as_deref().unwrap_or("unknown"); println!( "UPDATE AVAILABLE ({} -> {})", record.app_version.as_deref().unwrap_or("?"), latest, ); db.set_update_available(record.id, Some(latest), result.download_url.as_deref()).ok(); updates_found += 1; } Some(_) => { println!("up to date"); db.clear_update_available(record.id).ok(); } None => { if raw_info.is_none() { println!("no update info"); } else { println!("check failed"); } } } } println!(); if updates_found == 0 { println!("All AppImages are up to date."); } else { println!("{} update{} available.", updates_found, if updates_found == 1 { "" } else { "s" }); } ExitCode::SUCCESS } fn cmd_duplicates(db: &Database) -> ExitCode { let groups = duplicates::detect_duplicates(db); if groups.is_empty() { println!("No duplicate or multi-version AppImages found."); return ExitCode::SUCCESS; } let summary = duplicates::summarize_duplicates(&groups); println!( "Found {} duplicate groups ({} exact, {} multi-version)", summary.total_groups, summary.exact_duplicates, summary.multi_version, ); println!( "Potential savings: {}", humansize::format_size(summary.total_potential_savings, humansize::BINARY), ); println!(); for group in &groups { println!(" {} ({})", group.app_name, group.match_reason.label()); for member in &group.members { let r = &member.record; let version = r.app_version.as_deref().unwrap_or("?"); let size = humansize::format_size(r.size_bytes as u64, humansize::BINARY); let rec = member.recommendation.label(); println!(" {} v{} ({}) - {}", r.path, version, size, rec); } if group.potential_savings > 0 { println!( " Savings: {}", humansize::format_size(group.potential_savings, humansize::BINARY), ); } println!(); } ExitCode::SUCCESS } fn cmd_launch(db: &Database, path: &str) -> ExitCode { let file_path = std::path::Path::new(path); if !file_path.exists() { eprintln!("Error: file not found: {}", path); return ExitCode::FAILURE; } // Try to find in database for tracking let record = db.get_appimage_by_path(path).ok().flatten(); let launch_result = if let Some(ref record) = record { launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[]) } else { launcher::launch_appimage_simple(file_path, &[]) }; match launch_result { launcher::LaunchResult::Started { pid, method } => { let name = record.as_ref() .and_then(|r| r.app_name.as_deref()) .unwrap_or(path); println!("Launched {} (PID: {}, {})", name, pid, method.as_str()); ExitCode::SUCCESS } launcher::LaunchResult::Crashed { exit_code, stderr, method } => { let name = record.as_ref() .and_then(|r| r.app_name.as_deref()) .unwrap_or(path); eprintln!( "{} crashed on launch (exit code: {}, method: {})\n{}", name, exit_code.map(|c: i32| c.to_string()).unwrap_or_else(|| "unknown".into()), method.as_str(), stderr, ); ExitCode::FAILURE } launcher::LaunchResult::Failed(msg) => { eprintln!("Error: {}", msg); ExitCode::FAILURE } } } fn do_inspect(path: &std::path::Path, appimage_type: &discovery::AppImageType) -> ExitCode { println!("Inspecting: {}", path.display()); println!("Type: {:?}", appimage_type); match inspector::inspect_appimage(path, appimage_type) { Ok(metadata) => { println!("Name: {}", metadata.app_name.as_deref().unwrap_or("(unknown)")); println!("Version: {}", metadata.app_version.as_deref().unwrap_or("(unknown)")); if let Some(ref desc) = metadata.description { println!("Description: {}", desc); } if let Some(ref arch) = metadata.architecture { println!("Architecture: {}", arch); } if !metadata.categories.is_empty() { println!("Categories: {}", metadata.categories.join(", ")); } if let Some(ref icon) = metadata.cached_icon_path { println!("Icon: {}", icon.display()); } if !metadata.desktop_entry_content.is_empty() { println!(); println!("--- Desktop Entry ---"); println!("{}", metadata.desktop_entry_content); } ExitCode::SUCCESS } Err(e) => { eprintln!("Inspection failed: {}", e); ExitCode::FAILURE } } } fn cmd_update_all(db: &Database) -> ExitCode { let records = match db.get_all_appimages() { Ok(r) => r, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; let updatable: Vec<_> = records .iter() .filter(|r| r.latest_version.is_some()) .collect(); if updatable.is_empty() { println!("No updates available. Run 'driftwood check-updates' first."); return ExitCode::SUCCESS; } println!("Updating {} AppImages...", updatable.len()); let mut success_count = 0u32; let mut fail_count = 0u32; for record in &updatable { let name = record.app_name.as_deref().unwrap_or(&record.filename); let latest = record.latest_version.as_deref().unwrap_or("?"); print!(" {} -> {} ... ", name, latest); let appimage_path = std::path::Path::new(&record.path); match updater::perform_update(appimage_path, record.update_url.as_deref(), true, None) { Ok(result) => { println!("done ({})", result.new_path.display()); // Store rollback path if let Some(ref backup) = result.old_path_backup { db.set_previous_version(record.id, Some(&backup.to_string_lossy())).ok(); } db.clear_update_available(record.id).ok(); // Record in update history db.record_update( record.id, record.app_version.as_deref(), Some(latest), Some("cli"), None, true, ).ok(); success_count += 1; } Err(e) => { println!("FAILED: {}", e); fail_count += 1; } } } println!(); println!( "Updated {} of {} AppImages ({} failed)", success_count, updatable.len(), fail_count, ); ExitCode::SUCCESS } fn cmd_autostart(db: &Database, path: &str, enable: bool, disable: bool) -> ExitCode { let record = match db.get_appimage_by_path(path) { Ok(Some(r)) => r, Ok(None) => { eprintln!("Error: '{}' is not in the database. Run 'driftwood scan' first.", path); return ExitCode::FAILURE; } Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; let name = record.app_name.as_deref().unwrap_or(&record.filename); if enable { match integrator::enable_autostart(db, &record) { Ok(desktop_path) => { println!("Autostart enabled for {} ({})", name, desktop_path.display()); ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); ExitCode::FAILURE } } } else if disable { match integrator::disable_autostart(db, record.id) { Ok(()) => { println!("Autostart disabled for {}", name); ExitCode::SUCCESS } Err(e) => { eprintln!("Error: {}", e); ExitCode::FAILURE } } } else { // Show current status println!("{}: autostart {}", name, if record.autostart { "enabled" } else { "disabled" }); ExitCode::SUCCESS } } fn cmd_purge(db: &Database) -> ExitCode { let records = match db.get_all_appimages() { Ok(r) => r, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; let mut total_mods = 0u32; for record in &records { let mods = db.get_modifications(record.id).unwrap_or_default(); if !mods.is_empty() { let name = record.app_name.as_deref().unwrap_or(&record.filename); println!(" Undoing {} modifications for {}", mods.len(), name); total_mods += mods.len() as u32; integrator::undo_all_modifications(db, record.id).ok(); } } if total_mods == 0 { println!("No system modifications to undo."); } else { println!("Removed {} system modifications.", total_mods); } ExitCode::SUCCESS } fn cmd_verify(path: &str, expected_sha256: Option<&str>) -> ExitCode { let file_path = std::path::Path::new(path); if !file_path.exists() { eprintln!("Error: file not found: {}", path); return ExitCode::FAILURE; } if let Some(expected) = expected_sha256 { // SHA256 verification println!("Verifying SHA256 for {}...", path); let status = verification::verify_sha256(file_path, expected); println!(" {}", status.label()); match status { verification::VerificationStatus::ChecksumMatch => ExitCode::SUCCESS, _ => ExitCode::FAILURE, } } else { // Embedded signature check println!("Checking embedded signature for {}...", path); let status = verification::check_embedded_signature(file_path); println!(" {}", status.label()); // Also compute and display SHA256 println!(); match verification::compute_sha256(file_path) { Ok(hash) => { println!(" SHA256: {}", hash); ExitCode::SUCCESS } Err(e) => { eprintln!(" SHA256 computation failed: {}", e); ExitCode::FAILURE } } } } // --- Export/Import library --- fn cmd_export(db: &Database, output: Option<&str>) -> ExitCode { let records = match db.get_all_appimages() { Ok(r) => r, Err(e) => { eprintln!("Error: {}", e); return ExitCode::FAILURE; } }; let appimages: Vec = records .iter() .map(|r| { serde_json::json!({ "path": r.path, "app_name": r.app_name, "app_version": r.app_version, "integrated": r.integrated, "notes": r.notes, "categories": r.categories, }) }) .collect(); let export_data = serde_json::json!({ "version": 1, "exported_at": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), "appimages": appimages, }); let json_str = match serde_json::to_string_pretty(&export_data) { Ok(s) => s, Err(e) => { eprintln!("Error serializing export data: {}", e); return ExitCode::FAILURE; } }; if let Some(path) = output { if let Err(e) = std::fs::write(path, &json_str) { eprintln!("Error writing to {}: {}", path, e); return ExitCode::FAILURE; } } else { println!("{}", json_str); } eprintln!("Exported {} AppImages", records.len()); ExitCode::SUCCESS } fn cmd_import(db: &Database, file: &str) -> ExitCode { let content = match std::fs::read_to_string(file) { Ok(c) => c, Err(e) => { eprintln!("Error reading {}: {}", file, e); return ExitCode::FAILURE; } }; let data: serde_json::Value = match serde_json::from_str(&content) { Ok(v) => v, Err(e) => { eprintln!("Error parsing JSON: {}", e); return ExitCode::FAILURE; } }; let entries = match data.get("appimages").and_then(|a| a.as_array()) { Some(arr) => arr, None => { eprintln!("Error: JSON missing 'appimages' array"); return ExitCode::FAILURE; } }; let total = entries.len(); let mut imported = 0u32; let mut skipped = 0u32; for entry in entries { let path_str = match entry.get("path").and_then(|p| p.as_str()) { Some(p) => p, None => { skipped += 1; continue; } }; let file_path = std::path::Path::new(path_str); if !file_path.exists() { skipped += 1; continue; } // Validate that the file is actually an AppImage let appimage_type = match discovery::detect_appimage(file_path) { Some(t) => t, None => { eprintln!(" Skipping {} - not a valid AppImage", path_str); skipped += 1; continue; } }; let metadata = std::fs::metadata(file_path); let size_bytes = metadata.as_ref().map(|m| m.len() as i64).unwrap_or(0); let is_executable = metadata .as_ref() .map(|m| { use std::os::unix::fs::PermissionsExt; m.permissions().mode() & 0o111 != 0 }) .unwrap_or(false); let filename = file_path .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_default(); let file_modified = metadata .as_ref() .ok() .and_then(|m| m.modified().ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|dur| { chrono::DateTime::from_timestamp(dur.as_secs() as i64, 0) .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) }); let id = match db.upsert_appimage( path_str, &filename, Some(appimage_type.as_i32()), size_bytes, is_executable, file_modified.as_deref(), ) { Ok(id) => id, Err(e) => { eprintln!(" Error registering {}: {}", path_str, e); skipped += 1; continue; } }; // Restore metadata fields from the export let app_name = entry.get("app_name").and_then(|v| v.as_str()); let app_version = entry.get("app_version").and_then(|v| v.as_str()); let categories = entry.get("categories").and_then(|v| v.as_str()); if app_name.is_some() || app_version.is_some() { db.update_metadata( id, app_name, app_version, None, None, categories, None, None, None, ).ok(); } // Restore notes if present if let Some(notes_str) = entry.get("notes").and_then(|v| v.as_str()) { db.update_notes(id, Some(notes_str)).ok(); } // If it was integrated in the export, integrate it now let was_integrated = entry .get("integrated") .and_then(|v| v.as_bool()) .unwrap_or(false); if was_integrated { // Need the full record to integrate if let Ok(Some(record)) = db.get_appimage_by_id(id) { if !record.integrated { match integrator::integrate_tracked(&record, &db) { Ok(result) => { db.set_integrated( id, true, Some(&result.desktop_file_path.to_string_lossy()), ).ok(); } Err(e) => { eprintln!(" Warning: could not integrate {}: {}", path_str, e); } } } } } imported += 1; } eprintln!( "Imported {} of {} AppImages ({} skipped - file not found)", imported, total, skipped, ); ExitCode::SUCCESS }