use anyhow::Result;
use aranet_core::scan::{self, ScanOptions};
use owo_colors::OwoColorize;
use crate::style;
struct Check {
#[allow(dead_code)]
name: &'static str,
passed: bool,
warning: bool,
message: String,
}
impl Check {
fn pass(name: &'static str, message: impl Into<String>) -> Self {
Self {
name,
passed: true,
warning: false,
message: message.into(),
}
}
fn warn(name: &'static str, message: impl Into<String>) -> Self {
Self {
name,
passed: true,
warning: true,
message: message.into(),
}
}
fn fail(name: &'static str, message: impl Into<String>) -> Self {
Self {
name,
passed: false,
warning: false,
message: message.into(),
}
}
}
pub async fn cmd_doctor(verbose: bool, no_color: bool) -> Result<()> {
println!(
"{}",
style::format_title("Aranet Doctor - BLE Diagnostics", no_color)
);
println!();
let mut checks: Vec<Check> = Vec::new();
let mut check_num = 0;
check_num += 1;
print_check_start(check_num, "Bluetooth Adapter", no_color);
let adapter_check = check_adapter().await;
print_check_result(&adapter_check, no_color);
let adapter_ok = adapter_check.passed;
checks.push(adapter_check);
check_num += 1;
print_check_start(check_num, "Bluetooth Permissions", no_color);
let permission_check = check_permissions().await;
print_check_result(&permission_check, no_color);
checks.push(permission_check);
if adapter_ok {
check_num += 1;
print_check_start(check_num, "Device Scan", no_color);
let scan_check = check_scan().await;
print_check_result(&scan_check, no_color);
checks.push(scan_check);
}
check_num += 1;
print_check_start(check_num, "Configuration", no_color);
let config_check = check_config();
print_check_result(&config_check, no_color);
checks.push(config_check);
println!();
println!("{}", "─".repeat(50));
let passed = checks.iter().filter(|c| c.passed && !c.warning).count();
let warnings = checks.iter().filter(|c| c.warning).count();
let failed = checks.iter().filter(|c| !c.passed).count();
let summary = if no_color {
format!(
"Summary: {} passed, {} warnings, {} failed",
passed, warnings, failed
)
} else {
format!(
"Summary: {} passed, {} warnings, {} failed",
format!("{}", passed).green(),
format!("{}", warnings).yellow(),
format!("{}", failed).red()
)
};
println!("{}", summary);
println!();
if failed > 0 {
print_troubleshooting_help(verbose, no_color);
} else if warnings > 0 {
println!("System is functional but some checks had warnings.");
println!("Run with --verbose for more details.");
} else {
let msg = "All checks passed! Your system is ready to use Aranet devices.";
println!("{}", style::format_success(msg, no_color));
}
Ok(())
}
fn print_check_start(num: usize, name: &str, no_color: bool) {
use std::io::{Write, stdout};
if no_color {
print!("[{}] {} ... ", num, name);
} else {
print!("{} {} ... ", format!("[{}]", num).dimmed(), name);
}
let _ = stdout().flush();
}
fn print_check_result(check: &Check, no_color: bool) {
let (icon, msg) = if check.passed && !check.warning {
if no_color {
("[OK]".to_string(), check.message.clone())
} else {
(format!("{}", "[OK]".green()), check.message.clone())
}
} else if check.warning {
if no_color {
("[!!]".to_string(), check.message.clone())
} else {
(
format!("{}", "[!!]".yellow()),
format!("{}", check.message.yellow()),
)
}
} else if no_color {
("[FAIL]".to_string(), check.message.clone())
} else {
(
format!("{}", "[FAIL]".red()),
format!("{}", check.message.red()),
)
};
println!("{} {}", icon, msg);
}
async fn check_adapter() -> Check {
match scan::get_adapter().await {
Ok(_adapter) => Check::pass("Bluetooth Adapter", "Found and accessible"),
Err(e) => {
let msg = format!("Not available ({})", e);
Check::fail("Bluetooth Adapter", msg)
}
}
}
async fn check_scan() -> Check {
let options = ScanOptions::default()
.duration_secs(3)
.filter_aranet_only(true);
match scan::scan_with_options(options).await {
Ok(devices) => {
if devices.is_empty() {
Check::warn("BLE Scanning", "No Aranet devices found nearby")
} else {
let names: Vec<String> = devices.iter().filter_map(|d| d.name.clone()).collect();
Check::pass(
"BLE Scanning",
format!("Found {} device(s): {}", devices.len(), names.join(", ")),
)
}
}
Err(e) => Check::fail("BLE Scanning", format!("Failed ({})", e)),
}
}
async fn check_permissions() -> Check {
#[cfg(target_os = "macos")]
{
match scan::get_adapter().await {
Ok(_) => Check::pass("Bluetooth Permissions", "Bluetooth access granted"),
Err(_) => Check::warn(
"Bluetooth Permissions",
"May need to grant Bluetooth permission in System Settings",
),
}
}
#[cfg(target_os = "linux")]
{
if let Ok(output) = std::process::Command::new("groups").output() {
let groups = String::from_utf8_lossy(&output.stdout);
if groups.contains("bluetooth") {
Check::pass("Bluetooth Permissions", "User is in bluetooth group")
} else {
Check::warn(
"Bluetooth Permissions",
"User not in bluetooth group (may need: sudo usermod -aG bluetooth $USER)",
)
}
} else {
Check::warn("Bluetooth Permissions", "Could not check group membership")
}
}
#[cfg(target_os = "windows")]
{
Check::pass(
"Bluetooth Permissions",
"Windows grants Bluetooth access by default",
)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Check::warn(
"Bluetooth Permissions",
"Unknown platform - cannot check permissions",
)
}
}
fn check_config() -> Check {
use crate::config::Config;
let config_path = Config::path();
if !config_path.exists() {
return Check::pass("Configuration", "No config file (using defaults)");
}
match Config::load() {
Ok(config) => {
let alias_count = config.aliases.len();
let default_device = config.device.is_some();
Check::pass(
"Configuration",
format!(
"Valid ({} alias{}, default device: {})",
alias_count,
if alias_count == 1 { "" } else { "es" },
if default_device { "set" } else { "not set" }
),
)
}
Err(err) => Check::warn("Configuration", format!("Invalid config file ({err})")),
}
}
fn print_troubleshooting_help(verbose: bool, no_color: bool) {
let title = if no_color {
"Troubleshooting Tips:".to_string()
} else {
format!("{}", "Troubleshooting Tips:".yellow())
};
println!("{}", title);
println!();
#[cfg(target_os = "macos")]
{
println!("macOS:");
println!(" • Ensure Bluetooth is enabled in System Settings");
println!(" • Grant Bluetooth permission to Terminal/your app");
println!(" • Try: System Settings → Privacy & Security → Bluetooth");
if verbose {
println!(" • Check if other BLE apps work (e.g., LightBlue)");
println!(" • Try resetting Bluetooth: sudo pkill bluetoothd");
}
}
#[cfg(target_os = "linux")]
{
println!("Linux:");
println!(" • Ensure BlueZ is installed: sudo apt install bluez");
println!(" • Check Bluetooth service: systemctl status bluetooth");
println!(" • Add user to bluetooth group: sudo usermod -aG bluetooth $USER");
if verbose {
println!(" • Check adapter: hciconfig -a");
println!(" • Restart Bluetooth: sudo systemctl restart bluetooth");
}
}
#[cfg(target_os = "windows")]
{
println!("Windows:");
println!(" • Ensure Bluetooth is enabled in Settings");
println!(" • Check Device Manager for Bluetooth adapter");
println!(" • Update Bluetooth drivers if needed");
if verbose {
println!(" • Try: Settings → Bluetooth & devices → Bluetooth → On");
}
}
println!();
}