Skip to content

Commit 0cebe08

Browse files
Refine Wi-Fi UI to impala-style layout
1 parent abcdcef commit 0cebe08

8 files changed

Lines changed: 182 additions & 56 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "nettui"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
edition = "2024"
55
description = "Unified TUI for Wi-Fi and Ethernet"
66
license = "GPL-3.0-only"

src/app.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ impl App {
283283
.cloned()
284284
.ok_or_else(|| std::io::Error::other("no wifi adapter found"))?;
285285
self.wifi_backend.scan(&iface).await?;
286+
tokio::time::sleep(Duration::from_millis(1200)).await;
286287
self.last_action = Some(format!("Wi-Fi scan requested on {iface}"));
287288
self.set_toast(ToastKind::Info, format!("Wi-Fi scan requested on {iface}"));
288289
self.notify("Wi-Fi", &format!("Scan requested on {iface}"))
@@ -529,6 +530,7 @@ mod tests {
529530
connected_ssid: Some("Home".to_string()),
530531
known_networks: vec![],
531532
new_networks: vec![],
533+
device: None,
532534
};
533535
let ethernet = EthernetState {
534536
ifaces: vec![EthernetIface {
@@ -557,6 +559,7 @@ mod tests {
557559
connected_ssid: None,
558560
known_networks: vec![],
559561
new_networks: vec![],
562+
device: None,
560563
};
561564
let ethernet = EthernetState { ifaces: vec![] };
562565

src/backend/iwd.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
backend::traits::WifiBackend,
3-
domain::wifi::{WifiNetwork, WifiState},
3+
domain::wifi::{WifiDeviceInfo, WifiNetwork, WifiState},
44
};
55
use anyhow::Result;
66
use std::collections::HashSet;
@@ -38,6 +38,7 @@ impl WifiBackend for IwdBackend {
3838
let connected_ssid = parse_connected_network(&iface);
3939
let mut known_networks = parse_known_networks();
4040
let mut new_networks = parse_networks(&iface);
41+
let station_info = parse_station_info(&iface);
4142

4243
if let Some(conn) = &connected_ssid {
4344
for n in &mut known_networks {
@@ -56,11 +57,25 @@ impl WifiBackend for IwdBackend {
5657
let known_ssids: HashSet<&str> = known_networks.iter().map(|n| n.ssid.as_str()).collect();
5758
new_networks.retain(|n| !known_ssids.contains(n.ssid.as_str()));
5859

60+
let security = station_info.security.unwrap_or_else(|| {
61+
infer_connected_security(&known_networks, &new_networks)
62+
.unwrap_or_else(|| "-".to_string())
63+
});
64+
5965
Ok(WifiState {
6066
ifaces,
6167
connected_ssid,
6268
known_networks,
6369
new_networks,
70+
device: Some(WifiDeviceInfo {
71+
iface: iface.clone(),
72+
mode: "station".to_string(),
73+
powered: "On".to_string(),
74+
state: station_info.state.unwrap_or_else(|| "-".to_string()),
75+
scanning: station_info.scanning.unwrap_or_else(|| "-".to_string()),
76+
frequency: station_info.frequency.unwrap_or_else(|| "-".to_string()),
77+
security,
78+
}),
6479
})
6580
}
6681
}
@@ -157,6 +172,8 @@ fn parse_networks(iface: &str) -> Vec<WifiNetwork> {
157172
security,
158173
signal,
159174
connected,
175+
hidden: None,
176+
autoconnect: None,
160177
});
161178
}
162179

@@ -202,12 +219,75 @@ fn parse_known_networks() -> Vec<WifiNetwork> {
202219
security,
203220
signal: "-".to_string(),
204221
connected: false,
222+
hidden: None,
223+
autoconnect: None,
205224
});
206225
}
207226

208227
nets
209228
}
210229

