diff --git a/README.md b/README.md
index 937fe652..a2f73cea 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@

# nmrs
#### Wayland-native frontend for NetworkManager. Provides a GTK4 UI and a D-Bus proxy core, built in Rust.
-
+
#
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");
}
});