1use owo_colors::OwoColorize;
7use sysinfo::Networks;
8
9pub fn detect_active_interface_and_local_ip() -> (Option<String>, Option<String>) {
11 let local_ip = std::net::UdpSocket::bind("0.0.0.0:0")
12 .ok()
13 .and_then(|socket| {
14 socket.connect("8.8.8.8:53").ok()?;
15 socket.local_addr().ok().map(|addr| addr.ip().to_string())
16 });
17
18 let active_interface = {
19 #[cfg(target_os = "linux")]
20 {
21 let native_iface = std::fs::read_to_string("/proc/net/route")
22 .ok()
23 .and_then(|content| parse_proc_net_route(&content));
24
25 native_iface.or_else(|| {
26 std::process::Command::new("ip")
27 .args(["route", "show", "default"])
28 .output()
29 .ok()
30 .and_then(|o| String::from_utf8(o.stdout).ok())
31 .and_then(|s| {
32 s.split_whitespace()
33 .position(|w| w == "dev")
34 .and_then(|i| s.split_whitespace().nth(i + 1))
35 .map(|s| s.to_string())
36 })
37 })
38 }
39 #[cfg(target_os = "macos")]
40 {
41 std::process::Command::new("route")
42 .args(["-n", "get", "default"])
43 .output()
44 .ok()
45 .and_then(|o| String::from_utf8(o.stdout).ok())
46 .and_then(|s| {
47 s.lines()
48 .find(|l| l.contains("interface:"))
49 .and_then(|l| l.split_whitespace().last())
50 .map(|s| s.to_string())
51 })
52 }
53 #[cfg(target_os = "windows")]
54 {
55 std::process::Command::new("powershell")
56 .args(["-Command", "Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object -First 1 -ExpandProperty InterfaceAlias"])
57 .output()
58 .ok()
59 .and_then(|o| String::from_utf8(o.stdout).ok())
60 .map(|s| s.trim().to_string())
61 .filter(|s| !s.is_empty())
62 }
63 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
64 {
65 None
66 }
67 };
68
69 (local_ip, active_interface)
70}
71
72pub fn detect_public_ip() -> Option<String> {
74 std::process::Command::new("curl")
75 .args(["-s", "--max-time", "2", "https://api.ipify.org"])
76 .output()
77 .ok()
78 .and_then(|o| String::from_utf8(o.stdout).ok())
79 .map(|s| s.trim().to_string())
80 .filter(|s| !s.is_empty())
81}
82
83pub fn detect_networks(active_interface: Option<&str>, local_ip: Option<&str>) -> Vec<String> {
85 Networks::new_with_refreshed_list()
86 .iter()
87 .map(|(name, data)| {
88 let rx = format_bytes(data.total_received());
89 let tx = format_bytes(data.total_transmitted());
90 let is_up = data.operational_state() == sysinfo::InterfaceOperationalState::Up
91 || data.total_received() > 0
92 || data.total_transmitted() > 0;
93 let status = if is_up {
94 "Up".green().to_string()
95 } else {
96 "Down".red().to_string()
97 };
98
99 let mut ipv4_addresses = Vec::new();
100 let mut ipv6_addresses = Vec::new();
101
102 if is_up {
103 for ip_net in data.ip_networks() {
104 let ip = ip_net.addr;
105 let name_lower = name.to_lowercase();
106 let is_loopback_iface =
107 name_lower.starts_with("lo") || name_lower.contains("loopback");
108 if ip.is_loopback() && !is_loopback_iface {
109 continue;
110 }
111 match ip {
112 std::net::IpAddr::V4(v4) => {
113 ipv4_addresses.push(v4.to_string());
114 }
115 std::net::IpAddr::V6(v6) => {
116 if !v6.is_unicast_link_local() {
117 ipv6_addresses.push(v6.to_string());
118 }
119 }
120 }
121 }
122
123 if ipv4_addresses.is_empty() && ipv6_addresses.is_empty() {
125 if let (Some(active), Some(ip)) = (active_interface, local_ip) {
126 if name == active {
127 ipv4_addresses.push(ip.to_string());
128 }
129 }
130 }
131 }
132
133 let ip_str = if !ipv4_addresses.is_empty() || !ipv6_addresses.is_empty() {
134 let mut combined = Vec::new();
135 if !ipv4_addresses.is_empty() {
136 combined.push(ipv4_addresses.join(", "));
137 }
138 if !ipv6_addresses.is_empty() {
139 combined.push(ipv6_addresses.join(", "));
140 }
141 format!(" ({})", combined.join(", "))
142 } else {
143 String::new()
144 };
145
146 format!("{}{} [{}] RX: {} TX: {}", name, ip_str, status, rx, tx)
147 })
148 .collect()
149}
150
151pub fn format_bytes(bytes: u64) -> String {
153 const KB: u64 = 1024;
154 const MB: u64 = KB * 1024;
155 const GB: u64 = MB * 1024;
156
157 if bytes >= GB {
158 format!("{:.1} GB", bytes as f64 / GB as f64)
159 } else if bytes >= MB {
160 format!("{:.1} MB", bytes as f64 / MB as f64)
161 } else if bytes >= KB {
162 format!("{:.1} KB", bytes as f64 / KB as f64)
163 } else {
164 format!("{} B", bytes)
165 }
166}
167
168pub fn lookup_pci_vendor(vendor_id: &str) -> Option<String> {
172 let vendor_id = vendor_id.trim_start_matches("0x").to_lowercase();
173 let paths = ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"];
174 for path in &paths {
175 if let Ok(content) = std::fs::read_to_string(path) {
176 for line in content.lines() {
177 if line.starts_with('#') || line.is_empty() {
178 continue;
179 }
180 if !line.starts_with('\t') {
181 let parts: Vec<&str> = line.split_whitespace().collect();
182 if parts.len() >= 2 && parts[0].to_lowercase() == vendor_id {
183 let name = line.strip_prefix(parts[0]).unwrap().trim();
184 return Some(name.to_string());
185 }
186 }
187 }
188 }
189 }
190 None
191}
192
193pub fn detect_wifi() -> Option<String> {
195 #[cfg(target_os = "linux")]
196 {
197 let mut wifi_interface = None;
198 if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
199 for entry in entries.filter_map(|e| e.ok()) {
200 let path = entry.path();
201 if path.join("wireless").exists() || path.join("phy80211").exists() {
202 wifi_interface = Some(entry.file_name().to_string_lossy().to_string());
203 break;
204 }
205 }
206 }
207
208 if let Some(ref iface) = wifi_interface {
209 if let Ok(output) = std::process::Command::new("iw")
210 .args(["dev", iface, "link"])
211 .output()
212 {
213 if let Ok(stdout) = String::from_utf8(output.stdout) {
214 let (ssid, links) = parse_iw_link_output(&stdout);
215 if let Some(s) = ssid {
216 let card_model = get_wifi_card_model(iface);
217 let prefix = if let Some(m) = card_model {
218 format!("{} [{}] - ", m, iface)
219 } else {
220 format!("[{}] - ", iface)
221 };
222
223 if !links.is_empty() {
224 let mut link_strs = Vec::new();
225 for link in links {
226 let freq_str = link.freq.map(|f| {
227 let ghz_mhz = if f >= 1000.0 {
228 format!("{:.1} GHz", f / 1000.0)
229 } else {
230 format!("{} MHz", f)
231 };
232 if let Some(ch) = freq_to_channel(f) {
233 format!("{} ch{}", ghz_mhz, ch)
234 } else {
235 ghz_mhz
236 }
237 });
238
239 let mut rx_tx = Vec::new();
240 if let Some(rx) = link.rx_rate {
241 if rx != "0"
242 && !rx.starts_with("0 ")
243 && rx != "0 Mbps"
244 && rx != "0 MBit/s"
245 {
246 rx_tx.push(format!("↓{}", clean_rate(&rx)));
247 }
248 }
249 if let Some(tx) = link.tx_rate {
250 if tx != "0"
251 && !tx.starts_with("0 ")
252 && tx != "0 Mbps"
253 && tx != "0 MBit/s"
254 {
255 rx_tx.push(format!("↑{}", clean_rate(&tx)));
256 }
257 }
258
259 match (freq_str, rx_tx.is_empty()) {
260 (Some(f), false) => {
261 link_strs.push(format!("{} [{}]", f, rx_tx.join(" ")))
262 }
263 (Some(f), true) => link_strs.push(f),
264 (None, false) => link_strs.push(rx_tx.join(" ")),
265 _ => {}
266 }
267 }
268 if !link_strs.is_empty() {
269 return Some(format!(
270 "{}{}{} ({})",
271 prefix,
272 s,
273 "",
274 link_strs.join(", ")
275 ));
276 } else {
277 return Some(format!("{}{}", prefix, s));
278 }
279 }
280 return Some(format!("{}{}", prefix, s));
281 }
282 }
283 }
284 }
285
286 if let Ok(output) = std::process::Command::new("nmcli")
288 .args(["-t", "-f", "active,ssid,rate", "dev", "wifi"])
289 .output()
290 {
291 if let Ok(stdout) = String::from_utf8(output.stdout) {
292 for line in stdout.lines() {
293 let line = line.trim();
294 if let Some(rest) = line.strip_prefix("yes:") {
295 if let Some(colon_idx) = rest.rfind(':') {
296 let ssid = &rest[..colon_idx];
297 let rate = rest[colon_idx + 1..].trim();
298 if !ssid.is_empty() {
299 if !rate.is_empty()
300 && rate != "0"
301 && !rate.starts_with("0 ")
302 && rate != "0 Mbit/s"
303 && rate != "0 Mbps"
304 {
305 return Some(format!("{} ({})", ssid, clean_rate(rate)));
306 } else {
307 return Some(ssid.to_string());
308 }
309 }
310 } else if !rest.is_empty() {
311 return Some(rest.to_string());
312 }
313 }
314 }
315 }
316 }
317
318 if let Ok(output) = std::process::Command::new("iwgetid").arg("-r").output() {
320 if let Ok(stdout) = String::from_utf8(output.stdout) {
321 let ssid = stdout.trim();
322 if !ssid.is_empty() {
323 return Some(ssid.to_string());
324 }
325 }
326 }
327 None
328 }
329
330 #[cfg(target_os = "macos")]
331 {
332 if let Ok(output) = std::process::Command::new("/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport")
333 .arg("-I")
334 .output()
335 {
336 if let Ok(stdout) = String::from_utf8(output.stdout) {
337 return parse_airport_output(&stdout);
338 }
339 }
340 None
341 }
342
343 #[cfg(target_os = "windows")]
344 {
345 if let Ok(output) = std::process::Command::new("netsh")
346 .args(["wlan", "show", "interfaces"])
347 .output()
348 {
349 if let Ok(stdout) = String::from_utf8(output.stdout) {
350 return parse_netsh_output(&stdout);
351 }
352 }
353 None
354 }
355
356 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
357 {
358 None
359 }
360}
361
362#[cfg(any(target_os = "linux", test))]
363pub fn parse_proc_net_route(content: &str) -> Option<String> {
364 for line in content.lines().skip(1) {
365 let parts: Vec<&str> = line.split_whitespace().collect();
366 if parts.len() >= 8 {
367 let dest = parts[1];
368 let mask = parts[7];
369 if dest == "00000000" && mask == "00000000" {
370 return Some(parts[0].to_string());
371 }
372 }
373 }
374 None
375}
376
377#[allow(
378 clippy::manual_is_multiple_of,
379 clippy::manual_range_contains,
380 dead_code
381)]
382fn freq_to_channel(freq_mhz: f64) -> Option<u32> {
383 let freq = freq_mhz.round() as u32;
384 if freq >= 2412 && freq <= 2472 {
385 Some((freq - 2407) / 5)
386 } else if freq == 2484 {
387 Some(14)
388 } else if freq >= 5160 && freq <= 5885 {
389 if (freq - 5000) % 5 == 0 {
390 Some((freq - 5000) / 5)
391 } else {
392 None
393 }
394 } else if freq >= 5955 && freq <= 7115 {
395 if (freq - 5950) % 5 == 0 {
396 Some((freq - 5950) / 5)
397 } else {
398 None
399 }
400 } else {
401 None
402 }
403}
404
405#[allow(dead_code)]
406fn get_wifi_card_model(iface: &str) -> Option<String> {
407 let vendor = std::fs::read_to_string(format!("/sys/class/net/{}/device/vendor", iface)).ok()?;
408 let device = std::fs::read_to_string(format!("/sys/class/net/{}/device/device", iface)).ok()?;
409 let vendor_clean = vendor.trim().trim_start_matches("0x").to_lowercase();
410 let device_clean = device.trim().trim_start_matches("0x").to_lowercase();
411
412 let vendor_name = lookup_pci_vendor(&vendor_clean);
413 let model_name = crate::gpu::lookup_pci_device(&vendor_clean, &device_clean);
414
415 match (vendor_name, model_name) {
416 (Some(v), Some(m)) => {
417 let v_clean = v.replace(", Inc.", "").replace(" Corporation", "");
418 if m.to_lowercase().contains(&v_clean.to_lowercase())
419 || m.to_lowercase().contains(
420 &v_clean
421 .split_whitespace()
422 .next()
423 .unwrap_or("")
424 .to_lowercase(),
425 )
426 {
427 Some(m)
428 } else {
429 Some(format!("{} {}", v_clean, m))
430 }
431 }
432 (None, Some(m)) => Some(m),
433 _ => None,
434 }
435}
436
437#[allow(dead_code)]
438fn clean_rate(rate: &str) -> String {
439 rate.replace("MBit/s", "Mbps")
440 .replace("GBit/s", "Gbps")
441 .replace("Bit/s", "bps")
442}
443
444#[derive(Debug, Clone)]
445pub struct WifiLink {
446 pub freq: Option<f64>,
447 pub rx_rate: Option<String>,
448 pub tx_rate: Option<String>,
449}
450
451#[allow(dead_code)]
452pub fn parse_iw_link_output(stdout: &str) -> (Option<String>, Vec<WifiLink>) {
453 let mut ssid = None;
454 let mut links = Vec::new();
455 let mut current_link = None;
456
457 for line in stdout.lines() {
458 let trimmed = line.trim();
459 if trimmed.starts_with("Connected to") || trimmed.starts_with("link") {
460 if let Some(link) = current_link.take() {
461 links.push(link);
462 }
463 current_link = Some(WifiLink {
464 freq: None,
465 rx_rate: None,
466 tx_rate: None,
467 });
468 } else if trimmed.starts_with("SSID:") {
469 ssid = Some(trimmed.strip_prefix("SSID:").unwrap().trim().to_string());
470 } else if trimmed.starts_with("freq:") {
471 if let Some(ref mut link) = current_link {
472 let freq_str = trimmed.strip_prefix("freq:").unwrap().trim();
473 link.freq = freq_str.parse::<f64>().ok();
474 }
475 } else if trimmed.starts_with("rx bitrate:") {
476 if let Some(ref mut link) = current_link {
477 let rx_str = trimmed.strip_prefix("rx bitrate:").unwrap().trim();
478 let rate = rx_str
479 .split_whitespace()
480 .take(2)
481 .collect::<Vec<&str>>()
482 .join(" ");
483 link.rx_rate = Some(rate);
484 }
485 } else if trimmed.starts_with("tx bitrate:") {
486 if let Some(ref mut link) = current_link {
487 let tx_str = trimmed.strip_prefix("tx bitrate:").unwrap().trim();
488 let rate = tx_str
489 .split_whitespace()
490 .take(2)
491 .collect::<Vec<&str>>()
492 .join(" ");
493 link.tx_rate = Some(rate);
494 }
495 }
496 }
497 if let Some(link) = current_link {
498 links.push(link);
499 }
500 (ssid, links)
501}
502
503#[allow(dead_code)]
504pub fn parse_airport_output(stdout: &str) -> Option<String> {
505 let mut ssid = None;
506 let mut rate = None;
507 for line in stdout.lines() {
508 let trimmed = line.trim();
509 if trimmed.starts_with("SSID:") {
510 let val = trimmed.strip_prefix("SSID:").unwrap().trim().to_string();
511 if !val.is_empty() {
512 ssid = Some(val);
513 }
514 } else if trimmed.starts_with("lastTxRate:") {
515 let val = trimmed
516 .strip_prefix("lastTxRate:")
517 .unwrap()
518 .trim()
519 .to_string();
520 if !val.is_empty() {
521 rate = Some(val);
522 }
523 }
524 }
525 match (ssid, rate) {
526 (Some(s), Some(r)) => {
527 if r != "0" && !r.starts_with("0 ") && r != "0 Mbps" && r != "0 Mbit/s" {
528 Some(format!("{} (↑{} Mbps)", s, r))
529 } else {
530 Some(s)
531 }
532 }
533 (Some(s), None) => Some(s),
534 _ => None,
535 }
536}
537
538#[allow(dead_code)]
539pub fn parse_netsh_output(stdout: &str) -> Option<String> {
540 let mut ssid = None;
541 let mut rx = None;
542 let mut tx = None;
543 let mut band = None;
544 for line in stdout.lines() {
545 let trimmed = line.trim();
546 if trimmed.starts_with("SSID") {
547 if let Some(idx) = trimmed.find(':') {
548 let val = trimmed[idx + 1..].trim().to_string();
549 if !val.is_empty() {
550 ssid = Some(val);
551 }
552 }
553 } else if trimmed.starts_with("Receive rate (Mbps)") {
554 if let Some(idx) = trimmed.find(':') {
555 let val = trimmed[idx + 1..].trim().to_string();
556 if !val.is_empty() {
557 rx = Some(val);
558 }
559 }
560 } else if trimmed.starts_with("Transmit rate (Mbps)") {
561 if let Some(idx) = trimmed.find(':') {
562 let val = trimmed[idx + 1..].trim().to_string();
563 if !val.is_empty() {
564 tx = Some(val);
565 }
566 }
567 } else if trimmed.starts_with("Band") {
568 if let Some(idx) = trimmed.find(':') {
569 let val = trimmed[idx + 1..].trim().to_string();
570 if !val.is_empty() {
571 band = Some(val);
572 }
573 }
574 }
575 }
576 if let Some(s) = ssid {
577 let mut rate_strs = Vec::new();
578 if let Some(rx_val) = rx {
579 if rx_val != "0" {
580 rate_strs.push(format!("↓{} Mbps", rx_val));
581 }
582 }
583 if let Some(tx_val) = tx {
584 if tx_val != "0" {
585 rate_strs.push(format!("↑{} Mbps", tx_val));
586 }
587 }
588 let info = match (band, rate_strs.is_empty()) {
589 (Some(b), false) => format!("{} [{}]", b, rate_strs.join(" ")),
590 (Some(b), true) => b,
591 (None, false) => rate_strs.join(" "),
592 _ => String::new(),
593 };
594 if !info.is_empty() {
595 Some(format!("{} ({})", s, info))
596 } else {
597 Some(s)
598 }
599 } else {
600 None
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn test_format_bytes() {
610 assert_eq!(format_bytes(500), "500 B");
611 assert_eq!(format_bytes(1024), "1.0 KB");
612 assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
613 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
614 assert_eq!(format_bytes(1536), "1.5 KB");
615 }
616
617 #[test]
618 fn test_parse_proc_net_route() {
619 let sample =
620 "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n\
621 wlan0\t0000A8C0\t00000000\t0001\t0\t0\t600\t0000FFFF\t0\t0\t0\n\
622 wlan0\t00000000\t0100A8C0\t0003\t0\t0\t600\t00000000\t0\t0\t0\n";
623 assert_eq!(parse_proc_net_route(sample), Some("wlan0".to_string()));
624
625 let sample_no_default =
626 "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n\
627 wlan0\t0000A8C0\t00000000\t0001\t0\t0\t600\t0000FFFF\t0\t0\t0\n";
628 assert_eq!(parse_proc_net_route(sample_no_default), None);
629 }
630
631 #[test]
632 fn test_parse_airport_output() {
633 let sample = " agrCtlRSSI: -45\n lastTxRate: 866\n SSID: MyHomeWiFi\n";
634 assert_eq!(
635 parse_airport_output(sample),
636 Some("MyHomeWiFi (↑866 Mbps)".to_string())
637 );
638
639 let sample_no_rate = " agrCtlRSSI: -45\n SSID: GuestNetwork\n";
640 assert_eq!(
641 parse_airport_output(sample_no_rate),
642 Some("GuestNetwork".to_string())
643 );
644 }
645
646 #[test]
647 fn test_parse_netsh_output() {
648 let sample = " Name : Wi-Fi\n State : connected\n SSID : Office_Wi-Fi\n Receive rate (Mbps) : 433\n Transmit rate (Mbps) : 866\n Band : 5 GHz\n";
649 assert_eq!(
650 parse_netsh_output(sample),
651 Some("Office_Wi-Fi (5 GHz [↓433 Mbps ↑866 Mbps])".to_string())
652 );
653 }
654
655 #[test]
656 fn test_parse_iw_link_output() {
657 let sample = "Connected to 84:78:48:dc:97:23 (on wlp2s0)\n SSID: OfficeNet\n freq: 6135.0\n rx bitrate: 6.0 MBit/s\n tx bitrate: 864.6 MBit/s 160MHz HE-MCS 4\n";
658 let (ssid, links) = parse_iw_link_output(sample);
659 assert_eq!(ssid, Some("OfficeNet".to_string()));
660 assert_eq!(links.len(), 1);
661 assert_eq!(links[0].freq, Some(6135.0));
662 assert_eq!(links[0].rx_rate, Some("6.0 MBit/s".to_string()));
663 assert_eq!(links[0].tx_rate, Some("864.6 MBit/s".to_string()));
664
665 let sample_mlo = "Connected to aa:bb:cc:dd:ee:ff (on wlan0)\n SSID: HomeWiFi\n freq: 5180.0\n rx bitrate: 866.0 MBit/s\n tx bitrate: 866.0 MBit/s\nConnected to aa:bb:cc:dd:ee:01 (on wlan0)\n freq: 6135.0\n rx bitrate: 1200.0 MBit/s\n tx bitrate: 1200.0 MBit/s\n";
667 let (ssid_mlo, links_mlo) = parse_iw_link_output(sample_mlo);
668 assert_eq!(ssid_mlo, Some("HomeWiFi".to_string()));
669 assert_eq!(links_mlo.len(), 2);
670 assert_eq!(links_mlo[0].freq, Some(5180.0));
671 assert_eq!(links_mlo[1].freq, Some(6135.0));
672 }
673}