diff --git a/README.md b/README.md index 937fe652..a2f73cea 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![CI](https://github.com/cachebag/nmrs/actions/workflows/ci.yml/badge.svg) # nmrs #### Wayland-native frontend for NetworkManager. Provides a GTK4 UI and a D-Bus proxy core, built in Rust. -image +image # diff --git a/nmrs-core/src/dbus.rs b/nmrs-core/src/dbus.rs index 886af1f4..591dab28 100644 --- a/nmrs-core/src/dbus.rs +++ b/nmrs-core/src/dbus.rs @@ -102,6 +102,15 @@ trait NMAccessPoint { #[zbus(property)] fn rsn_flags(&self) -> Result; + + #[zbus(property)] + fn frequency(&self) -> Result; + + #[zbus(property)] + fn max_bitrate(&self) -> Result; + + #[zbus(property)] + fn mode(&self) -> Result; } impl NetworkManager { @@ -416,7 +425,7 @@ impl NetworkManager { None } - pub async fn show_details(&self, ssid: &str) -> zbus::Result { + pub async fn show_details(&self, net: &Network) -> zbus::Result { let nm = NMProxy::new(&self.conn).await?; for dp in nm.get_devices().await? { let dev = NMDeviceProxy::builder(&self.conn) @@ -439,27 +448,103 @@ impl NetworkManager { .await?; let ssid_bytes = ap.ssid().await?; - if std::str::from_utf8(&ssid_bytes).unwrap_or("") == ssid { - let strength = ap.strength().await?; + if std::str::from_utf8(&ssid_bytes).unwrap_or("") == net.ssid { + let strength = net.strength.unwrap_or(0); let bssid = ap.hw_address().await?; - let wpa = ap.wpa_flags().await?; - let rsn = ap.rsn_flags().await?; - let security = if wpa != 0 || rsn != 0 { - "secured" + 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 & 0x1) != 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) & 0x0100) != 0; + let eap = ((wpa_flags | rsn_flags) & 0x0200) != 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 { - "open" + parts.join(" + ") }; + + let active_ssid = self.current_ssid().await; + let status = if active_ssid.as_deref() == Some(&net.ssid) { + "Connected".to_string() + } else { + "Disconnected".to_string() + }; + + let channel = freq.and_then(NetworkManager::channel_from_freq); + let rate_mbps = max_br.map(|kbit| kbit / 1000); + let bars = NetworkManager::bars_from_strength(strength).to_string(); + let mode = mode_raw + .map(NetworkManager::mode_to_string) + .unwrap_or("Unknown") + .to_string(); + return Ok(NetworkInfo { - ssid: ssid.to_string(), + ssid: net.ssid.clone(), bssid, strength, - freq: None, - security: security.to_string(), + freq, + channel, + mode, + rate_mbps, + bars, + security, + status, }); } } } - Err(zbus::Error::Failure("Network not found".into())) } + + fn channel_from_freq(mhz: u32) -> Option { + match mhz { + 2412..=2472 => Some(((mhz - 2412) / 5 + 1) as u16), // ch 1..13 + 2484 => Some(14), + 5000..=5900 => Some(((mhz - 5000) / 5) as u16), // common 5 GHz mapping + 5955..=7115 => Some(((mhz - 5955) / 5 + 1) as u16), // 6 GHz ch 1..233 + _ => None, + } + } + + fn bars_from_strength(s: u8) -> &'static str { + match s { + 0..=24 => "▂___", + 25..=49 => "▂▄__", + 50..=74 => "▂▄▆_", + _ => "▂▄▆█", + } + } + + fn mode_to_string(m: u32) -> &'static str { + match m { + 1 => "Adhoc", + 2 => "Infra", + 3 => "AP", + _ => "Unknown", + } + } } diff --git a/nmrs-core/src/models.rs b/nmrs-core/src/models.rs index be742e03..85f7bd01 100644 --- a/nmrs-core/src/models.rs +++ b/nmrs-core/src/models.rs @@ -18,7 +18,12 @@ pub struct NetworkInfo { pub bssid: String, pub strength: u8, pub freq: Option, + pub channel: Option, + pub mode: String, + pub rate_mbps: Option, + pub bars: String, pub security: String, + pub status: String, } #[derive(Debug, Clone)] diff --git a/nmrs-ui/src/style.css b/nmrs-ui/src/style.css index b955546b..349ae0bd 100644 --- a/nmrs-ui/src/style.css +++ b/nmrs-ui/src/style.css @@ -32,6 +32,11 @@ switch:checked slider { background-color: #ffffff; } +.wifi-label { + font-weight: 600; + color: #e0e0e0; +} + list { background: #121212; border: none; @@ -100,18 +105,66 @@ label.network-poor { color: #ef4444; } margin-bottom: 4px; } +.network-arrow { + color: white; +} + .network-info { margin-top: 14px; padding-left: 6px; } .info-row { padding: 2px 0; } +.info-value { + color: #ffffff; + font-size: 14px; + font-weight: 500; +} + +.section-header { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + border-bottom: 1px solid #2a2a2a; + padding-bottom: 4px; + margin-bottom: 6px; + color: #bfbfbf; + letter-spacing: 0.5px; +} + +.advanced-section, +.basic-section { + padding-left: 0; + margin-left: 0; +} + +.basic-key, .info-label { - color: #a3a3a3; + font-weight: 600; font-size: 13px; - min-width: 120px; + color: #a3a3a3; + margin-bottom: 2px; + text-decoration: underline; } + +.basic-value, .info-value { - color: #ffffff; font-size: 14px; + color: #ffffff; font-weight: 500; + margin-bottom: 8px; +} + +.divider { + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.25; + border-bottom: 1px solid #2a2a2a; +} + +.wifi-secure { + color: white; +} + +.wifi-open { + color: white; } diff --git a/nmrs-ui/src/ui/mod.rs b/nmrs-ui/src/ui/mod.rs index 9ca8be27..6a2ace72 100644 --- a/nmrs-ui/src/ui/mod.rs +++ b/nmrs-ui/src/ui/mod.rs @@ -10,7 +10,7 @@ use gtk::{ pub fn build_ui(app: &Application) { let win = ApplicationWindow::new(app); - win.set_title(Some("nmrs")); + win.set_title(Some("")); win.set_default_size(400, 600); let vbox = GtkBox::new(Orientation::Vertical, 0); diff --git a/nmrs-ui/src/ui/network_page.rs b/nmrs-ui/src/ui/network_page.rs index 5295d204..36a927f3 100644 --- a/nmrs-ui/src/ui/network_page.rs +++ b/nmrs-ui/src/ui/network_page.rs @@ -4,9 +4,10 @@ use gtk::{Align, Box, Button, Image, Label, Orientation}; use nmrs_core::models::NetworkInfo; pub fn network_page(info: &NetworkInfo, stack: >k::Stack) -> Box { - let container = Box::new(Orientation::Vertical, 0); + let container = Box::new(Orientation::Vertical, 12); container.add_css_class("network-page"); + // Back button let back = Button::with_label("← Back"); back.add_css_class("back-button"); back.set_halign(Align::Start); @@ -20,26 +21,88 @@ pub fn network_page(info: &NetworkInfo, stack: >k::Stack) -> Box { )); container.append(&back); + // Header let header = Box::new(Orientation::Horizontal, 6); let icon = Image::from_icon_name("network-wireless-signal-excellent-symbolic"); - icon.set_pixel_size(22); + icon.set_pixel_size(24); let title = Label::new(Some(&info.ssid)); title.add_css_class("network-title"); header.append(&icon); header.append(&title); container.append(&header); - let info_box = Box::new(Orientation::Vertical, 6); - info_box.add_css_class("network-info"); + // Basic info section + let basic_box = Box::new(Orientation::Vertical, 6); + basic_box.add_css_class("basic-section"); - let fields = [ - ("BSSID", &info.bssid), + let basic_header = Label::new(Some("Basic")); + basic_header.add_css_class("section-header"); + basic_box.append(&basic_header); + + let status_fields = [ + ("Connection Status", info.status.as_str()), ("Signal Strength", &format!("{}%", info.strength)), - ("Security", &info.security), + ("Bars", info.bars.as_str()), ]; - for (label, value) in fields { - let row = Box::new(Orientation::Horizontal, 6); + for (label, value) in status_fields { + let row = Box::new(Orientation::Vertical, 2); + row.set_halign(Align::Start); + + let key = Label::new(Some(label)); + key.add_css_class("basic-key"); + key.set_halign(Align::Start); + + let val = Label::new(Some(value)); + val.add_css_class("basic-value"); + val.set_halign(Align::Start); + + row.append(&key); + row.append(&val); + basic_box.append(&row); + } + + container.append(&basic_box); + + // Advanced info section + let advanced_box = Box::new(Orientation::Vertical, 8); + advanced_box.add_css_class("advanced-section"); + + let advanced_header = Label::new(Some("Advanced")); + advanced_header.add_css_class("section-header"); + advanced_box.append(&advanced_header); + + let advanced_fields = [ + ("BSSID", info.bssid.as_str()), + ( + "Frequency", + &info + .freq + .map(|f| format!("{:.1} GHz", f as f32 / 1000.0)) + .unwrap_or_else(|| "-".into()), + ), + ( + "Channel", + &info + .channel + .map(|c| c.to_string()) + .unwrap_or_else(|| "-".into()), + ), + ("Mode", info.mode.as_str()), + ( + "Speed", + &info + .rate_mbps + .map(|r| format!("{r:.2} Mbps")) + .unwrap_or_else(|| "-".into()), + ), + ("Security", info.security.as_str()), + ]; + + for (label, value) in advanced_fields { + let row = Box::new(Orientation::Vertical, 3); + row.set_halign(Align::Start); + let key = Label::new(Some(label)); key.add_css_class("info-label"); key.set_halign(Align::Start); @@ -50,9 +113,10 @@ pub fn network_page(info: &NetworkInfo, stack: >k::Stack) -> Box { row.append(&key); row.append(&val); - info_box.append(&row); + + advanced_box.append(&row); } - container.append(&info_box); + container.append(&advanced_box); container } diff --git a/nmrs-ui/src/ui/networks.rs b/nmrs-ui/src/ui/networks.rs index ed02aa87..3b6efbc3 100644 --- a/nmrs-ui/src/ui/networks.rs +++ b/nmrs-ui/src/ui/networks.rs @@ -63,19 +63,23 @@ pub fn networks_view( arrow.set_cursor_from_name(Some("pointer")); let arrow_click = GestureClick::new(); - let ssid_clone = net.ssid.clone(); + let net_clone = net.clone(); arrow_click.connect_pressed(clone!( #[weak] stack, move |_, _, _, _| { - let ssid_clone = ssid_clone.clone(); + let net_data = net_clone.clone(); glib::MainContext::default().spawn_local(async move { if let Ok(nm) = NetworkManager::new().await - && let Ok(details) = nm.show_details(&ssid_clone).await + && let Ok(details) = nm.show_details(&net_data).await { - let page = network_page(&details, &stack); - stack.add_named(&page, Some("details")); + let container = network_page(&details, &stack); + + if let Some(old) = stack.child_by_name("details") { + stack.remove(&old); + } + stack.add_named(&container, Some("details")); stack.set_visible_child_name("details"); } });