use super::{Widget, WidgetAlignment, WidgetClickResult, WidgetContext};
use crate::rendering::{Cell, Theme, VideoBuffer};
use crate::window::manager::FocusState;
use crossterm::style::Color;
use std::cell::RefCell;
use std::time::{Duration, Instant};
#[derive(Clone)]
pub struct NetworkInfo {
pub interface: String,
pub is_connected: bool,
pub is_wifi: bool,
pub signal_strength: Option<u8>, }
struct NetworkCache {
info: Option<NetworkInfo>,
interface: String,
last_update: Instant,
}
thread_local! {
static NETWORK_CACHE: RefCell<NetworkCache> = RefCell::new(NetworkCache {
info: None,
interface: String::new(),
last_update: Instant::now() - Duration::from_secs(2), });
}
pub fn get_network_info(interface: &str) -> Option<NetworkInfo> {
NETWORK_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let interface_changed = cache.interface != interface;
if interface_changed || cache.last_update.elapsed() >= Duration::from_secs(1) {
cache.info = fetch_network_info(interface);
if interface_changed {
cache.interface = interface.to_string();
}
cache.last_update = Instant::now();
}
cache.info.clone()
})
}
fn fetch_network_info(interface: &str) -> Option<NetworkInfo> {
if interface.is_empty() {
return None;
}
let is_wifi = is_wifi_interface(interface);
let is_connected = check_interface_connected(interface);
let signal_strength = if is_wifi && is_connected {
get_wifi_signal_strength(interface)
} else {
None
};
Some(NetworkInfo {
interface: interface.to_string(),
is_connected,
is_wifi,
signal_strength,
})
}
fn is_wifi_interface(interface: &str) -> bool {
interface.starts_with("wlan")
|| interface.starts_with("wlp")
|| interface.starts_with("wifi")
|| interface.starts_with("ath")
|| interface.starts_with("ra")
|| (cfg!(target_os = "macos") && is_macos_wifi_interface(interface))
}
#[cfg(target_os = "macos")]
fn is_macos_wifi_interface(interface: &str) -> bool {
if let Ok(output) = std::process::Command::new("networksetup")
.args(["-listallhardwareports"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
let mut is_wifi_section = false;
for line in stdout.lines() {
if line.contains("Wi-Fi") || line.contains("AirPort") {
is_wifi_section = true;
} else if line.starts_with("Hardware Port:") {
is_wifi_section = false;
} else if is_wifi_section && line.contains("Device:") && line.contains(interface) {
return true;
}
}
}
false
}
#[cfg(not(target_os = "macos"))]
fn is_macos_wifi_interface(_interface: &str) -> bool {
false
}
#[cfg(target_os = "linux")]
fn check_interface_connected(interface: &str) -> bool {
let operstate_path = format!("/sys/class/net/{}/operstate", interface);
if let Ok(state) = std::fs::read_to_string(&operstate_path) {
state.trim() == "up"
} else {
false
}
}
#[cfg(target_os = "macos")]
fn check_interface_connected(interface: &str) -> bool {
if let Ok(output) = std::process::Command::new("ifconfig")
.arg(interface)
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.contains("status: active") || (stdout.contains("UP") && stdout.contains("inet "))
} else {
false
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn check_interface_connected(_interface: &str) -> bool {
false
}
#[cfg(target_os = "linux")]
fn get_wifi_signal_strength(interface: &str) -> Option<u8> {
let contents = std::fs::read_to_string("/proc/net/wireless").ok()?;
for line in contents.lines().skip(2) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let iface = parts[0].trim_end_matches(':');
if iface == interface {
if let Some(quality_str) = parts.get(2) {
if let Ok(quality) = quality_str.trim_end_matches('.').parse::<f32>() {
let percentage = ((quality / 70.0) * 100.0).min(100.0) as u8;
return Some(percentage);
}
}
}
}
None
}
#[cfg(target_os = "macos")]
fn get_wifi_signal_strength(_interface: &str) -> Option<u8> {
let airport_path =
"/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport";
if let Ok(output) = std::process::Command::new(airport_path).arg("-I").output() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("agrCtlRSSI:") {
if let Some(rssi_str) = line.split(':').nth(1) {
if let Ok(rssi) = rssi_str.trim().parse::<i32>() {
let percentage = ((rssi + 90) * 100 / 60).clamp(0, 100) as u8;
return Some(percentage);
}
}
}
}
}
None
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn get_wifi_signal_strength(_interface: &str) -> Option<u8> {
None
}
use crate::rendering::Charset;
fn get_signal_bars(strength: u8, charset: &Charset) -> [char; 4] {
let s1 = charset.network_signal_1;
let s2 = charset.network_signal_2;
let s3 = charset.network_signal_3;
let s4 = charset.network_signal_4;
if strength >= 80 {
[s1, s2, s3, s4] } else if strength >= 60 {
[s1, s2, s3, ' '] } else if strength >= 40 {
[s1, s2, ' ', ' '] } else if strength >= 20 {
[s1, ' ', ' ', ' '] } else {
[' ', ' ', ' ', ' '] }
}
fn get_signal_color(strength: u8) -> Color {
if strength >= 60 {
Color::Green
} else if strength >= 40 {
Color::Yellow
} else {
Color::Red
}
}
pub struct NetworkWidget {
hovered: bool,
cached_info: Option<NetworkInfo>,
interface: String,
enabled: bool,
}
impl NetworkWidget {
pub fn new() -> Self {
Self {
hovered: false,
cached_info: None,
interface: String::new(),
enabled: false,
}
}
pub fn configure(&mut self, interface: &str, enabled: bool) {
self.interface = interface.to_string();
self.enabled = enabled;
}
#[allow(dead_code)]
pub fn is_hovered(&self) -> bool {
self.hovered
}
}
impl Default for NetworkWidget {
fn default() -> Self {
Self::new()
}
}
impl Widget for NetworkWidget {
fn width(&self) -> u16 {
if !self.enabled || self.interface.is_empty() {
return 0;
}
if let Some(ref info) = self.cached_info {
let status_len = if info.is_wifi && info.is_connected && info.signal_strength.is_some()
{
4 } else {
1 };
(info.interface.len() + status_len + 4) as u16
} else {
0
}
}
fn render(&self, buffer: &mut VideoBuffer, x: u16, theme: &Theme, ctx: &WidgetContext) {
let info = match &self.cached_info {
Some(info) => info,
None => return,
};
let bg_color = match ctx.focus {
FocusState::Desktop | FocusState::Topbar => theme.topbar_bg_focused,
FocusState::Window(_) => theme.topbar_bg_unfocused,
};
let fg_color = theme.window_border_unfocused_fg;
let charset = ctx.charset;
let mut current_x = x;
buffer.set(current_x, 0, Cell::new_unchecked(' ', fg_color, bg_color));
current_x += 1;
for ch in info.interface.chars() {
buffer.set(current_x, 0, Cell::new_unchecked(ch, fg_color, bg_color));
current_x += 1;
}
buffer.set(current_x, 0, Cell::new_unchecked(' ', fg_color, bg_color));
current_x += 1;
if info.is_connected {
if info.is_wifi {
if let Some(strength) = info.signal_strength {
let bars = get_signal_bars(strength, charset);
let color = get_signal_color(strength);
for ch in bars {
buffer.set(current_x, 0, Cell::new_unchecked(ch, color, bg_color));
current_x += 1;
}
} else {
buffer.set(
current_x,
0,
Cell::new_unchecked(charset.network_connected, Color::Green, bg_color),
);
current_x += 1;
}
} else {
buffer.set(
current_x,
0,
Cell::new_unchecked(charset.network_connected, Color::Green, bg_color),
);
current_x += 1;
}
} else {
buffer.set(
current_x,
0,
Cell::new_unchecked(charset.network_disconnected, Color::Red, bg_color),
);
current_x += 1;
}
buffer.set(current_x, 0, Cell::new_unchecked(' ', fg_color, bg_color));
current_x += 1;
buffer.set(current_x, 0, Cell::new_unchecked(' ', fg_color, bg_color));
}
fn is_visible(&self, _ctx: &WidgetContext) -> bool {
self.enabled && !self.interface.is_empty() && self.cached_info.is_some()
}
fn contains(&self, point_x: u16, point_y: u16, widget_x: u16) -> bool {
point_y == 0 && point_x >= widget_x && point_x < widget_x + self.width()
}
fn update_hover(&mut self, mouse_x: u16, mouse_y: u16, widget_x: u16) {
self.hovered = self.contains(mouse_x, mouse_y, widget_x);
}
fn handle_click(&mut self, _mouse_x: u16, _mouse_y: u16, _widget_x: u16) -> WidgetClickResult {
WidgetClickResult::NotHandled
}
fn reset_state(&mut self) {
self.hovered = false;
}
fn update(&mut self, _ctx: &WidgetContext) {
if self.enabled && !self.interface.is_empty() {
self.cached_info = get_network_info(&self.interface);
} else {
self.cached_info = None;
}
}
fn alignment(&self) -> WidgetAlignment {
WidgetAlignment::Right
}
}