use crate::config::{Config, Launcher}; use anyhow::Result; use owo_colors::OwoColorize; use std::os::unix::fs::MetadataExt; use std::path::Path; use std::process::{Command, Stdio}; #[derive(Debug, Clone)] pub struct CheckResult { pub label: String, pub pass: bool, pub detail: String, } impl CheckResult { fn pass(label: impl Into, detail: impl Into) -> Self { Self { label: label.into(), pass: true, detail: detail.into() } } fn fail(label: impl Into, detail: impl Into) -> Self { Self { label: label.into(), pass: false, detail: detail.into() } } } pub fn run_checks(config: &Config, name: Option<&str>) -> Result> { let mut checks = vec![ global_umu_check(), global_vulkan_check(), global_display_check(), compat_dir_check(config), wineserver_check(config), ]; let launchers: Vec<&Launcher> = if let Some(n) = name { let l = config .find(n) .ok_or_else(|| anyhow::anyhow!("unknown launcher '{n}'"))?; vec![l] } else { config.launchers.iter().collect() }; for l in launchers { checks.extend(launcher_checks(l)); } Ok(checks) } pub fn run(config: &Config, name: Option<&str>) -> Result<()> { let checks = run_checks(config, name)?; let mut issues = 0u32; println!(); for c in &checks { if !c.pass { issues += 1; } if c.pass { println!(" {} {:24} {}", "✓".green().bold(), c.label, c.detail); } else { println!(" {} {:24} {}", "✗".red().bold(), c.label, c.detail); } } println!(); if issues == 0 { println!(" {}", "All checks passed.".green().bold()); } else { println!(" {}", format!("{issues} issue(s) found — see ✗ items above.").red().bold()); } println!(); Ok(()) } fn global_umu_check() -> CheckResult { match which("umu-run") { Some(p) => CheckResult::pass("umu-run", format!("found at {p}")), None => CheckResult::fail("umu-run", "not found — install umu-launcher"), } } fn global_vulkan_check() -> CheckResult { let ok = Command::new("vulkaninfo") .arg("--summary") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false); if ok { CheckResult::pass("vulkan", "vulkaninfo OK") } else { CheckResult::fail( "vulkan", "vulkaninfo failed — check GPU drivers / vulkan-tools", ) } } fn global_display_check() -> CheckResult { let display = std::env::var("DISPLAY").ok(); let wayland = std::env::var("WAYLAND_DISPLAY").ok(); match (display, wayland) { (Some(d), Some(_)) => CheckResult::pass("display", format!("XWayland (DISPLAY={d})")), (Some(d), None) => CheckResult::pass("display", format!("X11 (DISPLAY={d})")), (None, Some(_)) => CheckResult::fail( "display", "Wayland session but DISPLAY unset; XWayland needed", ), (None, None) => CheckResult::fail("display", "neither DISPLAY nor WAYLAND_DISPLAY set"), } } fn compat_dir_check(config: &Config) -> CheckResult { let n = count_ge_proton(&config.proton_compat_dir); if config.proton_version == "GE-Proton" { CheckResult::pass( "proton", format!( "tracking latest; {n} version(s) in {}", config.proton_compat_dir.display() ), ) } else { let path = config.proton_compat_dir.join(&config.proton_version); if path.exists() { CheckResult::pass("proton", format!("{} installed", config.proton_version)) } else { CheckResult::fail( "proton", format!( "{} missing — run: umutray update-proton --version={}", config.proton_version, config.proton_version ), ) } } } fn wineserver_check(config: &Config) -> CheckResult { let count = wineserver_count(); if count == 0 { return CheckResult::pass("wine procs", "no wineserver running"); } let any_running = config.launchers.iter().any(crate::launcher::is_running); if any_running { CheckResult::pass( "wine procs", format!("{count} wineserver process(es); launcher active"), ) } else { CheckResult::fail( "wine procs", format!("{count} stale wineserver process(es) — try: umutray kill"), ) } } fn wineserver_count() -> usize { Command::new("pgrep") .args(["-c", "-f", "wineserver"]) .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .and_then(|s| s.trim().parse::().ok()) .unwrap_or(0) } fn launcher_checks(l: &Launcher) -> Vec { let mut out = Vec::new(); let tag = format!("[{}]", l.name); if !l.prefix_dir.exists() { out.push(CheckResult::fail( format!("{tag} prefix"), format!( "{} missing — run: umutray setup {}", l.prefix_dir.display(), l.name ), )); return out; } out.push(CheckResult::pass( format!("{tag} prefix"), l.prefix_dir.display().to_string(), )); let exe = l.full_exe_path(); if exe.exists() { out.push(CheckResult::pass(format!("{tag} exe"), "installed")); } else { out.push(CheckResult::fail( format!("{tag} exe"), format!("missing — run: umutray setup {}", l.name), )); } if is_owned_by_current_user(&l.prefix_dir) { out.push(CheckResult::pass(format!("{tag} owner"), "owned by current user")); } else { out.push(CheckResult::fail( format!("{tag} owner"), "not owned by current user", )); } if crate::launcher::is_running(l) { out.push(CheckResult::pass(format!("{tag} process"), "currently running")); } else { out.push(CheckResult::pass(format!("{tag} process"), "not running")); } out } fn count_ge_proton(dir: &Path) -> usize { std::fs::read_dir(dir) .ok() .map(|entries| { entries .filter_map(|e| e.ok()) .filter(|e| { e.file_name() .to_str() .map(|s| s.starts_with("GE-Proton")) .unwrap_or(false) }) .count() }) .unwrap_or(0) } fn which(cmd: &str) -> Option { Command::new("which") .arg(cmd) .output() .ok() .filter(|o| o.status.success()) .and_then(|o| String::from_utf8(o.stdout).ok()) .map(|s| s.trim().to_string()) } fn is_owned_by_current_user(path: &Path) -> bool { let file_uid = match std::fs::metadata(path) { Ok(m) => m.uid(), Err(_) => return true, }; let current_uid: Option = Command::new("id") .arg("-u") .output() .ok() .and_then(|o| String::from_utf8(o.stdout).ok()) .and_then(|s| s.trim().parse().ok()); match current_uid { Some(uid) => uid == file_uid, None => true, } }