nm-wifi 0.3.0

A Terminal User Interface for managing Wi-Fi connections on Linux
Documentation
use std::{
    error::Error,
    future::Future,
    io,
    pin::Pin,
    sync::mpsc::{self, Receiver, TryRecvError},
};

use crate::{
    app::runtime::{
        RuntimeBackendDriver,
        RuntimeEvent,
        RuntimeRequest,
        ScanSnapshot,
    },
    network::ConnectionRequest,
    wifi::WifiNetwork,
};

pub type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;

pub trait NetworkBackend {
    fn connected_ssid(&self) -> Result<Option<String>, Box<dyn Error>>;
    fn adapter_name(&self) -> Result<Option<String>, Box<dyn Error>>;
    fn scan_networks(
        &self,
    ) -> BackendFuture<'_, Result<Vec<WifiNetwork>, Box<dyn Error>>>;
    fn connect(
        &self,
        request: ConnectionRequest<'_>,
    ) -> Result<(), Box<dyn Error>>;
    fn disconnect(&self, network: &WifiNetwork) -> Result<(), Box<dyn Error>>;
}

fn runtime_channel_closed_error() -> Box<dyn Error> {
    io::Error::other("runtime backend event channel closed").into()
}

fn poll_pending_event(
    pending_event: &mut Option<Receiver<RuntimeEvent>>,
) -> Result<Option<RuntimeEvent>, Box<dyn Error>> {
    match pending_event.as_mut() {
        Some(receiver) => match receiver.try_recv() {
            Ok(event) => {
                *pending_event = None;
                Ok(Some(event))
            }
            Err(TryRecvError::Empty) => Ok(None),
            Err(TryRecvError::Disconnected) => {
                *pending_event = None;
                Err(runtime_channel_closed_error())
            }
        },
        None => Ok(None),
    }
}

#[cfg(feature = "demo")]
#[derive(Debug, Default, Clone, Copy)]
pub struct DemoNetworkBackend;

#[cfg(feature = "demo")]
impl NetworkBackend for DemoNetworkBackend {
    fn connected_ssid(&self) -> Result<Option<String>, Box<dyn Error>> {
        crate::network::demo::get_connected_ssid()
    }

    fn adapter_name(&self) -> Result<Option<String>, Box<dyn Error>> {
        crate::network::demo::get_wifi_adapter_name()
    }

    fn scan_networks(
        &self,
    ) -> BackendFuture<'_, Result<Vec<WifiNetwork>, Box<dyn Error>>> {
        Box::pin(crate::network::demo::scan_wifi_networks())
    }

    fn connect(
        &self,
        request: ConnectionRequest<'_>,
    ) -> Result<(), Box<dyn Error>> {
        crate::network::demo::connect_to_network(request)
    }

    fn disconnect(&self, network: &WifiNetwork) -> Result<(), Box<dyn Error>> {
        crate::network::demo::disconnect_from_network(network)
    }
}

#[cfg(feature = "demo")]
#[derive(Default)]
struct DemoRuntimeDriver {
    pending_event: Option<Receiver<RuntimeEvent>>,
}

#[cfg(feature = "demo")]
impl RuntimeBackendDriver for DemoRuntimeDriver {
    fn begin(&mut self, request: RuntimeRequest) {
        let (sender, receiver) = mpsc::channel();
        let event = match request {
            RuntimeRequest::Scan => RuntimeEvent::Scan(Ok(ScanSnapshot {
                networks: crate::network::demo::demo_networks(),
                adapter_name: crate::network::demo::get_wifi_adapter_name()
                    .ok()
                    .flatten(),
            })),
            RuntimeRequest::Connect {
                network,
                passphrase,
            } => {
                let result = match passphrase.as_deref() {
                    Some(passphrase) => {
                        crate::network::demo::connect_to_network(
                            ConnectionRequest::Secured {
                                network: &network,
                                passphrase,
                            },
                        )
                    }
                    None => crate::network::demo::connect_to_network(
                        ConnectionRequest::Open { network: &network },
                    ),
                };
                RuntimeEvent::Connect(result.map_err(|error| error.to_string()))
            }
            RuntimeRequest::Disconnect { network } => RuntimeEvent::Disconnect(
                crate::network::demo::disconnect_from_network(&network)
                    .map_err(|error| error.to_string()),
            ),
        };
        let _ = sender.send(event);
        self.pending_event = Some(receiver);
    }

    fn poll_event(&mut self) -> Result<Option<RuntimeEvent>, Box<dyn Error>> {
        poll_pending_event(&mut self.pending_event)
    }
}

#[cfg(feature = "demo")]
pub(crate) fn default_runtime_driver() -> Box<dyn RuntimeBackendDriver> {
    Box::new(DemoRuntimeDriver::default())
}

