iwconf-win 0.1.2

A colorful, iwconfig-style WLAN CLI for Windows, backed by the native WLAN API (no netsh required).
mod cli;
mod config;
mod display;
mod error;
mod select;
mod wlan;

use std::process::ExitCode;
use std::time::Duration;

use clap::CommandFactory;
use clap_version_flag::{colorful_version, parse_with_version};

use cli::Cli;
use display::DisplayOpts;
use error::{IwconfError, Result};

fn main() -> ExitCode {
    let version = colorful_version!();
    let cli: Cli = match parse_with_version(Cli::command(), &version) {
        Ok(cli) => cli,
        Err(e) => {
            eprintln!("{e}");
            return ExitCode::FAILURE;
        }
    };

    match run(cli) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("iwconf: {e}");
            ExitCode::FAILURE
        }
    }
}

#[cfg(not(windows))]
fn run(_cli: Cli) -> Result<()> {
    Err(IwconfError::UnsupportedPlatform)
}

#[cfg(windows)]
fn run(cli: Cli) -> Result<()> {
    let settings = config::load()?;
    let opts = DisplayOpts::from_cli(
        cli.no_color || settings.no_color,
        cli.no_emoji || settings.no_emoji,
        cli.plain,
        cli.json,
    );
    let timeout = Duration::from_secs(cli.timeout.unwrap_or(settings.connect_timeout));

    let handle = wlan::native::WlanHandle::open()?;
    let interfaces = wlan::native::list_interfaces(&handle)?;

    let pattern = cli.iface.clone().or_else(|| settings.default_iface.clone());

    if cli.is_action() {
        // Mutating actions (connect/disconnect) require exactly one target
        // interface — pick it explicitly, or fall back to the first one if
        // there's only one adapter in the system at all.
        let iface = match &pattern {
            Some(p) => select::select(&interfaces, p, cli.regex)?,
            None if interfaces.len() == 1 => &interfaces[0],
            None => {
                return Err(IwconfError::Other(
                    "multiple interfaces present; use -i/--iface to pick one".into(),
                ))
            }
        };
        let guid_str = iface.guid.clone();
        let guid = parse_guid(&guid_str)?;

        if cli.disconnect {
            wlan::native::disconnect(&handle, &guid)?;
            println!("{}", success_line("disconnected", &iface.name, &opts));
            return Ok(());
        }

        if let Some(ssid) = &cli.essid {
            if cli.profile.is_none() && cli.key.is_none() {
                // No key and no saved profile: only proceed for open networks
                // the driver already reports as connectable-without-security.
                let networks = wlan::native::available_networks(&handle, &guid)?;
                let target = networks.iter().find(|n| &n.ssid == ssid);
                if let Some(net) = target {
                    if !net.authentication.eq_ignore_ascii_case("open")
                        && net.profile_name.is_none()
                    {
                        return Err(IwconfError::MissingPassphrase);
                    }
                }
            }
            let outcome = wlan::native::connect(
                &handle,
                &guid,
                ssid,
                cli.key.as_deref(),
                cli.profile.as_deref(),
                timeout,
            )?;
            match outcome {
                wlan::ConnectOutcome::Connected => {
                    println!("{}", success_line(&format!("connected to {ssid}"), &iface.name, &opts));
                }
                wlan::ConnectOutcome::TimedOut => {
                    return Err(IwconfError::ConnectTimeout(ssid.clone()));
                }
                wlan::ConnectOutcome::Failed => {
                    return Err(IwconfError::Other(format!("failed to connect to {ssid}")));
                }
            }
        }
        return Ok(());
    }

    // Read-only paths: show interface(s), or scan for networks.
    let selected: Vec<&wlan::Interface> = match &pattern {
        Some(p) => vec![select::select(&interfaces, p, cli.regex)?],
        None => interfaces.iter().collect(),
    };

    if cli.scan {
        for iface in &selected {
            let guid = parse_guid(&iface.guid)?;
            if cli.rescan {
                wlan::native::scan(&handle, &guid)?;
                std::thread::sleep(Duration::from_secs(2));
            }
            let networks = wlan::native::available_networks(&handle, &guid)?;
            if opts.json {
                println!("{}", serde_json::to_string_pretty(&networks).unwrap_or_default());
            } else {
                if selected.len() > 1 {
                    println!("== {} ==", iface.name);
                }
                display::print_networks(&networks, &opts);
            }
        }
        return Ok(());
    }

    if opts.json {
        let owned: Vec<&wlan::Interface> = selected;
        println!("{}", serde_json::to_string_pretty(&owned).unwrap_or_default());
    } else {
        let owned: Vec<wlan::Interface> = selected.into_iter().cloned().collect();
        display::print_interfaces(&owned, &opts);
    }

    Ok(())
}

#[cfg(windows)]
fn parse_guid(s: &str) -> Result<windows::core::GUID> {
    // `guid_to_string` in the native module formats via GUID's own Debug
    // impl ("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", no braces, 36 chars),
    // which is exactly the format `GUID::from(&str)` parses back. Keeping
    // the round-trip on the library's own (de)serialization avoids a
    // second, hand-rolled hex parser that could drift from it.
    if s.len() != 36 {
        return Err(IwconfError::Other(format!("malformed interface GUID: {s}")));
    }
    Ok(windows::core::GUID::from(s))
}

fn success_line(action: &str, iface_name: &str, opts: &DisplayOpts) -> String {
    let check = if opts.emoji { "" } else { "" };
    format!("{check}{iface_name}: {action}")
}