gbin 0.4.2

A suite of Linux utilities and programs.
use anyhow::{Result, bail};
use tokio::{process::Command, task::JoinHandle};

fn parse_scan_line(line: &str) -> Result<WifiNetwork> {
    let fields = line
        .replace(r"\:", ";")
        .replace(":", "|")
        .replace(";", ":")
        .split("|")
        .map(|s| s.to_string())
        .collect::<Vec<_>>();
    Ok(WifiNetwork {
        in_use: fields[0] == "*",
        bssid: fields[1].clone(),
        ssid: fields[2].clone(),
        mode: fields[3].clone(),
        band: fields[4].clone(),
        channel: fields[5].parse()?,
        rate: fields[6].clone(),
        signal: fields[7].parse()?,
        security: fields[8].clone(),
    })
}

#[derive(Debug)]
pub struct WifiNetwork {
    pub in_use: bool,
    pub bssid: String,
    pub ssid: String,
    pub mode: String,
    pub band: String,
    pub channel: usize,
    pub rate: String,
    pub signal: usize,
    pub security: String,
}

#[derive(Debug)]
pub struct WifiDevice {
    pub name: String,
    pub device_type: String,
    pub status: String,
    pub wifi_networks: Vec<WifiNetwork>,
    scan_pending: bool,
    pub scan_task: Option<JoinHandle<Result<Vec<WifiNetwork>>>>,
    pub connect_task: Option<JoinHandle<Result<()>>>,
    pub connect_ssid: Option<String>,
}
impl WifiDevice {
    pub fn new(name: String, device_type: String, status: String) -> Self {
        Self {
            name,
            device_type,
            status,
            wifi_networks: vec![],
            scan_pending: true,
            scan_task: None,
            connect_task: None,
            connect_ssid: None,
        }
    }
    pub fn scan(&mut self) {
        let name = self.name.clone();
        self.scan_task = Some(tokio::spawn(async move {
            let output = Command::new("nmcli")
                .args([
                    "-t",
                    "-f",
                    "IN-USE,BSSID,SSID,MODE,BAND,CHAN,RATE,SIGNAL,SECURITY",
                    "device",
                    "wifi",
                    "list",
                    "ifname",
                    &name,
                    "--rescan",
                    "yes",
                ])
                .output()
                .await?;
            let raw_output = String::from_utf8(output.stdout)?;
            raw_output
                .lines()
                .filter(|line| !line.is_empty())
                .map(parse_scan_line)
                .collect()
        }));
    }
    pub fn connect(&mut self, ssid: String, password: Option<String>) {
        if self.connect_task.is_some() {
            return;
        }
        let name = self.name.clone();
        self.connect_ssid = Some(ssid.clone());
        self.connect_task = Some(tokio::spawn(async move {
            let mut command = Command::new("nmcli");
            command.args(["device", "wifi", "connect", &ssid, "ifname", &name]);
            if let Some(password) = password {
                command.args(["password", &password]);
            }
            let output = command.output().await?;
            if !output.status.success() {
                bail!(String::from_utf8_lossy(&output.stderr).trim().to_string());
            }
            Ok(())
        }));
    }
    pub fn disconnect(&mut self) {
        if self.connect_task.is_some() {
            return;
        }
        let name = self.name.clone();
        self.connect_task = Some(tokio::spawn(async move {
            let output = Command::new("nmcli")
                .args(["device", "disconnect", &name])
                .output()
                .await?;
            if !output.status.success() {
                bail!(String::from_utf8_lossy(&output.stderr).trim().to_string());
            }
            Ok(())
        }));
    }
    pub async fn run(&mut self) -> Result<()> {
        if let Some(scan_task) = self.scan_task.as_ref() {
            if scan_task.is_finished() {
                let task = self.scan_task.take().unwrap();
                self.wifi_networks = task.await??;
            }
        }
        if let Some(connect_task) = self.connect_task.as_ref() {
            if connect_task.is_finished() {
                let task = self.connect_task.take().unwrap();
                let _ = task.await?;
                self.connect_ssid = None;
                self.scan_pending = true;
            }
        }
        Ok(())
    }
}

pub type WifiDevices = Vec<WifiDevice>;

pub struct WifiManager {
    pub wifi_devices: WifiDevices,
    pub find_devices_task: Option<JoinHandle<Result<WifiDevices>>>,
    pub scanning_device: Option<String>,
}
impl WifiManager {
    pub fn new() -> Self {
        Self {
            wifi_devices: vec![],
            find_devices_task: None,
            scanning_device: None,
        }
    }
    pub fn find_devices(&mut self) {
        self.find_devices_task = Some(tokio::spawn(async {
            let output = Command::new("nmcli")
                .args(vec!["-t", "-f", "DEVICE,TYPE,STATE", "device"])
                .output()
                .await?;
            let raw_output = String::from_utf8(output.stdout)?
                .replace(r"\:", ";")
                .replace(":", "|")
                .replace(";", ":");
            let lines = raw_output.split("\n");
            let mut devices = vec![];
            for line in lines {
                let info = line.split("|").collect::<Vec<_>>();
                if info.len() < 3 {
                    continue;
                }
                if info[1] != "wifi" {
                    continue;
                }
                let name = info[0];
                let device_type = info[1];
                let status = info[2];
                devices.push(WifiDevice::new(
                    name.to_string(),
                    device_type.to_string(),
                    status.to_string(),
                ));
            }
            Ok(devices)
        }));
    }
    pub fn scan_all_devices(&mut self) {
        for device in &mut self.wifi_devices {
            device.scan_pending = true;
        }
    }
    pub async fn run(&mut self) -> Result<()> {
        if let Some(find_devices_task) = self.find_devices_task.as_ref() {
            if find_devices_task.is_finished() {
                let task = self.find_devices_task.take().unwrap();
                self.wifi_devices = task.await??;
            }
        }
        for device in &mut self.wifi_devices {
            device.run().await?;
        }
        if let Some(name) = self.scanning_device.as_ref() {
            let still_scanning = self
                .wifi_devices
                .iter()
                .any(|d| &d.name == name && d.scan_task.is_some());
            if !still_scanning {
                self.scanning_device = None;
            }
        }
        if self.scanning_device.is_none() && self.find_devices_task.is_none() {
            if let Some(device) = self.wifi_devices.iter_mut().find(|d| d.scan_pending) {
                device.scan_pending = false;
                device.scan();
                self.scanning_device = Some(device.name.clone());
            }
        }
        Ok(())
    }
}