#[cfg(not(feature = "demo"))]
#[derive(Debug, Default, Clone, Copy)]
pub struct NetworkManagerBackend;

#[cfg(not(feature = "demo"))]
impl NetworkBackend for NetworkManagerBackend {
    fn connected_ssid(&self) -> Result<Option<String>, Box<dyn Error>> {
        crate::network::networkmanager::get_connected_ssid()
    }

    fn adapter_name(&self) -> Result<Option<String>, Box<dyn Error>> {
        crate::network::networkmanager::get_wifi_adapter_name()
    }

    fn scan_networks(
        &self,
    ) -> BackendFuture<'_, Result<Vec<WifiNetwork>, Box<dyn Error>>> {
        Box::pin(crate::network::networkmanager::scan_wifi_networks())
    }

    fn connect(
        &self,
        request: ConnectionRequest<'_>,
    ) -> Result<(), Box<dyn Error>> {
        crate::network::networkmanager::connect_to_network(request)
    }

    fn disconnect(&self, network: &WifiNetwork) -> Result<(), Box<dyn Error>> {
        crate::network::networkmanager::disconnect_from_network(network)
    }
}

#[cfg(not(feature = "demo"))]
#[derive(Default)]
struct NetworkManagerRuntimeDriver {
    pending_event: Option<Receiver<RuntimeEvent>>,
}

#[cfg(not(feature = "demo"))]
impl RuntimeBackendDriver for NetworkManagerRuntimeDriver {
    fn begin(&mut self, request: RuntimeRequest) {
        let (sender, receiver) = mpsc::channel();

        match request {
            RuntimeRequest::Scan => {
                tokio::spawn(async move {
                    let event = match tokio::task::spawn_blocking(|| {
                        let networks = crate::network::networkmanager::scan_wifi_networks_blocking();
                        let adapter_name = crate::network::networkmanager::get_wifi_adapter_name()
                            .ok()
                            .flatten();

                        match networks {
                            Ok(networks) => RuntimeEvent::Scan(Ok(ScanSnapshot {
                                networks,
                                adapter_name,
                            })),
                            Err(error) => RuntimeEvent::Scan(Err(error.to_string())),
                        }
                    })
                    .await
                    {
                        Ok(event) => event,
                        Err(error) => RuntimeEvent::Scan(Err(format!(
                            "runtime scan task failed: {error}"
                        ))),
                    };

                    let _ = sender.send(event);
                });
            }
            RuntimeRequest::Connect {
                network,
                passphrase,
            } => {
                tokio::spawn(async move {
                    let event = match tokio::task::spawn_blocking(move || {
                        let result = match passphrase.as_deref() {
                            Some(passphrase) => crate::network::networkmanager::connect_to_network(
                                ConnectionRequest::Secured {
                                    network: &network,
                                    passphrase,
                                },
                            ),
                            None => crate::network::networkmanager::connect_to_network(
                                ConnectionRequest::Open { network: &network },
                            ),
                        };

                        RuntimeEvent::Connect(result.map_err(|error| error.to_string()))
                    })
                    .await
                    {
                        Ok(event) => event,
                        Err(error) => RuntimeEvent::Connect(Err(format!(
                            "runtime connect task failed: {error}"
                        ))),
                    };

                    let _ = sender.send(event);
                });
            }
            RuntimeRequest::Disconnect { network } => {
                tokio::spawn(async move {
                    let event = match tokio::task::spawn_blocking(move || {
                        RuntimeEvent::Disconnect(
                            crate::network::networkmanager::disconnect_from_network(&network)
                                .map_err(|error| error.to_string()),
                        )
                    })
                    .await
                    {
                        Ok(event) => event,
                        Err(error) => RuntimeEvent::Disconnect(Err(format!(
                            "runtime disconnect task failed: {error}"
                        ))),
                    };

                    let _ = sender.send(event);
                });
            }
        }

        self.pending_event = Some(receiver);
    }

    fn poll_event(&mut self) -> Result<Option<RuntimeEvent>, Box<dyn Error>> {
        poll_pending_event(&mut self.pending_event)
    }
}

#[cfg(not(feature = "demo"))]
pub(crate) fn default_runtime_driver() -> Box<dyn RuntimeBackendDriver> {
    Box::new(NetworkManagerRuntimeDriver::default())
}

#[cfg(feature = "demo")]
pub fn default_backend() -> Box<dyn NetworkBackend> {
    Box::new(DemoNetworkBackend)
}

#[cfg(not(feature = "demo"))]
pub fn default_backend() -> Box<dyn NetworkBackend> {
    Box::new(NetworkManagerBackend)
}