| 1 | //! wanda doctor - Diagnose issues |
| 2 | |
| 3 | use clap::Args; |
| 4 | use console::style; |
| 5 | use std::path::PathBuf; |
| 6 | use wanda_core::{ |
| 7 | config::WandaConfig, |
| 8 | prefix::{PrefixHealth, PrefixManager}, |
| 9 | steam::{ProtonCompatibility, ProtonManager, SteamInstallation}, |
| 10 | Result, |
| 11 | }; |
| 12 | |
| 13 | #[derive(Args)] |
| 14 | pub struct DoctorArgs { |
| 15 | /// Generate full diagnostic report |
| 16 | #[arg(long)] |
| 17 | full: bool, |
| 18 | |
| 19 | /// Export report to file |
| 20 | #[arg(long)] |
| 21 | export: Option<PathBuf>, |
| 22 | |
| 23 | /// Attempt to fix detected issues |
| 24 | #[arg(long)] |
| 25 | fix: bool, |
| 26 | } |
| 27 | |
| 28 | pub async fn run(args: DoctorArgs, config_path: Option<PathBuf>) -> Result<()> { |
| 29 | let config = match &config_path { |
| 30 | Some(path) => WandaConfig::load_from(path)?, |
| 31 | None => WandaConfig::load()?, |
| 32 | }; |
| 33 | |
| 34 | println!("WANDA Diagnostic Report\n"); |
| 35 | println!("{}\n", "=".repeat(50)); |
| 36 | |
| 37 | let mut issues = Vec::new(); |
| 38 | |
| 39 | // Check Steam |
| 40 | print_section("Steam Installation"); |
| 41 | match SteamInstallation::discover(&config) { |
| 42 | Ok(steam) => { |
| 43 | println!( |
| 44 | " {} Found at {}{}", |
| 45 | style("OK").green(), |
| 46 | steam.root_path.display(), |
| 47 | if steam.is_flatpak { " (Flatpak)" } else { "" } |
| 48 | ); |
| 49 | println!(" {} {} libraries", style("OK").green(), steam.libraries.len()); |
| 50 | |
| 51 | let total_games: usize = steam.libraries.iter().map(|l| l.apps.len()).sum(); |
| 52 | let proton_games = steam |
| 53 | .get_all_games() |
| 54 | .iter() |
| 55 | .filter(|g| g.uses_proton()) |
| 56 | .count(); |
| 57 | println!(" {} {} games ({} using Proton)", style("OK").green(), total_games, proton_games); |
| 58 | |
| 59 | // Check Proton |
| 60 | print_section("Proton"); |
| 61 | match ProtonManager::discover(&steam, &config) { |
| 62 | Ok(proton_mgr) => { |
| 63 | if proton_mgr.versions.is_empty() { |
| 64 | println!(" {} No Proton versions found", style("ERROR").red()); |
| 65 | issues.push("No Proton versions installed".to_string()); |
| 66 | } else { |
| 67 | println!( |
| 68 | " {} {} versions found", |
| 69 | style("OK").green(), |
| 70 | proton_mgr.versions.len() |
| 71 | ); |
| 72 | |
| 73 | // List versions in full mode |
| 74 | if args.full { |
| 75 | for v in &proton_mgr.versions { |
| 76 | let compat = match v.compatibility { |
| 77 | ProtonCompatibility::Recommended => { |
| 78 | style("RECOMMENDED").green() |
| 79 | } |
| 80 | ProtonCompatibility::Supported => style("SUPPORTED").blue(), |
| 81 | ProtonCompatibility::Experimental => { |
| 82 | style("EXPERIMENTAL").yellow() |
| 83 | } |
| 84 | ProtonCompatibility::Unsupported => style("UNSUPPORTED").red(), |
| 85 | }; |
| 86 | println!(" {} {}", v.name, compat); |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | if let Some(recommended) = proton_mgr.get_recommended() { |
| 91 | println!( |
| 92 | " {} Recommended: {}", |
| 93 | style("OK").green(), |
| 94 | recommended.name |
| 95 | ); |
| 96 | } else { |
| 97 | println!( |
| 98 | " {} No recommended Proton version", |
| 99 | style("WARN").yellow() |
| 100 | ); |
| 101 | issues.push("No recommended Proton version found".to_string()); |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | Err(e) => { |
| 106 | println!(" {} Error: {}", style("ERROR").red(), e); |
| 107 | issues.push(format!("Proton detection failed: {}", e)); |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | Err(e) => { |
| 112 | println!(" {} Not found: {}", style("ERROR").red(), e); |
| 113 | issues.push("Steam installation not found".to_string()); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | // Check WANDA prefix |
| 118 | print_section("WANDA Prefix"); |
| 119 | let mut prefix_manager = PrefixManager::new(&config); |
| 120 | prefix_manager.load()?; |
| 121 | |
| 122 | if let Some(prefix) = prefix_manager.get("default") { |
| 123 | println!(" {} Found at {}", style("OK").green(), prefix.path.display()); |
| 124 | |
| 125 | let health = prefix_manager.validate("default")?; |
| 126 | match health { |
| 127 | PrefixHealth::Healthy => { |
| 128 | println!(" {} Prefix is healthy", style("OK").green()); |
| 129 | } |
| 130 | PrefixHealth::NeedsRepair(ref issue_list) => { |
| 131 | println!(" {} Prefix needs repair:", style("WARN").yellow()); |
| 132 | for issue in issue_list { |
| 133 | println!(" - {}", issue); |
| 134 | issues.push(format!("Prefix issue: {}", issue)); |
| 135 | } |
| 136 | } |
| 137 | PrefixHealth::Corrupted(reason) => { |
| 138 | println!(" {} Prefix is corrupted: {}", style("ERROR").red(), reason); |
| 139 | issues.push(format!("Prefix corrupted: {}", reason)); |
| 140 | } |
| 141 | PrefixHealth::NotCreated => { |
| 142 | println!(" {} Prefix not initialized", style("WARN").yellow()); |
| 143 | issues.push("WANDA prefix not created".to_string()); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | // Check WeMod |
| 148 | print_section("WeMod"); |
| 149 | if prefix.wemod_installed { |
| 150 | println!( |
| 151 | " {} Installed (version {})", |
| 152 | style("OK").green(), |
| 153 | prefix.wemod_version.as_deref().unwrap_or("unknown") |
| 154 | ); |
| 155 | |
| 156 | if prefix.wemod_exe().exists() { |
| 157 | println!(" {} Executable found", style("OK").green()); |
| 158 | } else { |
| 159 | println!(" {} Executable missing", style("ERROR").red()); |
| 160 | issues.push("WeMod executable not found".to_string()); |
| 161 | } |
| 162 | } else { |
| 163 | println!(" {} Not installed", style("WARN").yellow()); |
| 164 | issues.push("WeMod not installed".to_string()); |
| 165 | } |
| 166 | } else { |
| 167 | println!(" {} Not initialized", style("WARN").yellow()); |
| 168 | issues.push("WANDA not initialized".to_string()); |
| 169 | } |
| 170 | |
| 171 | // Check system dependencies |
| 172 | print_section("System Dependencies"); |
| 173 | check_command("wine", "Wine", &mut issues).await; |
| 174 | check_command("winetricks", "Winetricks", &mut issues).await; |
| 175 | check_command("steam", "Steam CLI", &mut issues).await; |
| 176 | check_command("xdg-open", "XDG utilities", &mut issues).await; |
| 177 | |
| 178 | // Summary |
| 179 | println!("\n{}", "=".repeat(50)); |
| 180 | if issues.is_empty() { |
| 181 | println!( |
| 182 | "\n{} All checks passed! WANDA is ready to use.", |
| 183 | style("SUCCESS").green().bold() |
| 184 | ); |
| 185 | } else { |
| 186 | println!( |
| 187 | "\n{} Found {} issue(s):\n", |
| 188 | style("ISSUES").yellow().bold(), |
| 189 | issues.len() |
| 190 | ); |
| 191 | for (i, issue) in issues.iter().enumerate() { |
| 192 | println!(" {}. {}", i + 1, issue); |
| 193 | } |
| 194 | |
| 195 | println!("\nSuggestions:"); |
| 196 | if issues.iter().any(|i| i.contains("not initialized")) { |
| 197 | println!(" - Run 'wanda init' to initialize WANDA"); |
| 198 | } |
| 199 | if issues.iter().any(|i| i.contains("WeMod")) { |
| 200 | println!(" - Run 'wanda wemod install' to install WeMod"); |
| 201 | } |
| 202 | if issues.iter().any(|i| i.contains("Prefix")) { |
| 203 | println!(" - Run 'wanda prefix repair' to fix prefix issues"); |
| 204 | } |
| 205 | if issues.iter().any(|i| i.contains("Proton")) { |
| 206 | println!(" - Install GE-Proton via ProtonUp-Qt or Steam"); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // Export if requested |
| 211 | if let Some(export_path) = args.export { |
| 212 | let report = format!( |
| 213 | "WANDA Diagnostic Report\n\nIssues:\n{}\n", |
| 214 | issues.join("\n") |
| 215 | ); |
| 216 | std::fs::write(&export_path, report)?; |
| 217 | println!("\nReport exported to: {}", export_path.display()); |
| 218 | } |
| 219 | |
| 220 | Ok(()) |
| 221 | } |
| 222 | |
| 223 | fn print_section(name: &str) { |
| 224 | println!("\n{}", style(format!("[{}]", name)).bold()); |
| 225 | } |
| 226 | |
| 227 | async fn check_command(cmd: &str, name: &str, issues: &mut Vec<String>) { |
| 228 | let result = tokio::process::Command::new("which") |
| 229 | .arg(cmd) |
| 230 | .output() |
| 231 | .await; |
| 232 | |
| 233 | match result { |
| 234 | Ok(output) if output.status.success() => { |
| 235 | let path = String::from_utf8_lossy(&output.stdout); |
| 236 | println!(" {} {} ({})", style("OK").green(), name, path.trim()); |
| 237 | } |
| 238 | _ => { |
| 239 | println!(" {} {} not found", style("WARN").yellow(), name); |
| 240 | issues.push(format!("{} not found in PATH", name)); |
| 241 | } |
| 242 | } |
| 243 | } |
| 244 |