From 0acda91044f2ec873cafd50902b3a3fd81b61c9a Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Thu, 11 Dec 2025 23:10:27 -0500 Subject: [PATCH 1/5] chore: crates.io docs --- nmrs/Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nmrs/Cargo.toml b/nmrs/Cargo.toml index d473d12b..c3950a15 100644 --- a/nmrs/Cargo.toml +++ b/nmrs/Cargo.toml @@ -2,6 +2,12 @@ name = "nmrs" version = "0.4.0" edition = "2024" +description = "A Rust library for NetworkManager over D-Bus" +license = "MIT" +repository = "https://github.com/cachebag/nmrs" +documentation = "https://docs.rs/nmrs" +keywords = ["networkmanager", "dbus", "wifi", "linux", "networking"] +categories = ["api-bindings", "asynchronous"] [dependencies] zbus = "5.11.0" From 5f1b8eada01917591659ecdc4bd57c87d3ff1c0c Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Fri, 12 Dec 2025 12:19:37 -0500 Subject: [PATCH 2/5] feat(#87): replaced debug printing with `log` crate --- nmrs-gui/Cargo.toml | 1 + nmrs-gui/src/ui/connect.rs | 9 ++-- nmrs/Cargo.toml | 1 + nmrs/src/connection.rs | 89 +++++++++++++++++---------------- nmrs/src/connection_settings.rs | 3 +- nmrs/src/lib.rs | 14 ++++++ nmrs/src/utils.rs | 7 +-- 7 files changed, 72 insertions(+), 52 deletions(-) diff --git a/nmrs-gui/Cargo.toml b/nmrs-gui/Cargo.toml index 4b3cfd8f..5b59fe35 100644 --- a/nmrs-gui/Cargo.toml +++ b/nmrs-gui/Cargo.toml @@ -4,6 +4,7 @@ version = "0.4.0" edition = "2024" [dependencies] +log = "0.4.29" nmrs = { path = "../nmrs" } tokio = { version = "1.47.1", features = ["full"] } gtk = { version = "0.10.1", package = "gtk4" } diff --git a/nmrs-gui/src/ui/connect.rs b/nmrs-gui/src/ui/connect.rs index 88fd633e..6a682568 100644 --- a/nmrs-gui/src/ui/connect.rs +++ b/nmrs-gui/src/ui/connect.rs @@ -3,6 +3,7 @@ use gtk::{ ApplicationWindow, Box as GtkBox, Dialog, Entry, EventControllerKey, Label, Orientation, prelude::*, }; +use log::{debug, error}; use nmrs::{ NetworkManager, models::{EapMethod, EapOptions, Phase2, WifiSecurity}, @@ -23,7 +24,7 @@ pub fn connect_modal( if let Some(current) = nm.current_ssid().await && current == ssid_owned { - println!("Already connected to {current}, skipping modal"); + debug!("Already connected to {current}, skipping modal"); return; } @@ -128,17 +129,17 @@ fn draw_connect_modal( WifiSecurity::WpaPsk { psk: pwd } }; - println!("Calling nm.connect() for '{ssid}'"); + debug!("Calling nm.connect() for '{ssid}'"); match nm.connect(&ssid, creds).await { Ok(_) => { - println!("nm.connect() succeeded!"); + debug!("nm.connect() succeeded!"); status.set_text("✓ Connected!"); on_success(); glib::timeout_future_seconds(1).await; dialog.close(); } Err(err) => { - eprintln!("nm.connect() failed: {err}"); + error!("nm.connect() failed: {err}"); let err_str = err.to_string().to_lowercase(); if err_str.contains("authentication") || err_str.contains("supplicant") diff --git a/nmrs/Cargo.toml b/nmrs/Cargo.toml index c3950a15..5c5c5fb9 100644 --- a/nmrs/Cargo.toml +++ b/nmrs/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["networkmanager", "dbus", "wifi", "linux", "networking"] categories = ["api-bindings", "asynchronous"] [dependencies] +log = "0.4.29" zbus = "5.11.0" zvariant = "5.7.0" serde = { version = "1", features = ["derive"] } diff --git a/nmrs/src/connection.rs b/nmrs/src/connection.rs index 33272ad0..4c6877a2 100644 --- a/nmrs/src/connection.rs +++ b/nmrs/src/connection.rs @@ -1,4 +1,5 @@ use futures_timer::Delay; +use log::{debug, error, info, warn}; use std::collections::HashMap; use zbus::Connection; use zvariant::OwnedObjectPath; @@ -33,7 +34,7 @@ enum SavedDecision { /// If a saved connection exists but fails, it will be deleted and a fresh /// connection will be attempted with the provided credentials. pub(crate) async fn connect(conn: &Connection, ssid: &str, creds: WifiSecurity) -> Result<()> { - println!( + debug!( "Connecting to '{}' | secured={} is_psk={} is_eap={}", ssid, creds.secured(), @@ -47,7 +48,7 @@ pub(crate) async fn connect(conn: &Connection, ssid: &str, creds: WifiSecurity) let decision = decide_saved_connection(saved_raw, &creds)?; let wifi_device = find_wifi_device(conn, &nm).await?; - eprintln!("Found WiFi device: {}", wifi_device.as_str()); + debug!("Found WiFi device: {}", wifi_device.as_str()); let wifi = NMWirelessProxy::builder(conn) .path(wifi_device.clone())? @@ -55,13 +56,13 @@ pub(crate) async fn connect(conn: &Connection, ssid: &str, creds: WifiSecurity) .await?; if let Some(active) = current_ssid(conn).await { - eprintln!("Currently connected to: {active}"); + debug!("Currently connected to: {active}"); if active == ssid { - eprintln!("Already connected to {active}, skipping connect()"); + debug!("Already connected to {active}, skipping connect()"); return Ok(()); } } else { - eprintln!("Not currently connected to any network"); + debug!("Not currently connected to any network"); } let specific_object = scan_and_resolve_ap(conn, &wifi, ssid).await?; @@ -79,10 +80,10 @@ pub(crate) async fn connect(conn: &Connection, ssid: &str, creds: WifiSecurity) .path(wifi_device.clone())? .build() .await?; - eprintln!("Waiting for connection to complete..."); + debug!("Waiting for connection to complete..."); wait_for_connection_state(&dev_proxy).await?; - eprintln!("---Connection request for '{ssid}' submitted successfully---"); + info!("Connection request for '{ssid}' submitted successfully"); Ok(()) } @@ -98,7 +99,7 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { use std::collections::HashMap; use zvariant::{OwnedObjectPath, Value}; - eprintln!("Starting forget operation for: {ssid}"); + debug!("Starting forget operation for: {ssid}"); let nm = NMProxy::new(conn).await?; @@ -126,7 +127,7 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { if let Ok(bytes) = ap.ssid().await && decode_ssid_or_empty(&bytes) == ssid { - eprintln!("Disconnecting from active network: {ssid}"); + debug!("Disconnecting from active network: {ssid}"); let dev_proxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(conn) .destination("org.freedesktop.NetworkManager")? .path(dev_path.clone())? @@ -135,35 +136,35 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { .await?; match dev_proxy.call_method("Disconnect", &()).await { - Ok(_) => eprintln!("Disconnect call succeeded"), - Err(e) => eprintln!("Disconnect call failed: {e}"), + Ok(_) => debug!("Disconnect call succeeded"), + Err(e) => warn!("Disconnect call failed: {e}"), } - eprintln!("About to enter wait loop..."); + debug!("About to enter wait loop..."); for i in 0..retries::FORGET_MAX_RETRIES { Delay::new(timeouts::forget_poll_interval()).await; match dev.state().await { Ok(current_state) => { - eprintln!("Wait loop {i}: device state = {current_state}"); + debug!("Wait loop {i}: device state = {current_state}"); if current_state == device_state::DISCONNECTED || current_state == device_state::UNAVAILABLE { - eprintln!("Device reached disconnected state"); + debug!("Device reached disconnected state"); break; } } Err(e) => { - eprintln!("Failed to get device state in wait loop {i}: {e}"); + warn!("Failed to get device state in wait loop {i}: {e}"); break; } } } - eprintln!("Wait loop completed"); + debug!("Wait loop completed"); } } } - eprintln!("Starting connection deletion phase..."); + debug!("Starting connection deletion phase..."); let settings: zbus::Proxy<'_> = zbus::proxy::Builder::new(conn) .destination("org.freedesktop.NetworkManager")? @@ -196,7 +197,7 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { && id.as_str() == ssid { should_delete = true; - eprintln!("Found connection by ID: {id}"); + debug!("Found connection by ID: {id}"); } if let Some(wifi_sec) = settings_map.get("802-11-wireless") @@ -210,7 +211,7 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { } if decode_ssid_or_empty(&raw) == ssid { should_delete = true; - eprintln!("Found connection by SSID match"); + debug!("Found connection by SSID match"); } } @@ -219,7 +220,7 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { let empty_psk = matches!(wsec.get("psk"), Some(Value::Str(s)) if s.is_empty()); if (missing_psk || empty_psk) && should_delete { - eprintln!("Connection has missing/empty PSK, will delete"); + debug!("Connection has missing/empty PSK, will delete"); } } @@ -227,10 +228,10 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { match cproxy.call_method("Delete", &()).await { Ok(_) => { deleted_count += 1; - eprintln!("Deleted connection: {}", cpath.as_str()); + debug!("Deleted connection: {}", cpath.as_str()); } Err(e) => { - eprintln!("Failed to delete connection {}: {}", cpath.as_str(), e); + warn!("Failed to delete connection {}: {}", cpath.as_str(), e); } } } @@ -238,10 +239,10 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> Result<()> { } if deleted_count > 0 { - eprintln!("Successfully deleted {deleted_count} connection(s) for '{ssid}'"); + info!("Successfully deleted {deleted_count} connection(s) for '{ssid}'"); Ok(()) } else { - eprintln!("No saved connections found for '{ssid}'"); + debug!("No saved connections found for '{ssid}'"); Err(ConnectionError::NoSavedConnection) } } @@ -346,7 +347,7 @@ async fn ensure_disconnected( wifi_device: &OwnedObjectPath, ) -> Result<()> { if let Some(active) = current_ssid(conn).await { - eprintln!("Disconnecting from {active}"); + debug!("Disconnecting from {active}"); if let Ok(conns) = nm.active_connections().await { for conn_path in conns { @@ -376,14 +377,14 @@ async fn connect_via_saved( creds: &WifiSecurity, saved: OwnedObjectPath, ) -> Result<()> { - eprintln!("Activating saved connection: {}", saved.as_str()); + debug!("Activating saved connection: {}", saved.as_str()); match nm .activate_connection(saved.clone(), wifi_device.clone(), ap.clone()) .await { Ok(active_conn) => { - eprintln!( + debug!( "activate_connection() succeeded, active connection: {}", active_conn.as_str() ); @@ -398,8 +399,8 @@ async fn connect_via_saved( let check_state = dev_check.state().await?; if check_state == device_state::DISCONNECTED { - eprintln!("Connection activated but device stuck in Disconnected state"); - eprintln!("Saved connection has invalid settings, deleting and retrying"); + warn!("Connection activated but device stuck in Disconnected state"); + warn!("Saved connection has invalid settings, deleting and retrying"); let _ = nm.deactivate_connection(active_conn).await; @@ -413,19 +414,19 @@ async fn connect_via_saved( let settings = build_wifi_connection(ap.as_str(), creds, &opts); - eprintln!("Creating fresh connection with corrected settings"); + debug!("Creating fresh connection with corrected settings"); nm.add_and_activate_connection(settings, wifi_device.clone(), ap.clone()) .await .map_err(|e| { - eprintln!("Fresh connection also failed. SOL: {e}"); + error!("Fresh connection also failed: {e}"); e })?; } } Err(e) => { - eprintln!("activate_connection() failed: {e}"); - eprintln!( + warn!("activate_connection() failed: {e}"); + warn!( "Saved connection may be corrupted, deleting and retrying with fresh connection" ); @@ -442,7 +443,7 @@ async fn connect_via_saved( nm.add_and_activate_connection(settings, wifi_device.clone(), ap.clone()) .await .map_err(|e| { - eprintln!("Fresh connection also failed. SOL: {e}"); + error!("Fresh connection also failed: {e}"); e })?; } @@ -472,7 +473,7 @@ async fn build_and_activate_new( let settings = build_wifi_connection(ssid, &creds, &opts); - eprintln!("Creating new connetion, settings: \n{settings:#?}"); + debug!("Creating new connection, settings: \n{settings:#?}"); ensure_disconnected(conn, nm, wifi_device).await?; @@ -480,9 +481,9 @@ async fn build_and_activate_new( .add_and_activate_connection(settings, wifi_device.clone(), ap.clone()) .await { - Ok(_) => eprintln!("add_and_activate_connection() succeeded"), + Ok(_) => debug!("add_and_activate_connection() succeeded"), Err(e) => { - eprintln!("add_and_activate_connection() failed: {e}"); + error!("add_and_activate_connection() failed: {e}"); return Err(e.into()); } } @@ -495,11 +496,11 @@ async fn build_and_activate_new( .await?; let initial_state = dev_proxy.state().await?; - eprintln!("Dev state after build_and_activate_new(): {initial_state:?}"); - eprintln!("Waiting for connection to complete..."); + debug!("Dev state after build_and_activate_new(): {initial_state:?}"); + debug!("Waiting for connection to complete..."); wait_for_connection_state(&dev_proxy).await?; - eprintln!("---Connection request for '{ssid}' submitted successfully---"); + info!("Connection request for '{ssid}' submitted successfully"); Ok(()) } @@ -514,15 +515,15 @@ async fn scan_and_resolve_ap( ssid: &str, ) -> Result { match wifi.request_scan(HashMap::new()).await { - Ok(_) => eprintln!("Scan requested successfully"), - Err(e) => eprintln!("Scan request failed: {e}"), + Ok(_) => debug!("Scan requested successfully"), + Err(e) => warn!("Scan request failed: {e}"), } Delay::new(timeouts::scan_wait()).await; - eprintln!("Scan wait complete"); + debug!("Scan wait complete"); let ap = find_ap(conn, wifi, ssid).await?; - eprintln!("Matched target SSID '{ssid}'"); + debug!("Matched target SSID '{ssid}'"); Ok(ap) } diff --git a/nmrs/src/connection_settings.rs b/nmrs/src/connection_settings.rs index a16b926f..33922e93 100644 --- a/nmrs/src/connection_settings.rs +++ b/nmrs/src/connection_settings.rs @@ -4,6 +4,7 @@ //! connection profiles. Saved connections persist across reboots and //! store credentials for automatic reconnection. +use log::debug; use std::collections::HashMap; use zbus::Connection; use zvariant::{OwnedObjectPath, Value}; @@ -76,6 +77,6 @@ pub(crate) async fn delete_connection(conn: &Connection, conn_path: OwnedObjectP .await?; cproxy.call_method("Delete", &()).await?; - eprintln!("Deleted connection: {}", conn_path.as_str()); + debug!("Deleted connection: {}", conn_path.as_str()); Ok(()) } diff --git a/nmrs/src/lib.rs b/nmrs/src/lib.rs index 11fdc6c1..b77df2b2 100644 --- a/nmrs/src/lib.rs +++ b/nmrs/src/lib.rs @@ -34,6 +34,20 @@ //! All operations return `Result`. The error type provides //! specific variants for common failures like authentication errors, timeouts, //! and missing devices. +//! +//! # Logging +//! +//! This crate uses the [`log`](https://docs.rs/log) facade for logging. To see +//! log output, add a logging implementation like `env_logger`: +//! +//! ```no_run +//! fn main() { +//! env_logger::init(); +//! // ... +//! } +//! ``` +//! +//! Then run with `RUST_LOG=nmrs=debug` to see debug output. // Internal implementation modules mod connection; diff --git a/nmrs/src/utils.rs b/nmrs/src/utils.rs index 8834b008..2e5fcd7e 100644 --- a/nmrs/src/utils.rs +++ b/nmrs/src/utils.rs @@ -3,6 +3,7 @@ //! Provides helpers for converting between Wi-Fi data representations: //! frequency to channel, signal strength to visual bars, SSID bytes to strings. +use log::warn; use std::str; use crate::constants::{frequency, signal_strength, wifi_mode}; @@ -65,7 +66,7 @@ pub(crate) fn decode_ssid_or_hidden(bytes: &[u8]) -> String { str::from_utf8(bytes) .map(|s| s.to_string()) .unwrap_or_else(|e| { - eprintln!("Warning: Invalid UTF-8 in SSID during comparison. {e}"); + warn!("Invalid UTF-8 in SSID during comparison: {e}"); String::new() }) } @@ -78,7 +79,7 @@ pub(crate) fn decode_ssid_or_empty(bytes: &[u8]) -> String { str::from_utf8(bytes) .map(|s| s.to_string()) .unwrap_or_else(|e| { - eprintln!("Warning: Invalid UTF-8 in SSID during comparison: {e}"); + warn!("Invalid UTF-8 in SSID during comparison: {e}"); String::new() }) } @@ -97,7 +98,7 @@ macro_rules! try_log { match $result { Ok(value) => value, Err(e) => { - eprintln!("Warning: {}: {:?}", $context, e); + log::warn!("{}: {:?}", $context, e); return None; } } From b573e229a0ece56dbf347bda5ea931f022c7ac67 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Fri, 12 Dec 2025 12:46:38 -0500 Subject: [PATCH 3/5] fix(#83): helper for duplicated network lookup logic --- Cargo.lock | 6 +- nmrs/src/connection.rs | 4 +- nmrs/src/lib.rs | 14 +-- nmrs/src/network_info.rs | 197 ++++++++++++++++----------------------- nmrs/src/scan.rs | 67 ++++++------- nmrs/src/utils.rs | 51 +++++++++- 6 files changed, 167 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee591584..fde8b54c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,9 +886,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -934,6 +934,7 @@ name = "nmrs" version = "0.4.0" dependencies = [ "futures-timer", + "log", "serde", "thiserror", "tokio", @@ -952,6 +953,7 @@ dependencies = [ "fs2", "glib", "gtk4", + "log", "nmrs", "tokio", ] diff --git a/nmrs/src/connection.rs b/nmrs/src/connection.rs index 4c6877a2..b0c894b3 100644 --- a/nmrs/src/connection.rs +++ b/nmrs/src/connection.rs @@ -426,9 +426,7 @@ async fn connect_via_saved( Err(e) => { warn!("activate_connection() failed: {e}"); - warn!( - "Saved connection may be corrupted, deleting and retrying with fresh connection" - ); + warn!("Saved connection may be corrupted, deleting and retrying with fresh connection"); let _ = delete_connection(conn, saved.clone()).await; diff --git a/nmrs/src/lib.rs b/nmrs/src/lib.rs index b77df2b2..ada0659e 100644 --- a/nmrs/src/lib.rs +++ b/nmrs/src/lib.rs @@ -38,16 +38,12 @@ //! # Logging //! //! This crate uses the [`log`](https://docs.rs/log) facade for logging. To see -//! log output, add a logging implementation like `env_logger`: -//! -//! ```no_run -//! fn main() { -//! env_logger::init(); -//! // ... -//! } +//! log output, add a logging implementation like `env_logger`. For example: + +//! ```no_run,ignore +//! env_logger::init(); +//! // ... //! ``` -//! -//! Then run with `RUST_LOG=nmrs=debug` to see debug output. // Internal implementation modules mod connection; diff --git a/nmrs/src/network_info.rs b/nmrs/src/network_info.rs index 1781a253..b93dcfb9 100644 --- a/nmrs/src/network_info.rs +++ b/nmrs/src/network_info.rs @@ -11,7 +11,8 @@ use crate::models::{ConnectionError, Network, NetworkInfo}; use crate::proxies::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; use crate::try_log; use crate::utils::{ - bars_from_strength, channel_from_freq, decode_ssid_or_empty, mode_to_string, strength_or_zero, + bars_from_strength, channel_from_freq, decode_ssid_or_empty, for_each_access_point, + mode_to_string, strength_or_zero, }; /// Returns detailed information about a network. @@ -25,125 +26,89 @@ use crate::utils::{ /// - Security capabilities (WEP, WPA, WPA2, PSK, 802.1X) /// - Current connection status pub(crate) async fn show_details(conn: &Connection, net: &Network) -> Result { - let nm = NMProxy::new(conn).await?; let active_ssid = current_ssid(conn).await; - let is_connected = active_ssid.as_deref() == Some(&net.ssid); - - for dp in nm.get_devices().await? { - let dev = NMDeviceProxy::builder(conn) - .path(dp.clone())? - .build() - .await?; - if dev.device_type().await? != device_type::WIFI { - continue; - } - - let wifi = NMWirelessProxy::builder(conn) - .path(dp.clone())? - .build() - .await?; - - let actual_bitrate = if is_connected { - wifi.bitrate().await.ok() - } else { - None - }; - - let target_ap_path = if is_connected { - let active_ap = wifi.active_access_point().await?; - if active_ap.as_str() != "/" { - Some(active_ap) - } else { - None + let is_connected_outer = active_ssid.as_deref() == Some(&net.ssid); + let target_ssid_outer = net.ssid.clone(); + let target_strength = net.strength; + + let results = for_each_access_point(conn, |ap| { + let target_ssid = target_ssid_outer.clone(); + let is_connected = is_connected_outer; + Box::pin(async move { + let ssid_bytes = ap.ssid().await?; + if decode_ssid_or_empty(&ssid_bytes) != target_ssid { + return Ok(None); } - } else { - None - }; - - let ap_paths = if let Some(active_path) = target_ap_path { - vec![active_path] - } else { - wifi.get_all_access_points().await? - }; - - for ap_path in ap_paths { - let ap = NMAccessPointProxy::builder(conn) - .path(ap_path.clone())? - .build() - .await?; - let ssid_bytes = ap.ssid().await?; - if decode_ssid_or_empty(&ssid_bytes) == net.ssid { - let strength = strength_or_zero(net.strength); - let bssid = ap.hw_address().await?; - let flags = ap.flags().await?; - let wpa_flags = ap.wpa_flags().await?; - let rsn_flags = ap.rsn_flags().await?; - let freq = ap.frequency().await.ok(); - let max_br = ap.max_bitrate().await.ok(); - let mode_raw = ap.mode().await.ok(); - - let wep = (flags & security_flags::WEP) != 0 && wpa_flags == 0 && rsn_flags == 0; - let wpa1 = wpa_flags != 0; - let wpa2_or_3 = rsn_flags != 0; - let psk = ((wpa_flags | rsn_flags) & security_flags::PSK) != 0; - let eap = ((wpa_flags | rsn_flags) & security_flags::EAP) != 0; - - let mut parts = Vec::new(); - if wep { - parts.push("WEP"); - } - if wpa1 { - parts.push("WPA"); - } - if wpa2_or_3 { - parts.push("WPA2/WPA3"); - } - if psk { - parts.push("PSK"); - } - if eap { - parts.push("802.1X"); - } - - let security = if parts.is_empty() { - "Open".to_string() - } else { - parts.join(" + ") - }; - - let status = if is_connected { - "Connected".to_string() - } else { - "Disconnected".to_string() - }; - - let channel = freq.and_then(channel_from_freq); - let rate_mbps = actual_bitrate - .or(max_br) - .map(|kbit| kbit / rate::KBIT_TO_MBPS); - let bars = bars_from_strength(strength).to_string(); - let mode = mode_raw - .map(mode_to_string) - .unwrap_or("Unknown") - .to_string(); - - return Ok(NetworkInfo { - ssid: net.ssid.clone(), - bssid, - strength, - freq, - channel, - mode, - rate_mbps, - bars, - security, - status, - }); + let strength = strength_or_zero(target_strength); + let bssid = ap.hw_address().await?; + let flags = ap.flags().await?; + let wpa_flags = ap.wpa_flags().await?; + let rsn_flags = ap.rsn_flags().await?; + let freq = ap.frequency().await.ok(); + let max_br = ap.max_bitrate().await.ok(); + let mode_raw = ap.mode().await.ok(); + + let wep = (flags & security_flags::WEP) != 0 && wpa_flags == 0 && rsn_flags == 0; + let wpa1 = wpa_flags != 0; + let wpa2_or_3 = rsn_flags != 0; + let psk = ((wpa_flags | rsn_flags) & security_flags::PSK) != 0; + let eap = ((wpa_flags | rsn_flags) & security_flags::EAP) != 0; + + let mut parts = Vec::new(); + if wep { + parts.push("WEP"); } - } - } - Err(ConnectionError::NotFound) + if wpa1 { + parts.push("WPA"); + } + if wpa2_or_3 { + parts.push("WPA2/WPA3"); + } + if psk { + parts.push("PSK"); + } + if eap { + parts.push("802.1X"); + } + + let security = if parts.is_empty() { + "Open".to_string() + } else { + parts.join(" + ") + }; + + let status = if is_connected { + "Connected".to_string() + } else { + "Disconnected".to_string() + }; + + let channel = freq.and_then(channel_from_freq); + let rate_mbps = max_br.map(|kbit| kbit / rate::KBIT_TO_MBPS); + let bars = bars_from_strength(strength).to_string(); + let mode = mode_raw + .map(mode_to_string) + .unwrap_or("Unknown") + .to_string(); + + Ok(Some(NetworkInfo { + ssid: target_ssid, + bssid, + strength, + freq, + channel, + mode, + rate_mbps, + bars, + security, + status, + })) + }) + }) + .await?; + + results.into_iter().next().ok_or(ConnectionError::NotFound) } /// Returns the SSID of the currently connected Wi-Fi network. diff --git a/nmrs/src/scan.rs b/nmrs/src/scan.rs index 36e8fc88..7d8d33c4 100644 --- a/nmrs/src/scan.rs +++ b/nmrs/src/scan.rs @@ -9,8 +9,8 @@ use zbus::Connection; use crate::Result; use crate::constants::{device_type, security_flags}; use crate::models::Network; -use crate::proxies::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; -use crate::utils::{decode_ssid_or_hidden, strength_or_zero}; +use crate::proxies::{NMDeviceProxy, NMProxy, NMWirelessProxy}; +use crate::utils::{decode_ssid_or_hidden, for_each_access_point}; /// Triggers a Wi-Fi scan on all wireless devices. /// @@ -51,30 +51,10 @@ pub(crate) async fn scan_networks(conn: &Connection) -> Result<()> { /// When multiple access points share the same SSID and frequency (e.g., mesh /// networks), the one with the strongest signal is returned. pub(crate) async fn list_networks(conn: &Connection) -> Result> { - let nm = NMProxy::new(conn).await?; - let devices = nm.get_devices().await?; - let mut networks: HashMap<(String, u32), Network> = HashMap::new(); - for dp in devices { - let d_proxy = NMDeviceProxy::builder(conn) - .path(dp.clone())? - .build() - .await?; - if d_proxy.device_type().await? != 2 { - continue; - } - - let wifi = NMWirelessProxy::builder(conn) - .path(dp.clone())? - .build() - .await?; - - for ap_path in wifi.get_all_access_points().await? { - let ap = NMAccessPointProxy::builder(conn) - .path(ap_path.clone())? - .build() - .await?; + let all_networks = for_each_access_point(conn, |ap| { + Box::pin(async move { let ssid_bytes = ap.ssid().await?; let ssid = decode_ssid_or_hidden(&ssid_bytes); let strength = ap.strength().await?; @@ -88,8 +68,8 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result> { let is_psk = (wpa & security_flags::PSK) != 0 || (rsn & security_flags::PSK) != 0; let is_eap = (wpa & security_flags::EAP) != 0 || (rsn & security_flags::EAP) != 0; - let new_net = Network { - device: dp.to_string(), + let network = Network { + device: String::new(), ssid: ssid.clone(), bssid: Some(bssid), strength: Some(strength), @@ -99,21 +79,26 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result> { is_eap, }; - // Use (SSID, frequency) as key to separate 2.4GHz and 5GHz - networks - .entry((ssid.clone(), frequency)) - .and_modify(|n| { - if strength > strength_or_zero(n.strength) { - *n = new_net.clone(); - } - if new_net.secured { - n.secured = true; - } - }) - .or_insert(new_net); - } + Ok(Some((ssid, frequency, network))) + }) + }) + .await?; + + // Deduplicate: use (SSID, frequency) as key, keep strongest signal + for (ssid, frequency, new_net) in all_networks { + let strength = new_net.strength.unwrap_or(0); + networks + .entry((ssid, frequency)) + .and_modify(|n| { + if strength > n.strength.unwrap_or(0) { + *n = new_net.clone(); + } + if new_net.secured { + n.secured = true; + } + }) + .or_insert(new_net); } - let result: Vec = networks.into_values().collect(); - Ok(result) + Ok(networks.into_values().collect()) } diff --git a/nmrs/src/utils.rs b/nmrs/src/utils.rs index 2e5fcd7e..bb475a83 100644 --- a/nmrs/src/utils.rs +++ b/nmrs/src/utils.rs @@ -5,8 +5,11 @@ use log::warn; use std::str; +use zbus::Connection; -use crate::constants::{frequency, signal_strength, wifi_mode}; +use crate::Result; +use crate::constants::{device_type, frequency, signal_strength, wifi_mode}; +use crate::proxies::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; /// Converts a Wi-Fi frequency in MHz to a channel number. /// @@ -90,6 +93,52 @@ pub(crate) fn strength_or_zero(strength: Option) -> u8 { strength.unwrap_or(0) } +/// This helper iterates through all WiFi access points and calls the provided async function. +/// +/// Loops through devices, filters for WiFi, and invokes `func` with each access point proxy. +/// The function is awaited immediately in the loop to avoid lifetime issues. +pub(crate) async fn for_each_access_point(conn: &Connection, mut func: F) -> Result> +where + F: for<'a> FnMut( + &'a NMAccessPointProxy<'a>, + ) -> std::pin::Pin< + Box>> + 'a>, + >, +{ + let nm = NMProxy::new(conn).await?; + let devices = nm.get_devices().await?; + + let mut results = Vec::new(); + + for dp in devices { + let d_proxy = NMDeviceProxy::builder(conn) + .path(dp.clone())? + .build() + .await?; + + if d_proxy.device_type().await? != device_type::WIFI { + continue; + } + + let wifi = NMWirelessProxy::builder(conn) + .path(dp.clone())? + .build() + .await?; + + for ap_path in wifi.get_all_access_points().await? { + let ap = NMAccessPointProxy::builder(conn) + .path(ap_path)? + .build() + .await?; + if let Some(result) = func(&ap).await? { + results.push(result); + } + } + } + + Ok(results) +} + /// Macro to convert Result to Option with error logging. /// Usage: `try_log!(result, "context message")?` #[macro_export] From 6de41bf2e9195150caa4a4668f38be9db3b4a72e Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Fri, 12 Dec 2025 13:07:38 -0500 Subject: [PATCH 4/5] fix(#61): UI on saved connections --- nmrs-gui/src/ui/networks.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nmrs-gui/src/ui/networks.rs b/nmrs-gui/src/ui/networks.rs index 966577ad..1eb5f518 100644 --- a/nmrs-gui/src/ui/networks.rs +++ b/nmrs-gui/src/ui/networks.rs @@ -126,10 +126,14 @@ impl NetworkRowController { let have = nm_c.has_saved_connection(&ssid_c).await.unwrap_or(false); if have { + status_c.set_text(&format!("Connecting to {}...", ssid_c)); window_c.set_sensitive(false); let creds = WifiSecurity::WpaPsk { psk: "".into() }; match nm_c.connect(&ssid_c, creds).await { - Ok(_) => on_success_c(), + Ok(_) => { + status_c.set_text(""); + on_success_c(); + } Err(e) => status_c.set_text(&format!("Failed to connect: {e}")), } window_c.set_sensitive(true); @@ -143,10 +147,14 @@ impl NetworkRowController { ); } } else { + status_c.set_text(&format!("Connecting to {}...", ssid_c)); window_c.set_sensitive(false); let creds = WifiSecurity::Open; match nm_c.connect(&ssid_c, creds).await { - Ok(_) => on_success_c(), + Ok(_) => { + status_c.set_text(""); + on_success_c(); + } Err(e) => status_c.set_text(&format!("Failed to connect: {e}")), } window_c.set_sensitive(true); From a6ab314b2178005b7520127d0a6bc7fb86a25438 Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Fri, 12 Dec 2025 13:13:14 -0500 Subject: [PATCH 5/5] chore: update CHANGELOG.md and Nix sha --- CHANGELOG.md | 8 ++++++-- package.nix | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548614e9..91cb7db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ - **nmrs-gui**: Renamed crate from `nmrs-ui` to `nmrs-gui` ### Added -- Core: `StateReason` enum and `reason_to_error()` for mapping NetworkManager failure codes to typed errors ([#82](https://github.com/cachebag/nmrs/issues/82)) +- Core: `StateReason` enum and `reason_to_error()` for mapping NetworkManager failure codes to typed errors ([#82](https://github.com/cachebag/nmrs/issues/82), [#85](https://github.com/cachebag/nmrs/issues/85)) - Core: Comprehensive documentation across all modules ([#82](https://github.com/cachebag/nmrs/issues/82)) +- Core: Logging support via `log` crate facade ([#87](https://github.com/cachebag/nmrs/issues/87)) - UI: Pre-defined themes (Catppuccin, Dracula, Gruvbox, Nord, Tokyo) ([#106](https://github.com/cachebag/nmrs/issues/106)) - CLI: `--version` flag with build hash extraction ([#108](https://github.com/cachebag/nmrs/issues/108)) @@ -18,11 +19,14 @@ - Core: Decomposed `connect()` into smaller helper functions ([#81](https://github.com/cachebag/nmrs/issues/81)) - Core: Extracted disconnect + wait logic to unified helper ([#79](https://github.com/cachebag/nmrs/issues/79)) - Core: Unified state polling logic ([#80](https://github.com/cachebag/nmrs/issues/80)) +- Core: Eliminated network lookup duplication via shared helper function ([#83](https://github.com/cachebag/nmrs/issues/83)) +- Core: Replaced `eprintln!` with structured logging (`debug!`, `info!`, `warn!`, `error!`) ([#87](https://github.com/cachebag/nmrs/issues/87)) ### Fixed -- Core: Auth error mapping now properly distinguishes supplicant failures, DHCP errors, and timeouts ([#82](https://github.com/cachebag/nmrs/issues/82), [#116](https://github.com/cachebag/nmrs/issues/116)) +- Core: Auth error mapping now properly distinguishes supplicant failures, DHCP errors, and timeouts ([#82](https://github.com/cachebag/nmrs/issues/82), [#85](https://github.com/cachebag/nmrs/issues/85), [#116](https://github.com/cachebag/nmrs/issues/116)) - Core: `bitrate` property now fetches real connection speeds ([#110](https://github.com/cachebag/nmrs/issues/110)) - UI: Re-aligned refresh button ([#111](https://github.com/cachebag/nmrs/issues/111)) +- UI: Show connection status when connecting with saved credentials ([#61](https://github.com/cachebag/nmrs/issues/61)) ## [0.3.0-beta] - 2025-12-08 ### Fixed diff --git a/package.nix b/package.nix index 6fbe27d0..76e6c730 100644 --- a/package.nix +++ b/package.nix @@ -19,7 +19,7 @@ rustPlatform.buildRustPackage { src = ./.; - cargoHash = "sha256-/FHd8A9/3E9F2YaWOSDVhCaboN/dYGah53fI1Dg3w5w="; + cargoHash = "sha256-Z538+q/Af7nshS8G8mPV7aGfTd1XiGKjYljNf9FW3HA="; nativeBuildInputs = [ pkg-config