iwconf-win 0.1.2

A colorful, iwconfig-style WLAN CLI for Windows, backed by the native WLAN API (no netsh required).
//! All the "make it look like `netsh wlan show interface` but better"
//! rendering lives here. Every color call goes through [`paint`] so
//! `--no-color`/`--plain`/`NO_COLOR` all degrade to identical plain text.

use make_colors::make_colors_hex;

use crate::wlan::{ConnectionInfo, Interface, InterfaceState, NetworkInfo};

#[derive(Debug, Clone, Copy)]
pub struct DisplayOpts {
    pub color: bool,
    pub emoji: bool,
    pub json: bool,
}

impl DisplayOpts {
    pub fn from_cli(no_color: bool, no_emoji: bool, plain: bool, json: bool) -> Self {
        let no_color_env = std::env::var_os("NO_COLOR").is_some();
        Self {
            color: !(no_color || plain || no_color_env),
            emoji: !(no_emoji || plain),
            json,
        }
    }
}

/// Hex palette, named so intent stays legible in call sites instead of
/// scattering `#RRGGBB` literals through the render functions.
mod palette {
    pub const CYAN: &str = "#00FFFF";
    pub const GREEN: &str = "#33CC66";
    pub const YELLOW: &str = "#FFD400";
    pub const RED: &str = "#FF5555";
    pub const MAGENTA: &str = "#CC66FF";
    pub const GRAY: &str = "#888888";
    pub const WHITE: &str = "#EEEEEE";
    pub const BLUE: &str = "#4FA8FF";
}

fn paint(text: &str, hex: &str, opts: &DisplayOpts) -> String {
    if !opts.color {
        return text.to_string();
    }
    make_colors_hex(text, hex, None).unwrap_or_else(|_| text.to_string())
}

fn signal_color(quality: u8) -> &'static str {
    if quality >= 70 {
        palette::GREEN
    } else if quality >= 40 {
        palette::YELLOW
    } else {
        palette::RED
    }
}

/// Renders an 8-cell braille-block-style bar, tallest bar first, plus the
/// numeric percentage — the "mini chart" iwconfig doesn't have but
/// `iwconf` does.
fn signal_bar(quality: u8, opts: &DisplayOpts) -> String {
    const CELLS: usize = 10;
    let filled = ((quality as usize * CELLS) / 100).min(CELLS);
    let bar: String = "".repeat(filled) + &"".repeat(CELLS - filled);
    let colored = paint(&bar, signal_color(quality), opts);
    format!("{colored} {quality:>3}%")
}

fn state_glyph(state: InterfaceState, opts: &DisplayOpts) -> String {
    if !opts.emoji {
        return format!("[{state}]");
    }
    let emoji = match state {
        InterfaceState::Connected => "🟢",
        InterfaceState::Disconnected => "",
        InterfaceState::Associating | InterfaceState::Authenticating => "🟡",
        InterfaceState::Discovering => "🔎",
        InterfaceState::Disconnecting => "🟠",
        InterfaceState::AdHocNetworkFormed => "🔗",
        InterfaceState::NotReady => "🔴",
        InterfaceState::Unknown => "",
    };
    format!("{emoji} {state}")
}

fn lock_glyph(secured: bool, opts: &DisplayOpts) -> &'static str {
    if !opts.emoji {
        return "";
    }
    if secured {
        "🔒 "
    } else {
        "🔓 "
    }
}

pub fn print_interfaces(interfaces: &[Interface], opts: &DisplayOpts) {
    for (i, iface) in interfaces.iter().enumerate() {
        if i > 0 {
            println!();
        }
        print_interface(iface, opts);
    }
}

