1use chrono::{DateTime, Utc};
11use ratatui::buffer::Buffer;
12use ratatui::layout::Rect;
13use ratatui::style::{Modifier, Style};
14use ratatui::text::{Line, Span};
15use ratatui::widgets::Widget;
16use zero_engine_client::{Source, Stat};
17
18use crate::theme::Theme;
19
20#[derive(Debug)]
22pub struct StatWidget<'a, T: std::fmt::Display> {
23 stat: &'a Stat<T>,
24 now: DateTime<Utc>,
25 stale_after: chrono::Duration,
26 theme: Theme,
27 show_source: bool,
28}
29
30impl<'a, T: std::fmt::Display> StatWidget<'a, T> {
31 pub fn new(stat: &'a Stat<T>) -> Self {
32 Self {
33 stat,
34 now: Utc::now(),
35 stale_after: chrono::Duration::seconds(5),
36 theme: Theme::default(),
37 show_source: false,
38 }
39 }
40
41 #[must_use]
42 pub fn stale_after(mut self, d: chrono::Duration) -> Self {
43 self.stale_after = d;
44 self
45 }
46
47 #[must_use]
48 pub fn theme(mut self, t: Theme) -> Self {
49 self.theme = t;
50 self
51 }
52
53 #[must_use]
54 pub fn show_source(mut self, yes: bool) -> Self {
55 self.show_source = yes;
56 self
57 }
58}
59
60impl<T: std::fmt::Display> Widget for StatWidget<'_, T> {
61 fn render(self, area: Rect, buf: &mut Buffer) {
62 let stale = self.stat.is_stale(self.now, self.stale_after);
63 let value_style = if stale {
64 Style::default()
65 .fg(self.theme.caution)
66 .add_modifier(Modifier::DIM)
67 } else {
68 Style::default().fg(self.theme.primary)
69 };
70
71 let mut spans: Vec<Span<'_>> = vec![Span::styled(self.stat.value.to_string(), value_style)];
72
73 if let Some(n) = self.stat.n {
74 spans.push(Span::styled(
75 format!(" n={n}"),
76 Style::default().fg(self.theme.metadata),
77 ));
78 }
79
80 if self.show_source {
81 let source = match self.stat.source {
82 Source::Http => "http",
83 Source::Ws => "ws",
84 Source::Mcp => "mcp",
85 Source::Derived => "derived",
86 Source::Mock => "mock",
87 };
88 spans.push(Span::styled(
89 format!(" [{source}]"),
90 Style::default().fg(self.theme.metadata),
91 ));
92 }
93
94 Line::from(spans).render(area, buf);
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::StatWidget;
101 use chrono::Utc;
102 use ratatui::buffer::Buffer;
103 use ratatui::layout::Rect;
104 use ratatui::widgets::Widget;
105 use zero_engine_client::{Source, Stat};
106
107 #[test]
108 fn renders_value_and_n() {
109 let stat: Stat<f64> = Stat::new(58.4, Source::Ws).with_n(312);
110 let area = Rect::new(0, 0, 40, 1);
111 let mut buf = Buffer::empty(area);
112 StatWidget::new(&stat)
113 .stale_after(chrono::Duration::seconds(60))
114 .render(area, &mut buf);
115 let rendered: String = (0..area.width)
116 .map(|x| buf[(x, 0)].symbol().to_string())
117 .collect();
118 assert!(rendered.contains("58.4"));
119 assert!(rendered.contains("n=312"));
120 }
121
122 #[test]
123 fn stale_badge_triggers_on_age() {
124 let stale_ts = Utc::now() - chrono::Duration::seconds(30);
125 let stat: Stat<f64> = Stat::new(1.0, Source::Ws).with_as_of(stale_ts);
126 assert!(stat.is_stale(Utc::now(), chrono::Duration::seconds(5)));
127 }
128}