use crate::config::Config; use anyhow::Result; use std::collections::HashMap; use std::path::{Path, PathBuf}; const MAX_DEPTH: u32 = 3; pub fn run(config: &Config, extra_dirs: &[PathBuf], apply: bool) -> Result<()> { let mut roots = default_roots(); roots.extend(extra_dirs.iter().cloned()); roots.sort(); roots.dedup(); let existing: Vec = roots.into_iter().filter(|r| r.is_dir()).collect(); let prefixes = scan_prefixes(&existing); println!( "Scanned {} root{} → found {} prefix{}.\n", existing.len(), if existing.len() == 1 { "" } else { "s" }, prefixes.len(), if prefixes.len() == 1 { "" } else { "es" }, ); let by_launcher = match_launchers(config, &prefixes); if apply { apply_findings(config, &by_launcher)?; } else { print_findings(config, &by_launcher); } Ok(()) } fn default_roots() -> Vec { let Ok(home) = std::env::var("HOME").map(PathBuf::from) else { return Vec::new(); }; vec![ home.join("Games"), home.join(".wine"), home.join(".local/share/lutris/runners/wine"), home.join(".local/share/bottles/bottles"), home.join(".var/app/com.usebottles.bottles/data/bottles/bottles"), home.join("Games/Heroic/Prefixes/default"), home.join(".var/app/com.heroicgameslauncher.hgl/config/heroic/Prefixes/default"), ] } fn scan_prefixes(roots: &[PathBuf]) -> Vec { let mut out = Vec::new(); for root in roots { collect_prefixes(root, 0, &mut out); } out.sort(); out.dedup(); out } fn collect_prefixes(dir: &Path, depth: u32, out: &mut Vec) { if dir.join("drive_c").is_dir() { out.push(dir.to_path_buf()); return; } // Proton / umu layout: /pfx/drive_c if dir.join("pfx/drive_c").is_dir() { out.push(dir.join("pfx")); return; } if depth >= MAX_DEPTH { return; } let Ok(entries) = std::fs::read_dir(dir) else { return; }; for entry in entries.flatten() { if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { collect_prefixes(&entry.path(), depth + 1, out); } } } fn match_launchers(config: &Config, prefixes: &[PathBuf]) -> HashMap> { let mut by_launcher: HashMap> = HashMap::new(); for l in &config.launchers { for prefix in prefixes { if prefix.join("drive_c").join(&l.exe_path).exists() { by_launcher .entry(l.name.clone()) .or_default() .push(prefix.clone()); } } } by_launcher } fn print_findings(config: &Config, by_launcher: &HashMap>) { let mut any_divergent = false; for l in &config.launchers { match by_launcher.get(&l.name) { None => { println!(" · {:12} not found", l.name); } Some(matches) if matches.len() > 1 => { println!(" \x1b[33m⚠\x1b[0m {:12} multiple prefixes:", l.name); for p in matches { println!(" {}", p.display()); } } Some(matches) => { let detected = &matches[0]; if *detected == l.prefix_dir { println!(" \x1b[1;32m✓\x1b[0m {:12} {}", l.name, detected.display()); } else { any_divergent = true; println!( " \x1b[36m→\x1b[0m {:12} {} (was {})", l.name, detected.display(), l.prefix_dir.display() ); } } } } if any_divergent { println!("\nRerun with --apply to update config."); } } fn apply_findings(config: &Config, by_launcher: &HashMap>) -> Result<()> { let mut c = config.clone(); let mut updated = 0; let mut ambiguous = 0; for l in c.launchers.iter_mut() { let Some(matches) = by_launcher.get(&l.name) else { continue; }; if matches.len() > 1 { ambiguous += 1; println!( " \x1b[33m⚠\x1b[0m {:12} ambiguous — update via `config edit`", l.name ); continue; } let detected = &matches[0]; if *detected == l.prefix_dir { println!(" \x1b[1;32m✓\x1b[0m {:12} unchanged", l.name); continue; } println!( " \x1b[1;32m→\x1b[0m {:12} {} → {}", l.name, l.prefix_dir.display(), detected.display() ); l.prefix_dir = detected.clone(); updated += 1; } if updated > 0 { c.save()?; println!( "\nUpdated {updated} launcher{}.", if updated == 1 { "" } else { "s" } ); } else { println!("\nNothing to update."); } if ambiguous > 0 { println!( "{ambiguous} launcher{} skipped (multiple matches).", if ambiguous == 1 { "" } else { "s" } ); } Ok(()) }