pub fn print_interface(iface: &Interface, opts: &DisplayOpts) {
    let label = if opts.emoji {
        format!("📶 {}", iface.name)
    } else {
        iface.name.clone()
    };
    println!("{}", paint(&label, palette::CYAN, opts));
    println!(
        "  {}  {}",
        paint("Description", palette::GRAY, opts),
        iface.description
    );
    println!(
        "  {}  {}",
        paint("State", palette::GRAY, opts),
        state_glyph(iface.state, opts)
    );

    match &iface.connection {
        Some(conn) => print_connection(conn, opts),
        None => {
            let msg = if opts.emoji { "💤 not connected" } else { "not connected" };
            println!("  {}  {}", paint("Connection", palette::GRAY, opts), msg);
        }
    }
}

fn print_connection(conn: &ConnectionInfo, opts: &DisplayOpts) {
    let secured = !conn.authentication.eq_ignore_ascii_case("open");
    println!(
        "  {}  {}{}",
        paint("SSID", palette::GRAY, opts),
        lock_glyph(secured, opts),
        paint(&conn.ssid, palette::WHITE, opts)
    );
    println!("  {}  {}", paint("BSSID", palette::GRAY, opts), conn.bssid);
    println!(
        "  {}  {}",
        paint("Network type", palette::GRAY, opts),
        conn.network_type
    );
    println!(
        "  {}  {}",
        paint("Radio type", palette::GRAY, opts),
        paint(&conn.radio_type, palette::BLUE, opts)
    );
    println!(
        "  {}  {}",
        paint("Authentication", palette::GRAY, opts),
        paint(&conn.authentication, palette::MAGENTA, opts)
    );
    println!("  {}  {}", paint("Cipher", palette::GRAY, opts), conn.cipher);
    println!(
        "  {}  {}",
        paint("Connection mode", palette::GRAY, opts),
        conn.connection_mode
    );
    if let (Some(ch), Some(band)) = (conn.channel, conn.band_ghz) {
        println!(
            "  {}  {} ({band:.1} GHz)",
            paint("Channel", palette::GRAY, opts),
            ch
        );
    }
    if let (Some(rx), Some(tx)) = (conn.rx_rate_mbps, conn.tx_rate_mbps) {
        println!(
            "  {}  rx {rx} Mbps / tx {tx} Mbps",
            paint("Link rate", palette::GRAY, opts)
        );
    }
    if let Some(q) = conn.signal_quality {
        println!(
            "  {}  {}",
            paint("Signal", palette::GRAY, opts),
            signal_bar(q, opts)
        );
    }
    if let Some(profile) = &conn.profile_name {
        println!("  {}  {}", paint("Profile", palette::GRAY, opts), profile);
    }
}

pub fn print_networks(networks: &[NetworkInfo], opts: &DisplayOpts) {
    if networks.is_empty() {
        println!("{}", paint("No networks visible.", palette::GRAY, opts));
        return;
    }
    let mut sorted: Vec<&NetworkInfo> = networks.iter().collect();
    sorted.sort_by_key(|net| std::cmp::Reverse(net.signal_quality));

    for net in sorted {
        let secured = !net.authentication.eq_ignore_ascii_case("open");
        let marker = if net.already_connected {
            if opts.emoji { "➡️ " } else { "* " }
        } else {
            "  "
        };
        println!(
            "{marker}{}{}",
            lock_glyph(secured, opts),
            paint(&net.ssid, palette::WHITE, opts)
        );
        println!(
            "     {}  {}",
            paint("Signal", palette::GRAY, opts),
            signal_bar(net.signal_quality, opts)
        );
        println!(
            "     {}  {} / {}",
            paint("Security", palette::GRAY, opts),
            paint(&net.authentication, palette::MAGENTA, opts),
            net.encryption
        );
        if !net.connectable {
            println!(
                "     {}  not connectable right now",
                paint("Note", palette::YELLOW, opts)
            );
        }
        if let Some(profile) = &net.profile_name {
            println!(
                "     {}  {profile}",
                paint("Saved profile", palette::GRAY, opts)
            );
        }
        println!();
    }
}