230+
#[derive(Default)]
231+
struct StationInfo {
232+
state: Option<String>,
233+
scanning: Option<String>,
234+
frequency: Option<String>,
235+
security: Option<String>,
236+
}
237+
238+
fn parse_station_info(iface: &str) -> StationInfo {
239+
let out = std::process::Command::new("iwctl")
240+
.arg("station")
241+
.arg(iface)
242+
.arg("show")
243+
.output();
244+
245+
let Ok(out) = out else {
246+
return StationInfo::default();
247+
};
248+
if !out.status.success() {
249+
return StationInfo::default();
250+
}
251+
252+
let txt = String::from_utf8_lossy(&out.stdout);
253+
let mut info = StationInfo::default();
254+
255+
for raw in txt.lines() {
256+
let line = raw.trim();
257+
if let Some(v) = value_after_key(line, "State") {
258+
info.state = Some(v.to_string());
259+
} else if let Some(v) = value_after_key(line, "Scanning") {
260+
info.scanning = Some(v.to_string());
261+
} else if let Some(v) = value_after_key(line, "Frequency") {
262+
info.frequency = Some(v.to_string());
263+
} else if let Some(v) = value_after_key(line, "Security") {
264+
info.security = Some(v.to_string());
265+
}
266+
}
267+
268+
info
269+
}
270+
271+
fn value_after_key<'a>(line: &'a str, key: &str) -> Option<&'a str> {
272+
if !line.starts_with(key) {
273+
return None;
274+
}
275+
let mut rest = &line[key.len()..];
276+
rest = rest.trim();
277+
if let Some(stripped) = rest.strip_prefix(':') {
278+
rest = stripped.trim();
279+
}
280+
if rest.is_empty() { None } else { Some(rest) }
281+
}
282+
283+
fn infer_connected_security(known: &[WifiNetwork], new: &[WifiNetwork]) -> Option<String> {
284+
known
285+
.iter()
286+
.find(|n| n.connected)
287+
.map(|n| n.security.clone())
288+
.or_else(|| new.iter().find(|n| n.connected).map(|n| n.security.clone()))
289+
}
290+
211291
async fn run_iwctl(args: &[&str]) -> Result<()> {
212292
let out = Command::new("iwctl").args(args).output().await?;
213293
if out.status.success() {

src/domain/wifi.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ pub struct WifiNetwork {
44
pub security: String,
55
pub signal: String,
66
pub connected: bool,
7+
pub hidden: Option<bool>,
8+
pub autoconnect: Option<bool>,
9+
}
10+
11+
#[derive(Debug, Clone)]
12+
pub struct WifiDeviceInfo {
13+
pub iface: String,
14+
pub mode: String,
15+
pub powered: String,
16+
pub state: String,
17+
pub scanning: String,
18+
pub frequency: String,
19+
pub security: String,
720
}
821

922
#[derive(Debug, Clone)]
@@ -12,6 +25,7 @@ pub struct WifiState {
1225
pub connected_ssid: Option<String>,
1326
pub known_networks: Vec<WifiNetwork>,
1427
pub new_networks: Vec<WifiNetwork>,
28+
pub device: Option<WifiDeviceInfo>,
1529
}
1630

1731
impl WifiState {
@@ -21,6 +35,7 @@ impl WifiState {
2135
connected_ssid: None,
2236
known_networks: Vec::new(),
2337
new_networks: Vec::new(),
38+
device: None,
2439
}
2540
}
2641

src/ui/components.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
app::App,
3-
domain::common::{ActiveTab, ToastKind, WifiFocus},
3+
domain::common::{ActiveTab, ToastKind},
44
};
55
use ratatui::{
66
Frame,
@@ -53,23 +53,15 @@ pub fn render_footer(app: &App, frame: &mut Frame, area: Rect) {
5353

5454
match app.active_tab {
5555
ActiveTab::Wifi => {
56-
let focus = match app.wifi_focus {
57-
WifiFocus::KnownNetworks => "Known",
58-
WifiFocus::NewNetworks => "New",
59-
WifiFocus::Adapter => "Adapter",
60-
};
6156
spans.extend([
6257
Span::from("Tab").bold(),
6358
Span::from(" section"),
6459
Span::from(" | "),
65-
Span::from("focus:").bold(),
66-
Span::from(format!(" {focus}")),
67-
Span::from(" | "),
6860
Span::from("s").bold(),
6961
Span::from(" scan"),
7062
Span::from(" | "),
7163
Span::from("Enter").bold(),
72-
Span::from(" connect/disconnect"),
64+
Span::from(" dis/connect"),
7365
Span::from(" | "),
7466
Span::from("i").bold(),
7567
Span::from(" details"),

src/ui/layout.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ratatui::{
99
};
1010

1111
pub fn render(app: &mut App, frame: &mut Frame) {
12-
const MIN_W: u16 = 90;
12+
const MIN_W: u16 = 118;
1313
const MIN_H: u16 = 26;
1414

1515
let area = frame.area();

0 commit comments

Comments
 (0)