use crate::render::{Bounds, RenderContext};
use super::{Msg, Widget, WidgetStyle, draw_text_pill, glyph_label, measure_text_pill};
const WIRELESS_GLYPH: &str = "\u{f1eb}"; const WIRED_GLYPH: &str = "\u{f0e8}"; const DISCONNECTED_GLYPH: &str = "\u{f00d}"; const UNKNOWN_GLYPH: &str = "\u{f128}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkState {
Disconnected,
Wired,
Wireless,
Unknown,
}
impl NetworkState {
pub fn label(self) -> &'static str {
match self {
NetworkState::Disconnected => "disconnected",
NetworkState::Wired => "wired",
NetworkState::Wireless => "wifi",
NetworkState::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Network {
state: NetworkState,
ssid: Option<String>,
}
impl Network {
pub fn new(state: NetworkState, ssid: Option<&str>) -> Self {
let ssid = if state == NetworkState::Wireless {
ssid.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
} else {
None
};
Self { state, ssid }
}
pub fn state(&self) -> NetworkState {
self.state
}
pub fn ssid(&self) -> Option<&str> {
self.ssid.as_deref()
}
pub fn label(&self) -> String {
match (self.state, self.ssid.as_deref()) {
(NetworkState::Wireless, Some(ssid)) => format!("wifi {ssid}"),
_ => self.state.label().to_string(),
}
}
}
fn default_glyph(state: NetworkState) -> &'static str {
match state {
NetworkState::Wireless => WIRELESS_GLYPH,
NetworkState::Wired => WIRED_GLYPH,
NetworkState::Disconnected => DISCONNECTED_GLYPH,
NetworkState::Unknown => UNKNOWN_GLYPH,
}
}
pub struct NetworkWidget {
bounds: Bounds,
state: Option<Network>,
style: WidgetStyle,
}
impl NetworkWidget {
pub fn new(bounds: Bounds) -> Self {
Self {
bounds,
state: None,
style: WidgetStyle::default(),
}
}
pub fn with_style(mut self, style: WidgetStyle) -> Self {
self.style = style;
self
}
pub fn label(&self) -> String {
self.state.as_ref().map(Network::label).unwrap_or_default()
}
fn display_text(&self) -> String {
match &self.state {
Some(network) => glyph_label(
self.style.glyph(default_glyph(network.state())),
&network.label(),
),
None => String::new(),
}
}
}
impl Widget for NetworkWidget {
fn update(&mut self, msg: &Msg) -> bool {
match msg {
Msg::Network(next) => {
if &self.state == next {
return false;
}
self.state = next.clone();
true
}
_ => false,
}
}
fn draw(&self, ctx: &mut RenderContext) {
draw_text_pill(
ctx,
&self.style,
self.bounds,
&self.display_text(),
self.style.base_colors(),
);
}
fn measure(&self, ctx: &mut RenderContext, _height: u32) -> u32 {
measure_text_pill(ctx, &self.style, &self.display_text())
}
fn bounds(&self) -> Bounds {
self.bounds
}
fn set_bounds(&mut self, bounds: Bounds) {
self.bounds = bounds;
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Local, TimeZone};
fn network(state: NetworkState, ssid: Option<&str>) -> Msg {
Msg::Network(Some(Network::new(state, ssid)))
}
#[test]
fn state_labels_are_unambiguous_words() {
assert_eq!(NetworkState::Disconnected.label(), "disconnected");
assert_eq!(NetworkState::Wired.label(), "wired");
assert_eq!(NetworkState::Wireless.label(), "wifi");
assert_eq!(NetworkState::Unknown.label(), "unknown");
}
#[test]
fn ssid_is_kept_only_on_a_wireless_link() {
assert_eq!(
Network::new(NetworkState::Wired, Some("home-net")).ssid(),
None
);
assert_eq!(
Network::new(NetworkState::Disconnected, Some("home-net")).ssid(),
None
);
assert_eq!(
Network::new(NetworkState::Unknown, Some("home-net")).ssid(),
None
);
assert_eq!(
Network::new(NetworkState::Wireless, Some("home-net")).ssid(),
Some("home-net")
);
}
#[test]
fn ssid_is_trimmed_and_blank_names_are_dropped() {
assert_eq!(
Network::new(NetworkState::Wireless, Some(" home-net ")).ssid(),
Some("home-net")
);
assert_eq!(Network::new(NetworkState::Wireless, Some("")).ssid(), None);
assert_eq!(
Network::new(NetworkState::Wireless, Some(" ")).ssid(),
None
);
assert_eq!(Network::new(NetworkState::Wireless, None).ssid(), None);
}
#[test]
fn label_shows_ssid_on_wifi_and_the_bare_state_otherwise() {
assert_eq!(
Network::new(NetworkState::Wireless, Some("home-net")).label(),
"wifi home-net"
);
assert_eq!(Network::new(NetworkState::Wireless, None).label(), "wifi");
assert_eq!(Network::new(NetworkState::Wired, None).label(), "wired");
assert_eq!(
Network::new(NetworkState::Disconnected, None).label(),
"disconnected"
);
assert_eq!(Network::new(NetworkState::Unknown, None).label(), "unknown");
}
#[test]
fn first_reading_changes_state_and_sets_label() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert_eq!(widget.label(), "");
assert!(widget.update(&network(NetworkState::Wireless, Some("home-net"))));
assert_eq!(widget.label(), "wifi home-net");
}
#[test]
fn identical_reading_is_not_a_visible_change() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&network(NetworkState::Wireless, Some("home-net"))));
assert!(!widget.update(&network(NetworkState::Wireless, Some(" home-net "))));
assert_eq!(widget.label(), "wifi home-net");
}
#[test]
fn a_new_ssid_is_a_visible_change() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&network(NetworkState::Wireless, Some("home-net"))));
assert!(widget.update(&network(NetworkState::Wireless, Some("cafe-net"))));
assert_eq!(widget.label(), "wifi cafe-net");
}
#[test]
fn a_new_state_is_a_visible_change() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&network(NetworkState::Wireless, Some("home-net"))));
assert!(widget.update(&network(NetworkState::Wired, None)));
assert_eq!(widget.label(), "wired");
}
#[test]
fn network_going_absent_is_a_visible_change_then_blank() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&network(NetworkState::Wireless, Some("home-net"))));
assert!(widget.update(&Msg::Network(None)));
assert_eq!(widget.label(), "");
}
#[test]
fn absent_reading_before_any_network_is_not_a_change() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert!(!widget.update(&Msg::Network(None)));
assert_eq!(widget.label(), "");
}
#[test]
fn unrelated_message_is_ignored() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
widget.update(&network(NetworkState::Wired, None));
let tick = Msg::Tick(Local.with_ymd_and_hms(2026, 6, 27, 8, 0, 0).unwrap());
assert!(!widget.update(&tick));
assert_eq!(widget.label(), "wired");
}
#[test]
fn set_bounds_repositions_the_widget() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 1, 1));
widget.set_bounds(Bounds::new(10, 0, 200, 32));
assert_eq!(widget.bounds(), Bounds::new(10, 0, 200, 32));
}
#[test]
fn display_text_uses_a_state_driven_glyph() {
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert_eq!(widget.display_text(), "");
widget.update(&network(NetworkState::Wireless, Some("home-net")));
assert_eq!(
widget.display_text(),
format!("{WIRELESS_GLYPH} wifi home-net")
);
widget.update(&network(NetworkState::Wired, None));
assert_eq!(widget.display_text(), format!("{WIRED_GLYPH} wired"));
widget.update(&network(NetworkState::Disconnected, None));
assert_eq!(
widget.display_text(),
format!("{DISCONNECTED_GLYPH} disconnected")
);
}
#[test]
fn an_absent_network_measures_zero_a_present_one_reserves_a_slot() {
let mut ctx = RenderContext::new(320, 32);
let mut widget = NetworkWidget::new(Bounds::new(0, 0, 320, 32));
assert_eq!(widget.measure(&mut ctx, 32), 0);
widget.update(&network(NetworkState::Wired, None));
assert!(widget.measure(&mut ctx, 32) > 0);
}
}