use crate::render::{Bounds, RenderContext};
use super::{Msg, Widget, WidgetStyle, draw_text_pill, glyph_label, measure_text_pill};
const SYSTEM_GLYPH: &str = "\u{f2db}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SystemStats {
cpu: u8,
mem: u8,
}
impl SystemStats {
pub fn new(cpu: f64, mem: f64) -> Self {
Self {
cpu: normalize_percent(cpu),
mem: normalize_percent(mem),
}
}
pub fn cpu(self) -> u8 {
self.cpu
}
pub fn mem(self) -> u8 {
self.mem
}
pub fn label(self) -> String {
format!("CPU {}% MEM {}%", self.cpu, self.mem)
}
}
fn normalize_percent(value: f64) -> u8 {
if value.is_nan() {
0
} else {
value.clamp(0.0, 100.0).round() as u8
}
}
pub struct SystemWidget {
bounds: Bounds,
state: Option<SystemStats>,
style: WidgetStyle,
}
impl SystemWidget {
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.map(SystemStats::label).unwrap_or_default()
}
fn display_text(&self) -> String {
match self.state {
Some(stats) => glyph_label(self.style.glyph(SYSTEM_GLYPH), &stats.label()),
None => String::new(),
}
}
}
impl Widget for SystemWidget {
fn update(&mut self, msg: &Msg) -> bool {
match msg {
Msg::System(next) => {
if self.state == Some(*next) {
return false;
}
self.state = Some(*next);
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 stats(cpu: f64, mem: f64) -> Msg {
Msg::System(SystemStats::new(cpu, mem))
}
#[test]
fn new_rounds_each_reading_to_whole_percent() {
let s = SystemStats::new(12.4, 46.6);
assert_eq!(s.cpu(), 12);
assert_eq!(s.mem(), 47);
}
#[test]
fn new_clamps_out_of_range_readings() {
let s = SystemStats::new(137.0, -5.0);
assert_eq!(s.cpu(), 100);
assert_eq!(s.mem(), 0);
}
#[test]
fn new_treats_nan_readings_as_zero() {
let s = SystemStats::new(f64::NAN, f64::NAN);
assert_eq!(s.cpu(), 0);
assert_eq!(s.mem(), 0);
}
#[test]
fn label_joins_cpu_and_memory() {
assert_eq!(SystemStats::new(12.0, 47.0).label(), "CPU 12% MEM 47%");
assert_eq!(SystemStats::new(0.0, 100.0).label(), "CPU 0% MEM 100%");
}
#[test]
fn first_sample_changes_state_and_sets_label() {
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
assert_eq!(widget.label(), "");
assert!(widget.update(&stats(12.0, 47.0)));
assert_eq!(widget.label(), "CPU 12% MEM 47%");
}
#[test]
fn identical_sample_is_not_a_visible_change() {
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&stats(12.0, 47.0)));
assert!(!widget.update(&stats(12.2, 47.3)));
assert_eq!(widget.label(), "CPU 12% MEM 47%");
}
#[test]
fn a_new_cpu_reading_is_a_visible_change() {
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&stats(12.0, 47.0)));
assert!(widget.update(&stats(30.0, 47.0)));
assert_eq!(widget.label(), "CPU 30% MEM 47%");
}
#[test]
fn a_new_memory_reading_is_a_visible_change() {
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
assert!(widget.update(&stats(12.0, 47.0)));
assert!(widget.update(&stats(12.0, 63.0)));
assert_eq!(widget.label(), "CPU 12% MEM 63%");
}
#[test]
fn unrelated_message_is_ignored() {
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
widget.update(&stats(12.0, 47.0));
let tick = Msg::Tick(Local.with_ymd_and_hms(2026, 6, 27, 8, 0, 0).unwrap());
assert!(!widget.update(&tick));
assert_eq!(widget.label(), "CPU 12% MEM 47%");
}
#[test]
fn set_bounds_repositions_the_widget() {
let mut widget = SystemWidget::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_prefixes_the_system_glyph() {
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
assert_eq!(widget.display_text(), "");
widget.update(&stats(12.0, 47.0));
assert_eq!(
widget.display_text(),
format!("{SYSTEM_GLYPH} CPU 12% MEM 47%")
);
}
#[test]
fn no_sample_measures_zero_a_sampled_one_reserves_a_slot() {
let mut ctx = RenderContext::new(320, 32);
let mut widget = SystemWidget::new(Bounds::new(0, 0, 320, 32));
assert_eq!(widget.measure(&mut ctx, 32), 0);
widget.update(&stats(12.0, 47.0));
assert!(widget.measure(&mut ctx, 32) > 0);
}
}