From 98c8f4b590a27de34bbb639c0b49cc17a209815e Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Sun, 2 Nov 2025 00:37:59 -0400 Subject: [PATCH] ui+dbus: network info popup, basic network info (security, bssid, strength) --- nmrs-core/src/dbus.rs | 49 +++++++++++++++++++++- nmrs-core/src/models.rs | 12 +++++- nmrs-ui/src/style.css | 74 ++++++++++++++++++++++++++++++++++ nmrs-ui/src/ui/mod.rs | 1 + nmrs-ui/src/ui/network_page.rs | 43 ++++++++++++++++++++ nmrs-ui/src/ui/networks.rs | 44 +++++++++++++++++++- 6 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 nmrs-ui/src/ui/network_page.rs diff --git a/nmrs-core/src/dbus.rs b/nmrs-core/src/dbus.rs index 3edf641c..886af1f4 100644 --- a/nmrs-core/src/dbus.rs +++ b/nmrs-core/src/dbus.rs @@ -1,4 +1,4 @@ -use crate::models::{Device, DeviceState, DeviceType, Network}; +use crate::models::{Device, DeviceState, DeviceType, Network, NetworkInfo}; use crate::wifi_builders::build_wifi_connection; use futures_timer::Delay; use std::collections::HashMap; @@ -415,4 +415,51 @@ impl NetworkManager { } None } + + pub async fn show_details(&self, ssid: &str) -> zbus::Result { + let nm = NMProxy::new(&self.conn).await?; + for dp in nm.get_devices().await? { + let dev = NMDeviceProxy::builder(&self.conn) + .path(dp.clone())? + .build() + .await?; + if dev.device_type().await? != 2 { + continue; + } + + let wifi = NMWirelessProxy::builder(&self.conn) + .path(dp.clone())? + .build() + .await?; + + for ap_path in wifi.get_all_access_points().await? { + let ap = NMAccessPointProxy::builder(&self.conn) + .path(ap_path.clone())? + .build() + .await?; + + let ssid_bytes = ap.ssid().await?; + if std::str::from_utf8(&ssid_bytes).unwrap_or("") == ssid { + let strength = ap.strength().await?; + 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" + } else { + "open" + }; + return Ok(NetworkInfo { + ssid: ssid.to_string(), + bssid, + strength, + freq: None, + security: security.to_string(), + }); + } + } + } + + Err(zbus::Error::Failure("Network not found".into())) + } } diff --git a/nmrs-core/src/models.rs b/nmrs-core/src/models.rs index d2da1179..be742e03 100644 --- a/nmrs-core/src/models.rs +++ b/nmrs-core/src/models.rs @@ -13,6 +13,14 @@ pub struct Network { pub is_eap: bool, } +pub struct NetworkInfo { + pub ssid: String, + pub bssid: String, + pub strength: u8, + pub freq: Option, + pub security: String, +} + #[derive(Debug, Clone)] pub struct Device { pub path: String, @@ -113,7 +121,7 @@ impl Display for DeviceType { DeviceType::Wifi => write!(f, "Wi-Fi"), DeviceType::WifiP2P => write!(f, "Wi-Fi P2P"), DeviceType::Loopback => write!(f, "Loopback"), - DeviceType::Other(v) => write!(f, "Other({})", v), + DeviceType::Other(v) => write!(f, "Other({v})"), } } } @@ -127,7 +135,7 @@ impl Display for DeviceState { DeviceState::Prepare => write!(f, "Preparing"), DeviceState::Config => write!(f, "Configuring"), DeviceState::Activated => write!(f, "Activated"), - DeviceState::Other(v) => write!(f, "Other({})", v), + DeviceState::Other(v) => write!(f, "Other({v})"), } } } diff --git a/nmrs-ui/src/style.css b/nmrs-ui/src/style.css index 6dba6381..b13ad1cd 100644 --- a/nmrs-ui/src/style.css +++ b/nmrs-ui/src/style.css @@ -159,3 +159,77 @@ headerbar label:last-child { opacity: 0.7; margin-top: 4px; } + +.network-arrow:hover { + opacity: 1.0; + transform: scale(1.1); + transition: transform 120ms ease, opacity 120ms ease; +} + +.network-details label { + font-size: 14px; + color: #e0e0e0; +} + +.network-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 8px; +} + +.network-page { + background: #1e1e1e; + border-radius: 12px; + border: 1px solid #3a3a3a; + padding: 20px; +} + +.network-title { + font-size: 18px; + font-weight: bold; + color: #ffffff; +} + +.network-detail { + font-size: 14px; + color: #cccccc; + margin-left: 6px; +} + +.disconnect-btn { + background: #3a7bd5; + color: #ffffff; + border-radius: 8px; + padding: 6px 14px; +} + +.forget-btn { + background: #d54d3a; + color: #ffffff; + border-radius: 8px; + padding: 6px 14px; +} + +.network-dialog { + background: #1a1a1a; + border: none; + box-shadow: none; +} + +.network-page { + border-radius: 12px; + background: #1a1a1a; + padding: 20px; +} + +button.disconnect-btn { + background: #2563eb; + color: white; + border-radius: 8px; +} + +button.forget-btn { + background: #b91c1c; + color: white; + border-radius: 8px; +} diff --git a/nmrs-ui/src/ui/mod.rs b/nmrs-ui/src/ui/mod.rs index 94119e36..f8f1855c 100644 --- a/nmrs-ui/src/ui/mod.rs +++ b/nmrs-ui/src/ui/mod.rs @@ -1,5 +1,6 @@ pub mod connect; pub mod header; +pub mod network_page; pub mod networks; use gtk::prelude::*; diff --git a/nmrs-ui/src/ui/network_page.rs b/nmrs-ui/src/ui/network_page.rs new file mode 100644 index 00000000..d0622112 --- /dev/null +++ b/nmrs-ui/src/ui/network_page.rs @@ -0,0 +1,43 @@ +use gtk::prelude::*; +use gtk::{Box, Button, Image, Label, Orientation}; +use nmrs_core::models::NetworkInfo; +use relm4::RelmWidgetExt; + +pub fn network_page(info: &NetworkInfo) -> Box { + let container = Box::new(Orientation::Vertical, 16); + container.add_css_class("network-page"); + container.set_margin_all(20); + + let header = Box::new(Orientation::Horizontal, 8); + let icon = Image::from_icon_name("network-wireless-signal-excellent-symbolic"); + icon.set_pixel_size(32); + let title = Label::new(Some(&info.ssid)); + title.add_css_class("network-title"); + header.append(&icon); + header.append(&title); + container.append(&header); + + let details = Box::new(Orientation::Vertical, 4); + let bssid = Label::new(Some(&format!("BSSID: {}", info.bssid))); + let strength = Label::new(Some(&format!("Signal: {}%", info.strength))); + let security = Label::new(Some(&format!("Security: {}", info.security))); + details.append(&bssid); + details.append(&strength); + details.append(&security); + container.append(&details); + + let actions = Box::new(Orientation::Horizontal, 12); + actions.set_halign(gtk::Align::End); + + let disconnect = Button::with_label("Disconnect"); + disconnect.add_css_class("disconnect-btn"); + actions.append(&disconnect); + + let forget = Button::with_label("Forget"); + forget.add_css_class("forget-btn"); + actions.append(&forget); + + container.append(&actions); + + container +} diff --git a/nmrs-ui/src/ui/networks.rs b/nmrs-ui/src/ui/networks.rs index 629af12e..2ba52ba3 100644 --- a/nmrs-ui/src/ui/networks.rs +++ b/nmrs-ui/src/ui/networks.rs @@ -1,12 +1,14 @@ use glib::clone; use gtk::Align; use gtk::GestureClick; +use gtk::gdk; use gtk::prelude::*; use gtk::{Box, Image, Label, ListBox, ListBoxRow, Orientation}; use nmrs_core::models::WifiSecurity; use nmrs_core::{NetworkManager, models}; use crate::ui::connect; +use crate::ui::network_page::network_page; pub fn networks_view( networks: &[models::Network], @@ -59,7 +61,46 @@ pub fn networks_view( let arrow = Image::from_icon_name("go-next-symbolic"); arrow.set_halign(Align::End); arrow.add_css_class("network-arrow"); - hbox.append(&arrow); + + let arrow_click = GestureClick::new(); + arrow.set_cursor_from_name(Some("pointer")); + let ssid_clone = net.ssid.clone(); + arrow_click.connect_pressed(clone!( + #[weak] + parent_window, + move |_, _, _, _| { + let ssid_clone = ssid_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 page = network_page(&details); + let dialog = gtk::Window::builder() + .title(&details.ssid) + .child(&page) + .transient_for(&parent_window) + .default_width(300) + .default_height(200) + .build(); + + let key = gtk::EventControllerKey::new(); + { + let dialog = dialog.clone(); + key.connect_key_pressed(move |_, key, _, _| { + if key == gdk::Key::Escape { + dialog.close(); + return glib::Propagation::Stop; + } + glib::Propagation::Proceed + }); + } + dialog.add_controller(key); + dialog.present(); + } + }); + } + )); + arrow.add_controller(arrow_click); let ssid_str = net.ssid.clone(); let secured = net.secured; @@ -93,6 +134,7 @@ pub fn networks_view( row.add_controller(gesture); + hbox.append(&arrow); row.set_child(Some(&hbox)); list.append(&row); }