ntw 0.2.1

A macOS network toolkit providing live speed metrics and simple interface management.
Documentation
use anyhow::{Context, Result};
use std::process::{Command, Stdio};

use inquire::MultiSelect;

use super::list;

pub async fn remove_network_or_interactive(iface: &str, ssid: Option<&str>) -> Result<()> {
    match ssid {
        Some(s) => {
            remove_network(iface, s)?;
            println!("{}", s);
            Ok(())
        }
        None => remove_networks_interactive(iface).await,
    }
}

pub async fn remove_networks_interactive(iface: &str) -> Result<()> {
    let networks = list::get_preferred_networks(iface)?;

    if networks.is_empty() {
        println!("No networks found on interface '{}'.", iface);
        return Ok(());
    }

    let chosen = MultiSelect::new("Select networks to remove", networks).prompt()?;

    if chosen.is_empty() {
        println!("No networks selected. Aborting.");
        return Ok(());
    }

    let results = remove_networks_parallel(iface, chosen).await;

    let (ok, failed): (Vec<_>, Vec<_>) = results.into_iter().partition(|(_, r)| r.is_ok());

    println!();
    println!("Removed {} network(s) from {}", ok.len(), iface);
    for (ssid, _) in ok {
        println!("{}", ssid);
    }

    if !failed.is_empty() {
        println!();
        println!("Failed to remove {} network(s):", failed.len());
        for (ssid, err) in failed {
            println!("{} ({})", ssid, err.unwrap_err());
        }
    }

    Ok(())
}

async fn remove_networks_parallel(iface: &str, ssids: Vec<String>) -> Vec<(String, Result<()>)> {
    let mut set = tokio::task::JoinSet::new();

    for ssid in ssids {
        let iface = iface.to_string();
        set.spawn_blocking(move || {
            let res = remove_network(&iface, &ssid);
            (ssid, res)
        });
    }

    let mut results = Vec::new();

    while let Some(res) = set.join_next().await {
        if let Ok(pair) = res {
            results.push(pair);
        }
    }

    results
}

pub fn remove_network(iface: &str, ssid: &str) -> Result<()> {
    let status = Command::new("networksetup")
        .args(["-removepreferredwirelessnetwork", iface, ssid])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .with_context(|| format!("failed to execute networksetup for '{}'", ssid))?;

    if !status.success() {
        anyhow::bail!("network not found or removal failed");
    }

    Ok(())
}