Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
<img width="752" height="802" alt="image" src="https://github.com/user-attachments/assets/3494e88e-5cdd-4848-9fd7-b85b9a5ea2ef" />
<img width="603" height="718" alt="image" src="https://github.com/user-attachments/assets/67f424b2-4c48-4d87-8ec2-7ae2db090048" />

#

Expand Down
109 changes: 97 additions & 12 deletions nmrs-core/src/dbus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ trait NMAccessPoint {

#[zbus(property)]
fn rsn_flags(&self) -> Result<u32>;

#[zbus(property)]
fn frequency(&self) -> Result<u32>;

#[zbus(property)]
fn max_bitrate(&self) -> Result<u32>;

#[zbus(property)]
fn mode(&self) -> Result<u32>;
}

impl NetworkManager {
Expand Down Expand Up @@ -416,7 +425,7 @@ impl NetworkManager {
None
}

pub async fn show_details(&self, ssid: &str) -> zbus::Result<NetworkInfo> {
pub async fn show_details(&self, net: &Network) -> zbus::Result<NetworkInfo> {
let nm = NMProxy::new(&self.conn).await?;
for dp in nm.get_devices().await? {
let dev = NMDeviceProxy::builder(&self.conn)
Expand All @@ -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<u16> {
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",
}
}
}
5 changes: 5 additions & 0 deletions nmrs-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ pub struct NetworkInfo {
pub bssid: String,
pub strength: u8,
pub freq: Option<u32>,
pub channel: Option<u16>,
pub mode: String,
pub rate_mbps: Option<u32>,
pub bars: String,
pub security: String,
pub status: String,
}

#[derive(Debug, Clone)]
Expand Down
59 changes: 56 additions & 3 deletions nmrs-ui/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ switch:checked slider {
background-color: #ffffff;
}

.wifi-label {
font-weight: 600;
color: #e0e0e0;
}

list {
background: #121212;
border: none;
Expand Down Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion nmrs-ui/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
86 changes: 75 additions & 11 deletions nmrs-ui/src/ui/network_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use gtk::{Align, Box, Button, Image, Label, Orientation};
use nmrs_core::models::NetworkInfo;

pub fn network_page(info: &NetworkInfo, stack: &gtk::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);
Expand All @@ -20,26 +21,88 @@ pub fn network_page(info: &NetworkInfo, stack: &gtk::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);
Expand All @@ -50,9 +113,10 @@ pub fn network_page(info: &NetworkInfo, stack: &gtk::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
}
Loading