1bacf345f0
Pure whitespace normalization — no logic changes. Mostly: - collapsing multi-line match/if arms rustfmt prefers inline - inlining short `with_context`/`ok_or_else` closures - reformatting nested method chains for consistency Build and clippy stay clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
179 lines
5.2 KiB
Rust
179 lines
5.2 KiB
Rust
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<PathBuf> = 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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<PathBuf>) {
|
|
if dir.join("drive_c").is_dir() {
|
|
out.push(dir.to_path_buf());
|
|
return;
|
|
}
|
|
// Proton / umu layout: <gameid>/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<String, Vec<PathBuf>> {
|
|
let mut by_launcher: HashMap<String, Vec<PathBuf>> = 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<String, Vec<PathBuf>>) {
|
|
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<String, Vec<PathBuf>>) -> 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(())
|
|
}
|