wifui 0.5.0

A lightweight, keyboard-driven Terminal User Interface (TUI) for managing Wi-Fi connections on Windows and Linux.
/// Typed errors for WifUI WiFi operations
use thiserror::Error;

/// Result type alias for WiFi operations
pub type WifiResult<T> = Result<T, WifiError>;

/// Errors that can occur during WiFi operations
#[allow(dead_code)]
#[derive(Error, Debug)]
pub enum WifiError {
    #[cfg(all(not(windows), not(target_os = "linux")))]
    #[error("Wi-Fi backend is not implemented for this platform")]
    UnsupportedPlatform,

    #[error("{backend} Wi-Fi backend is unavailable")]
    BackendUnavailable { backend: String },

    #[error("{backend} reported no usable Wi-Fi interface")]
    MissingInterface { backend: String },

    #[error("D-Bus operation failed: {operation}")]
    Dbus { operation: String },

    #[error("{backend} does not support {operation} yet")]
    UnsupportedOperation { backend: String, operation: String },

    #[error("Wi-Fi network was not found: {ssid}")]
    NetworkNotFound { ssid: String },

    #[cfg(windows)]
    #[error("Failed to open WLAN handle (code: {code})")]
    HandleOpenFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to enumerate interfaces (code: {code})")]
    InterfaceEnumFailed { code: u32 },

    #[cfg(windows)]
    #[error("No WiFi interface found")]
    NoInterface,

    #[cfg(windows)]
    #[error("Failed to get available networks (code: {code})")]
    NetworkListFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to register notification (code: {code})")]
    NotificationRegistrationFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to scan networks (code: {code})")]
    ScanFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to connect (code: {code})")]
    ConnectionFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to add profile (code: {code}, reason: {reason})")]
    ProfileAddFailed { code: u32, reason: u32 },

    #[cfg(windows)]
    #[error("Failed to get profile (code: {code})")]
    ProfileGetFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to set profile (code: {code}, reason: {reason})")]
    ProfileSetFailed { code: u32, reason: u32 },

    #[cfg(windows)]
    #[error("Failed to delete profile (code: {code})")]
    ProfileDeleteFailed { code: u32 },

    #[cfg(windows)]
    #[error("Failed to disconnect (code: {code})")]
    DisconnectFailed { code: u32 },

    #[cfg(windows)]
    #[error("Could not find connectionMode in profile XML")]
    ProfileXmlInvalid,

    #[error("Internal error: {0}")]
    Internal(String),
}

#[allow(dead_code)]
impl WifiError {
    pub fn dbus(operation: &str, error: &impl std::fmt::Display) -> Self {
        let err_str = error.to_string();
        let formatted = if err_str.contains("AccessDenied")
            || err_str.contains("NotAuthorized")
            || err_str.contains("InteractiveAuthorizationRequired")
            || err_str.contains("PermissionDenied")
        {
            format!(
                "{operation}: {err_str} (Polkit permission denied; verify your user network control privileges)"
            )
        } else {
            format!("{operation}: {err_str}")
        };
        Self::Dbus {
            operation: formatted,
        }
    }
}

/// Convert a WLAN reason code to a human-readable string
#[cfg(windows)]
pub fn wlan_reason_to_string(code: u32) -> String {
    match code {
        0 => "Success".to_string(),
        1 => "Unknown Failure".to_string(),
        0x00010001 => "Network Not Compatible".to_string(),
        0x00010002 => "Profile Not Compatible".to_string(),
        0x00028002 => "Association Failed".to_string(),
        0x00028003 => "Association Timeout".to_string(),
        0x00028004 => "Pre-Security Failure".to_string(),
        0x00028005 => "Start Security Failure".to_string(),
        0x00028006 => "Security Failure".to_string(),
        0x00028007 => "Security Timeout".to_string(),
        0x00028008 => "Roaming Failure".to_string(),
        0x00028009 => "Roaming Security Failure".to_string(),
        0x0002800A => "Ad-hoc Security Failure".to_string(),
        0x0002800B => "Driver Disconnected (Possible Wrong Password)".to_string(),
        0x0002800C => "Driver Operation Failure".to_string(),
        0x0002800D => "IHV Not Available".to_string(),
        0x0002800E => "IHV Not Responding".to_string(),
        // ACM reason codes
        0x00038001 => "ACM Base".to_string(),
        0x00038002 => "Connection Failed (Network Not Available or Wrong Password)".to_string(),
        0x00038003 => "Profile Not Found".to_string(),
        0x00038004 => "Profile Already Exists".to_string(),
        0x00038005 => "Profile Name Too Long".to_string(),
        0x00038006 => "Profile Invalid".to_string(),
        0x00038014 => "Connection Failed (Profile Issue)".to_string(),
        0x00050004 => "Incorrect Password".to_string(),
        0x00048005 => "Incorrect Password (Key Exchange Timeout)".to_string(),
        0x00048014 => "Authentication Timeout (Possible Wrong Password)".to_string(),
        0x00080006 => "MSM Security Missing".to_string(),
        _ => format!("Unknown Error (Code: {code}, 0x{code:X})"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn formats_polkit_permission_denied_errors() {
        let err = WifiError::dbus(
            "activate connection",
            &"org.freedesktop.DBus.Error.AccessDenied: Rejected",
        );
        assert!(err.to_string().contains("Polkit permission denied"));
    }
}