Skip to main content

presentar_terminal/widgets/
info_dense.rs

1//! Information-Dense Widgets (Tufte-inspired)
2//!
3//! Widgets designed to maximize data-ink ratio and answer user questions directly.
4//! These prioritize information density over decoration.
5//!
6//! # Design Principles (Tufte)
7//! - Maximize data-ink ratio
8//! - Show comparisons and context
9//! - Avoid chart junk
10//! - Use small multiples
11//! - Show outliers, not repetitive data
12
13use presentar_core::{
14    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
15    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
16};
17use std::any::Any;
18use std::time::Duration;
19
20// =============================================================================
21// Color Helpers
22// =============================================================================
23
24fn color_for_cpu_percent(pct: f32) -> Color {
25    if pct > 50.0 {
26        Color {
27            r: 1.0,
28            g: 0.4,
29            b: 0.4,
30            a: 1.0,
31        } // Red
32    } else if pct > 10.0 {
33        Color {
34            r: 1.0,
35            g: 0.8,
36            b: 0.4,
37            a: 1.0,
38        } // Yellow
39    } else if pct > 1.0 {
40        Color {
41            r: 0.6,
42            g: 0.9,
43            b: 0.6,
44            a: 1.0,
45        } // Green
46    } else {
47        Color {
48            r: 0.5,
49            g: 0.5,
50            b: 0.6,
51            a: 1.0,
52        } // Dim
53    }
54}
55
56fn color_for_status(level: HealthLevel) -> Color {
57    match level {
58        HealthLevel::Critical => Color {
59            r: 1.0,
60            g: 0.2,
61            b: 0.2,
62            a: 1.0,
63        },
64        HealthLevel::High => Color {
65            r: 1.0,
66            g: 0.5,
67            b: 0.3,
68            a: 1.0,
69        },
70        HealthLevel::Moderate => Color {
71            r: 1.0,
72            g: 0.8,
73            b: 0.4,
74            a: 1.0,
75        },
76        HealthLevel::Ok => Color {
77            r: 0.5,
78            g: 0.9,
79            b: 0.5,
80            a: 1.0,
81        },
82    }
83}
84
85/// Health level for system metrics (renamed to avoid conflict with `dataframe::HealthLevel`)
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum HealthLevel {
88    Critical,
89    High,
90    Moderate,
91    Ok,
92}
93
94impl HealthLevel {
95    pub fn as_str(&self) -> &'static str {
96        match self {
97            Self::Critical => "CRITICAL",
98            Self::High => "HIGH",
99            Self::Moderate => "MODERATE",
100            Self::Ok => "OK",
101        }
102    }
103}
104
105// =============================================================================
106// 1. TopProcessesTable - Information-dense process list
107// =============================================================================
108
109/// Process entry for the table
110#[derive(Debug, Clone)]
111pub struct CpuConsumer {
112    pub pid: u32,
113    pub cpu_percent: f32,
114    pub memory_bytes: u64,
115    pub name: String,
116}
117
118impl CpuConsumer {
119    pub fn new(pid: u32, cpu_percent: f32, memory_bytes: u64, name: impl Into<String>) -> Self {
120        Self {
121            pid,
122            cpu_percent,
123            memory_bytes,
124            name: name.into(),
125        }
126    }
127
128    fn memory_display(&self) -> String {
129        if self.memory_bytes > 1_073_741_824 {
130            format!("{:.1}G", self.memory_bytes as f64 / 1_073_741_824.0)
131        } else if self.memory_bytes > 1_048_576 {
132            format!("{:.0}M", self.memory_bytes as f64 / 1_048_576.0)
133        } else {
134            format!("{:.0}K", self.memory_bytes as f64 / 1024.0)
135        }
136    }
137}
138
139/// Information-dense process table showing top CPU consumers.
140/// Answers: "What's using my CPU?"
141#[derive(Debug, Clone)]
142pub struct TopProcessesTable {
143    /// All processes (will be sorted by CPU)
144    processes: Vec<CpuConsumer>,
145    /// Total CPU percentage for header
146    total_cpu: f32,
147    /// Maximum processes to show
148    max_display: usize,
149    /// Cached bounds
150    bounds: Rect,
151}
152
153impl Default for TopProcessesTable {
154    fn default() -> Self {
155        Self::new(vec![], 0.0)
156    }
157}
158
159impl TopProcessesTable {
160    /// Create a new top processes table
161    #[must_use]
162    pub fn new(mut processes: Vec<CpuConsumer>, total_cpu: f32) -> Self {
163        // Sort by CPU descending
164        processes.sort_by(|a, b| {
165            b.cpu_percent
166                .partial_cmp(&a.cpu_percent)
167                .unwrap_or(std::cmp::Ordering::Equal)
168        });
169        Self {
170            processes,
171            total_cpu,
172            max_display: 10,
173            bounds: Rect::default(),
174        }
175    }
176
177    /// Set maximum processes to display
178    #[must_use]
179    pub fn with_max_display(mut self, max: usize) -> Self {
180        self.max_display = max;
181        self
182    }
183
184    /// Update processes
185    pub fn set_processes(&mut self, mut processes: Vec<CpuConsumer>, total_cpu: f32) {
186        processes.sort_by(|a, b| {
187            b.cpu_percent
188                .partial_cmp(&a.cpu_percent)
189                .unwrap_or(std::cmp::Ordering::Equal)
190        });
191        self.processes = processes;
192        self.total_cpu = total_cpu;
193    }
194}
195
196impl Brick for TopProcessesTable {
197    fn brick_name(&self) -> &'static str {
198        "top_processes_table"
199    }
200    fn assertions(&self) -> &[BrickAssertion] {
201        static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
202        A
203    }
204    fn budget(&self) -> BrickBudget {
205        BrickBudget::uniform(16)
206    }
207    fn verify(&self) -> BrickVerification {
208        BrickVerification {
209            passed: self.assertions().to_vec(),
210            failed: vec![],
211            verification_time: Duration::from_micros(10),
212        }
213    }
214    fn to_html(&self) -> String {
215        String::new()
216    }
217    fn to_css(&self) -> String {
218        String::new()
219    }
220}
221
222impl Widget for TopProcessesTable {
223    fn type_id(&self) -> TypeId {
224        TypeId::of::<Self>()
225    }
226
227    fn measure(&self, constraints: Constraints) -> Size {
228        let height = (self.max_display + 3) as f32; // header + column header + processes + summary
229        constraints.constrain(Size::new(constraints.max_width, height))
230    }
231
232    fn layout(&mut self, bounds: Rect) -> LayoutResult {
233        self.bounds = bounds;
234        // Adjust max_display based on available height
235        self.max_display = ((bounds.height - 3.0) as usize).max(3);
236        LayoutResult {
237            size: Size::new(bounds.width, bounds.height),
238        }
239    }
240
241    fn paint(&self, canvas: &mut dyn Canvas) {
242        let x = self.bounds.x;
243        let mut y = self.bounds.y;
244        let w = self.bounds.width as usize;
245
246        let header_style = TextStyle {
247            color: Color {
248                r: 0.6,
249                g: 0.8,
250                b: 1.0,
251                a: 1.0,
252            },
253            ..Default::default()
254        };
255        let dim_style = TextStyle {
256            color: Color {
257                r: 0.5,
258                g: 0.5,
259                b: 0.6,
260                a: 1.0,
261            },
262            ..Default::default()
263        };
264
265        // Header
266        let header = format!("TOP CPU CONSUMERS ({:.0}% total)", self.total_cpu);
267        canvas.draw_text(
268            &header[..header.len().min(w)],
269            Point::new(x, y),
270            &header_style,
271        );
272        y += 1.0;
273
274        // Column headers
275        let col_header = format!("{:>7} {:>6} {:>6}  {:<}", "PID", "CPU%", "MEM", "COMMAND");
276        canvas.draw_text(
277            &col_header[..col_header.len().min(w)],
278            Point::new(x, y),
279            &dim_style,
280        );
281        y += 1.0;
282
283        // Process rows
284        let mut other_cpu = 0.0_f32;
285        let mut other_count = 0_usize;
286
287        for (i, proc) in self.processes.iter().enumerate() {
288            if i < self.max_display && y < self.bounds.y + self.bounds.height - 1.0 {
289                let color = color_for_cpu_percent(proc.cpu_percent);
290                let max_name = w.saturating_sub(22);
291                let name = if proc.name.len() > max_name {
292                    format!("{}...", &proc.name[..max_name.saturating_sub(3)])
293                } else {
294                    proc.name.clone()
295                };
296
297                let line = format!(
298                    "{:>7} {:>5.1}% {:>6}  {}",
299                    proc.pid,
300                    proc.cpu_percent,
301                    proc.memory_display(),
302                    name
303                );
304                canvas.draw_text(
305                    &line[..line.len().min(w)],
306                    Point::new(x, y),
307                    &TextStyle {
308                        color,
309                        ..Default::default()
310                    },
311                );
312                y += 1.0;
313            } else {
314                other_cpu += proc.cpu_percent;
315                other_count += 1;
316            }
317        }
318
319        // Summary of other processes
320        if other_count > 0 && y < self.bounds.y + self.bounds.height {
321            let other_line = format!("  [{other_count} other processes totaling {other_cpu:.1}%]");
322            canvas.draw_text(
323                &other_line[..other_line.len().min(w)],
324                Point::new(x, y),
325                &dim_style,
326            );
327        }
328    }
329
330    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
331        None
332    }
333    fn children(&self) -> &[Box<dyn Widget>] {
334        &[]
335    }
336    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
337        &mut []
338    }
339}
340
341// =============================================================================
342// 2. CoreUtilizationHistogram - Shows distribution, not 48 identical values
343// =============================================================================
344
345/// Histogram showing core utilization distribution
346#[derive(Debug, Clone)]
347pub struct CoreUtilizationHistogram {
348    /// Core percentages (0-100)
349    core_percentages: Vec<f64>,
350    /// Cached bounds
351    bounds: Rect,
352}
353
354impl Default for CoreUtilizationHistogram {
355    fn default() -> Self {
356        Self::new(vec![])
357    }
358}
359
360impl CoreUtilizationHistogram {
361    #[must_use]
362    pub fn new(core_percentages: Vec<f64>) -> Self {
363        Self {
364            core_percentages,
365            bounds: Rect::default(),
366        }
367    }
368
369    pub fn set_percentages(&mut self, percentages: Vec<f64>) {
370        self.core_percentages = percentages;
371    }
372
373    fn bucket_counts(&self) -> (usize, usize, usize, usize, usize) {
374        let mut b100 = 0; // 95-100%
375        let mut bhigh = 0; // 70-95%
376        let mut bmed = 0; // 30-70%
377        let mut blow = 0; // 1-30%
378        let mut bidle = 0; // <1%
379
380        for &pct in &self.core_percentages {
381            if pct >= 95.0 {
382                b100 += 1;
383            } else if pct >= 70.0 {
384                bhigh += 1;
385            } else if pct >= 30.0 {
386                bmed += 1;
387            } else if pct >= 1.0 {
388                blow += 1;
389            } else {
390                bidle += 1;
391            }
392        }
393        (b100, bhigh, bmed, blow, bidle)
394    }
395}
396
397impl Brick for CoreUtilizationHistogram {
398    fn brick_name(&self) -> &'static str {
399        "core_utilization_histogram"
400    }
401    fn assertions(&self) -> &[BrickAssertion] {
402        static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
403        A
404    }
405    fn budget(&self) -> BrickBudget {
406        BrickBudget::uniform(16)
407    }
408    fn verify(&self) -> BrickVerification {
409        BrickVerification {
410            passed: self.assertions().to_vec(),
411            failed: vec![],
412            verification_time: Duration::from_micros(10),
413        }
414    }
415    fn to_html(&self) -> String {
416        String::new()
417    }
418    fn to_css(&self) -> String {
419        String::new()
420    }
421}
422
423impl Widget for CoreUtilizationHistogram {
424    fn type_id(&self) -> TypeId {
425        TypeId::of::<Self>()
426    }
427
428    fn measure(&self, constraints: Constraints) -> Size {
429        constraints.constrain(Size::new(constraints.max_width, 6.0)) // header + 5 buckets max
430    }
431
432    fn layout(&mut self, bounds: Rect) -> LayoutResult {
433        self.bounds = bounds;
434        LayoutResult {
435            size: Size::new(bounds.width, bounds.height),
436        }
437    }
438
439    fn paint(&self, canvas: &mut dyn Canvas) {
440        let x = self.bounds.x;
441        let mut y = self.bounds.y;
442        let w = self.bounds.width as usize;
443
444        let header_style = TextStyle {
445            color: Color {
446                r: 0.6,
447                g: 0.8,
448                b: 1.0,
449                a: 1.0,
450            },
451            ..Default::default()
452        };
453        let dim_style = TextStyle {
454            color: Color {
455                r: 0.5,
456                g: 0.5,
457                b: 0.6,
458                a: 1.0,
459            },
460            ..Default::default()
461        };
462        let bright_style = TextStyle {
463            color: Color {
464                r: 1.0,
465                g: 1.0,
466                b: 1.0,
467                a: 1.0,
468            },
469            ..Default::default()
470        };
471
472        canvas.draw_text("CORE UTILIZATION", Point::new(x, y), &header_style);
473        y += 1.0;
474
475        let (b100, bhigh, bmed, blow, bidle) = self.bucket_counts();
476        let total = self.core_percentages.len();
477        let bar_max = w.saturating_sub(20);
478
479        let draw_bar =
480            |canvas: &mut dyn Canvas, y: f32, label: &str, count: usize, color: Color| {
481                if count == 0 {
482                    return;
483                }
484                let bar_w = if total > 0 {
485                    (count * bar_max) / total
486                } else {
487                    0
488                };
489                let bar: String = "█".repeat(bar_w);
490                let pad: String = "░".repeat(bar_max - bar_w);
491
492                canvas.draw_text(
493                    label,
494                    Point::new(x, y),
495                    &TextStyle {
496                        color,
497                        ..Default::default()
498                    },
499                );
500                canvas.draw_text(
501                    &bar,
502                    Point::new(x + 10.0, y),
503                    &TextStyle {
504                        color,
505                        ..Default::default()
506                    },
507                );
508                canvas.draw_text(&pad, Point::new(x + 10.0 + bar_w as f32, y), &dim_style);
509                canvas.draw_text(
510                    &format!(" x{count}"),
511                    Point::new(x + 10.0 + bar_max as f32, y),
512                    &bright_style,
513                );
514            };
515
516        if b100 > 0 {
517            draw_bar(
518                canvas,
519                y,
520                "   100%",
521                b100,
522                Color {
523                    r: 1.0,
524                    g: 0.3,
525                    b: 0.3,
526                    a: 1.0,
527                },
528            );
529            y += 1.0;
530        }
531        if bhigh > 0 {
532            draw_bar(
533                canvas,
534                y,
535                " 70-95%",
536                bhigh,
537                Color {
538                    r: 1.0,
539                    g: 0.6,
540                    b: 0.3,
541                    a: 1.0,
542                },
543            );
544            y += 1.0;
545        }
546        if bmed > 0 {
547            draw_bar(
548                canvas,
549                y,
550                " 30-70%",
551                bmed,
552                Color {
553                    r: 1.0,
554                    g: 1.0,
555                    b: 0.4,
556                    a: 1.0,
557                },
558            );
559            y += 1.0;
560        }
561        if blow > 0 {
562            draw_bar(
563                canvas,
564                y,
565                "  1-30%",
566                blow,
567                Color {
568                    r: 0.5,
569                    g: 0.9,
570                    b: 0.5,
571                    a: 1.0,
572                },
573            );
574            y += 1.0;
575        }
576        if bidle > 0 {
577            draw_bar(
578                canvas,
579                y,
580                "   idle",
581                bidle,
582                Color {
583                    r: 0.4,
584                    g: 0.4,
585                    b: 0.5,
586                    a: 1.0,
587                },
588            );
589        }
590    }
591
592    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
593        None
594    }
595    fn children(&self) -> &[Box<dyn Widget>] {
596        &[]
597    }
598    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
599        &mut []
600    }
601}
602
603// =============================================================================
604// 3. TrendSparkline - Sparkline with context (min/max/avg/current)
605// =============================================================================
606
607/// Sparkline with statistical context
608#[derive(Debug, Clone)]
609pub struct TrendSparkline {
610    /// History values (0-1 normalized or 0-100 percentage)
611    history: Vec<f64>,
612    /// Title for the widget
613    title: String,
614    /// Whether values are percentages (0-100) or normalized (0-1)
615    is_percentage: bool,
616    /// Cached bounds
617    bounds: Rect,
618}
619
620impl Default for TrendSparkline {
621    fn default() -> Self {
622        Self::new("TREND", vec![])
623    }
624}
625
626impl TrendSparkline {
627    #[must_use]
628    pub fn new(title: impl Into<String>, history: Vec<f64>) -> Self {
629        Self {
630            history,
631            title: title.into(),
632            is_percentage: true,
633            bounds: Rect::default(),
634        }
635    }
636
637    /// Mark values as normalized (0-1) instead of percentage (0-100)
638    #[must_use]
639    pub fn normalized(mut self) -> Self {
640        self.is_percentage = false;
641        self
642    }
643
644    pub fn set_history(&mut self, history: Vec<f64>) {
645        self.history = history;
646    }
647
648    pub fn push(&mut self, value: f64) {
649        self.history.push(value);
650        if self.history.len() > 120 {
651            self.history.remove(0);
652        }
653    }
654
655    fn stats(&self) -> (f64, f64, f64, f64) {
656        if self.history.is_empty() {
657            return (0.0, 0.0, 0.0, 0.0);
658        }
659        let mult = if self.is_percentage { 1.0 } else { 100.0 };
660        let current = self.history.last().copied().unwrap_or(0.0) * mult;
661        let min = self.history.iter().copied().fold(f64::MAX, f64::min) * mult;
662        let max = self.history.iter().copied().fold(f64::MIN, f64::max) * mult;
663        let avg = self.history.iter().sum::<f64>() / self.history.len() as f64 * mult;
664        (current, min, max, avg)
665    }
666}
667
668impl Brick for TrendSparkline {
669    fn brick_name(&self) -> &'static str {
670        "trend_sparkline"
671    }
672    fn assertions(&self) -> &[BrickAssertion] {
673        static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
674        A
675    }
676    fn budget(&self) -> BrickBudget {
677        BrickBudget::uniform(16)
678    }
679    fn verify(&self) -> BrickVerification {
680        BrickVerification {
681            passed: self.assertions().to_vec(),
682            failed: vec![],
683            verification_time: Duration::from_micros(10),
684        }
685    }
686    fn to_html(&self) -> String {
687        String::new()
688    }
689    fn to_css(&self) -> String {
690        String::new()
691    }
692}
693
694impl Widget for TrendSparkline {
695    fn type_id(&self) -> TypeId {
696        TypeId::of::<Self>()
697    }
698
699    fn measure(&self, constraints: Constraints) -> Size {
700        constraints.constrain(Size::new(constraints.max_width, 4.0)) // header + sparkline + 2 stat lines
701    }
702
703    fn layout(&mut self, bounds: Rect) -> LayoutResult {
704        self.bounds = bounds;
705        LayoutResult {
706            size: Size::new(bounds.width, bounds.height),
707        }
708    }
709
710    fn paint(&self, canvas: &mut dyn Canvas) {
711        let x = self.bounds.x;
712        let mut y = self.bounds.y;
713        let w = self.bounds.width as usize;
714
715        let header_style = TextStyle {
716            color: Color {
717                r: 0.6,
718                g: 0.8,
719                b: 1.0,
720                a: 1.0,
721            },
722            ..Default::default()
723        };
724        let dim_style = TextStyle {
725            color: Color {
726                r: 0.5,
727                g: 0.5,
728                b: 0.6,
729                a: 1.0,
730            },
731            ..Default::default()
732        };
733        let bright_style = TextStyle {
734            color: Color {
735                r: 1.0,
736                g: 1.0,
737                b: 1.0,
738                a: 1.0,
739            },
740            ..Default::default()
741        };
742
743        canvas.draw_text(&self.title, Point::new(x, y), &header_style);
744        y += 1.0;
745
746        let (current, min, max, avg) = self.stats();
747
748        // Color based on current level
749        let trend_color = if current > 80.0 {
750            Color {
751                r: 1.0,
752                g: 0.4,
753                b: 0.4,
754                a: 1.0,
755            }
756        } else if current > 50.0 {
757            Color {
758                r: 1.0,
759                g: 0.8,
760                b: 0.4,
761                a: 1.0,
762            }
763        } else {
764            Color {
765                r: 0.5,
766                g: 0.9,
767                b: 0.5,
768                a: 1.0,
769            }
770        };
771
772        // Draw sparkline
773        let mult = if self.is_percentage { 1.0 } else { 100.0 };
774        let chars: String = self
775            .history
776            .iter()
777            .rev()
778            .take(w)
779            .rev()
780            .map(|&v| {
781                let pct = (v * mult).clamp(0.0, 100.0);
782                let idx = ((pct / 100.0) * 7.0).round() as usize;
783                ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][idx.min(7)]
784            })
785            .collect();
786
787        canvas.draw_text(
788            &chars,
789            Point::new(x, y),
790            &TextStyle {
791                color: trend_color,
792                ..Default::default()
793            },
794        );
795        y += 1.0;
796
797        // Stats
798        let now_avg = format!("Now: {current:.0}%  Avg: {avg:.0}%");
799        canvas.draw_text(
800            &now_avg[..now_avg.len().min(w)],
801            Point::new(x, y),
802            &bright_style,
803        );
804        y += 1.0;
805
806        let min_max = format!("Min: {min:.0}%  Max: {max:.0}%");
807        canvas.draw_text(
808            &min_max[..min_max.len().min(w)],
809            Point::new(x, y),
810            &dim_style,
811        );
812    }
813
814    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
815        None
816    }
817    fn children(&self) -> &[Box<dyn Widget>] {
818        &[]
819    }
820    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
821        &mut []
822    }
823}
824
825// =============================================================================
826// 4. SystemStatus - Load and thermal status with contextual coloring
827// =============================================================================
828
829/// System status display (load, thermals)
830#[derive(Debug, Clone)]
831pub struct SystemStatus {
832    /// Load averages (1, 5, 15 minute)
833    load_1m: f64,
834    load_5m: f64,
835    load_15m: f64,
836    /// Number of cores (for per-core load calculation)
837    core_count: usize,
838    /// Thermal data: (`avg_temp`, `max_temp`) in Celsius
839    thermal: Option<(f64, f64)>,
840    /// Cached bounds
841    bounds: Rect,
842}
843
844impl Default for SystemStatus {
845    fn default() -> Self {
846        Self::new(0.0, 0.0, 0.0, 1)
847    }
848}
849
850impl SystemStatus {
851    #[must_use]
852    pub fn new(load_1m: f64, load_5m: f64, load_15m: f64, core_count: usize) -> Self {
853        Self {
854            load_1m,
855            load_5m,
856            load_15m,
857            core_count: core_count.max(1),
858            thermal: None,
859            bounds: Rect::default(),
860        }
861    }
862
863    #[must_use]
864    pub fn with_thermal(mut self, avg_temp: f64, max_temp: f64) -> Self {
865        self.thermal = Some((avg_temp, max_temp));
866        self
867    }
868
869    pub fn set_load(&mut self, l1: f64, l5: f64, l15: f64) {
870        self.load_1m = l1;
871        self.load_5m = l5;
872        self.load_15m = l15;
873    }
874
875    pub fn set_thermal(&mut self, avg: f64, max: f64) {
876        self.thermal = Some((avg, max));
877    }
878
879    /// Get the health level for current load
880    pub fn load_status(&self) -> HealthLevel {
881        let per_core = self.load_1m / self.core_count as f64;
882        if per_core > 1.5 {
883            HealthLevel::Critical
884        } else if per_core > 1.0 {
885            HealthLevel::High
886        } else if per_core > 0.7 {
887            HealthLevel::Moderate
888        } else {
889            HealthLevel::Ok
890        }
891    }
892
893    /// Get the health level for thermal status
894    pub fn thermal_status(&self) -> Option<HealthLevel> {
895        self.thermal.map(|(_, max)| {
896            if max > 90.0 {
897                HealthLevel::Critical
898            } else if max > 80.0 {
899                HealthLevel::High
900            } else if max > 70.0 {
901                HealthLevel::Moderate
902            } else {
903                HealthLevel::Ok
904            }
905        })
906    }
907}
908
909impl Brick for SystemStatus {
910    fn brick_name(&self) -> &'static str {
911        "system_status"
912    }
913    fn assertions(&self) -> &[BrickAssertion] {
914        static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
915        A
916    }
917    fn budget(&self) -> BrickBudget {
918        BrickBudget::uniform(16)
919    }
920    fn verify(&self) -> BrickVerification {
921        BrickVerification {
922            passed: self.assertions().to_vec(),
923            failed: vec![],
924            verification_time: Duration::from_micros(10),
925        }
926    }
927    fn to_html(&self) -> String {
928        String::new()
929    }
930    fn to_css(&self) -> String {
931        String::new()
932    }
933}
934
935impl Widget for SystemStatus {
936    fn type_id(&self) -> TypeId {
937        TypeId::of::<Self>()
938    }
939
940    fn measure(&self, constraints: Constraints) -> Size {
941        let height = if self.thermal.is_some() { 2.0 } else { 1.0 };
942        constraints.constrain(Size::new(constraints.max_width, height))
943    }
944
945    fn layout(&mut self, bounds: Rect) -> LayoutResult {
946        self.bounds = bounds;
947        LayoutResult {
948            size: Size::new(bounds.width, bounds.height),
949        }
950    }
951
952    fn paint(&self, canvas: &mut dyn Canvas) {
953        let x = self.bounds.x;
954        let mut y = self.bounds.y;
955        let w = self.bounds.width as usize;
956
957        // Load line
958        let load_stat = self.load_status();
959        let per_core = self.load_1m / self.core_count as f64;
960        let load_line = format!(
961            "LOAD: {:.2} / {:.2} / {:.2}  ({:.2}/core) - {}",
962            self.load_1m,
963            self.load_5m,
964            self.load_15m,
965            per_core,
966            load_stat.as_str()
967        );
968        canvas.draw_text(
969            &load_line[..load_line.len().min(w)],
970            Point::new(x, y),
971            &TextStyle {
972                color: color_for_status(load_stat),
973                ..Default::default()
974            },
975        );
976
977        // Thermal line (only if data present)
978        if let Some((avg, max)) = self.thermal {
979            y += 1.0;
980            let therm_stat = self.thermal_status().unwrap_or(HealthLevel::Ok);
981            let therm_line = format!(
982                "THERMAL: {:.0}°C avg, {:.0}°C max - {}",
983                avg,
984                max,
985                therm_stat.as_str()
986            );
987            canvas.draw_text(
988                &therm_line[..therm_line.len().min(w)],
989                Point::new(x, y),
990                &TextStyle {
991                    color: color_for_status(therm_stat),
992                    ..Default::default()
993                },
994            );
995        }
996    }
997
998    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
999        None
1000    }
1001    fn children(&self) -> &[Box<dyn Widget>] {
1002        &[]
1003    }
1004    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1005        &mut []
1006    }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012    use crate::direct::{CellBuffer, DirectTerminalCanvas};
1013
1014    // HealthLevel tests
1015    #[test]
1016    fn test_health_level_as_str() {
1017        assert_eq!(HealthLevel::Critical.as_str(), "CRITICAL");
1018        assert_eq!(HealthLevel::High.as_str(), "HIGH");
1019        assert_eq!(HealthLevel::Moderate.as_str(), "MODERATE");
1020        assert_eq!(HealthLevel::Ok.as_str(), "OK");
1021    }
1022
1023    // Color helper tests
1024    #[test]
1025    fn test_color_for_cpu_percent_high() {
1026        let color = color_for_cpu_percent(75.0);
1027        assert!(color.r > 0.9); // Red
1028    }
1029
1030    #[test]
1031    fn test_color_for_cpu_percent_medium() {
1032        let color = color_for_cpu_percent(30.0);
1033        assert!(color.r > 0.9 && color.g > 0.7); // Yellow
1034    }
1035
1036    #[test]
1037    fn test_color_for_cpu_percent_low() {
1038        let color = color_for_cpu_percent(5.0);
1039        assert!(color.g > 0.8); // Green
1040    }
1041
1042    #[test]
1043    fn test_color_for_cpu_percent_idle() {
1044        let color = color_for_cpu_percent(0.5);
1045        assert!(color.r < 0.6 && color.g < 0.6); // Dim
1046    }
1047
1048    #[test]
1049    fn test_color_for_status() {
1050        let critical = color_for_status(HealthLevel::Critical);
1051        assert!(critical.r > 0.9 && critical.g < 0.3);
1052
1053        let high = color_for_status(HealthLevel::High);
1054        assert!(high.r > 0.9);
1055
1056        let moderate = color_for_status(HealthLevel::Moderate);
1057        assert!(moderate.r > 0.9 && moderate.g > 0.7);
1058
1059        let ok = color_for_status(HealthLevel::Ok);
1060        assert!(ok.g > 0.8);
1061    }
1062
1063    // CpuConsumer tests
1064    #[test]
1065    fn test_cpu_consumer_new() {
1066        let proc = CpuConsumer::new(123, 45.5, 1_000_000_000, "firefox");
1067        assert_eq!(proc.pid, 123);
1068        assert!((proc.cpu_percent - 45.5).abs() < 0.01);
1069        assert_eq!(proc.memory_bytes, 1_000_000_000);
1070        assert_eq!(proc.name, "firefox");
1071    }
1072
1073    #[test]
1074    fn test_cpu_consumer_memory_display_gb() {
1075        let proc = CpuConsumer::new(1, 10.0, 2_000_000_000, "test");
1076        let display = proc.memory_display();
1077        assert!(display.contains('G'));
1078    }
1079
1080    #[test]
1081    fn test_cpu_consumer_memory_display_mb() {
1082        let proc = CpuConsumer::new(1, 10.0, 500_000_000, "test");
1083        let display = proc.memory_display();
1084        assert!(display.contains('M'));
1085    }
1086
1087    #[test]
1088    fn test_cpu_consumer_memory_display_kb() {
1089        let proc = CpuConsumer::new(1, 10.0, 500_000, "test");
1090        let display = proc.memory_display();
1091        assert!(display.contains('K'));
1092    }
1093
1094    // TopProcessesTable tests
1095    #[test]
1096    fn test_top_processes_table() {
1097        let procs = vec![
1098            CpuConsumer::new(123, 45.0, 1_000_000_000, "firefox"),
1099            CpuConsumer::new(456, 23.0, 500_000_000, "chrome"),
1100        ];
1101        let table = TopProcessesTable::new(procs, 68.0);
1102        assert!(table.verify().is_valid());
1103    }
1104
1105    #[test]
1106    fn test_top_processes_table_default() {
1107        let table = TopProcessesTable::default();
1108        assert!(table.processes.is_empty());
1109    }
1110
1111    #[test]
1112    fn test_top_processes_table_with_max_display() {
1113        let table = TopProcessesTable::new(vec![], 0.0).with_max_display(5);
1114        assert_eq!(table.max_display, 5);
1115    }
1116
1117    #[test]
1118    fn test_top_processes_table_set_processes() {
1119        let mut table = TopProcessesTable::new(vec![], 0.0);
1120        table.set_processes(
1121            vec![
1122                CpuConsumer::new(1, 20.0, 100, "a"),
1123                CpuConsumer::new(2, 50.0, 200, "b"),
1124            ],
1125            70.0,
1126        );
1127        assert_eq!(table.processes.len(), 2);
1128        assert_eq!(table.total_cpu, 70.0);
1129        // Should be sorted by CPU descending
1130        assert_eq!(table.processes[0].pid, 2);
1131    }
1132
1133    #[test]
1134    fn test_top_processes_table_sorts_by_cpu() {
1135        let procs = vec![
1136            CpuConsumer::new(1, 10.0, 100, "low"),
1137            CpuConsumer::new(2, 90.0, 100, "high"),
1138            CpuConsumer::new(3, 50.0, 100, "mid"),
1139        ];
1140        let table = TopProcessesTable::new(procs, 150.0);
1141        assert_eq!(table.processes[0].pid, 2); // high
1142        assert_eq!(table.processes[1].pid, 3); // mid
1143        assert_eq!(table.processes[2].pid, 1); // low
1144    }
1145
1146    #[test]
1147    fn test_top_processes_table_brick_name() {
1148        let table = TopProcessesTable::default();
1149        assert_eq!(table.brick_name(), "top_processes_table");
1150    }
1151
1152    #[test]
1153    fn test_top_processes_table_measure() {
1154        let table = TopProcessesTable::default();
1155        let constraints = Constraints::tight(Size::new(80.0, 40.0));
1156        let size = table.measure(constraints);
1157        assert!(size.height > 0.0);
1158    }
1159
1160    #[test]
1161    fn test_top_processes_table_layout() {
1162        let mut table = TopProcessesTable::default();
1163        let bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1164        let result = table.layout(bounds);
1165        assert_eq!(result.size.width, 80.0);
1166        assert_eq!(result.size.height, 20.0);
1167    }
1168
1169    #[test]
1170    fn test_top_processes_table_paint() {
1171        let mut table =
1172            TopProcessesTable::new(vec![CpuConsumer::new(1, 50.0, 1_000_000_000, "test")], 50.0);
1173        table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1174        let mut buffer = CellBuffer::new(80, 20);
1175        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1176        table.paint(&mut canvas);
1177    }
1178
1179    #[test]
1180    fn test_top_processes_table_children() {
1181        let table = TopProcessesTable::default();
1182        assert!(table.children().is_empty());
1183    }
1184
1185    // CoreUtilizationHistogram tests
1186    #[test]
1187    fn test_core_histogram() {
1188        let hist = CoreUtilizationHistogram::new(vec![95.0, 96.0, 50.0, 10.0, 0.5]);
1189        let (b100, _bhigh, bmed, blow, bidle) = hist.bucket_counts();
1190        assert_eq!(b100, 2);
1191        assert_eq!(bmed, 1);
1192        assert_eq!(blow, 1);
1193        assert_eq!(bidle, 1);
1194    }
1195
1196    #[test]
1197    fn test_core_histogram_default() {
1198        let hist = CoreUtilizationHistogram::default();
1199        assert!(hist.core_percentages.is_empty());
1200    }
1201
1202    #[test]
1203    fn test_core_histogram_bucket_counts_empty() {
1204        let hist = CoreUtilizationHistogram::new(vec![]);
1205        let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1206        assert_eq!(b100 + bhigh + bmed + blow + bidle, 0);
1207    }
1208
1209    #[test]
1210    fn test_core_histogram_all_buckets() {
1211        // Test all bucket ranges based on actual thresholds:
1212        // 95-100: b100, 70-95: bhigh, 30-70: bmed, 1-30: blow, <1: bidle
1213        let hist = CoreUtilizationHistogram::new(vec![
1214            97.0, // 95-100: b100
1215            80.0, // 70-95: bhigh
1216            50.0, // 30-70: bmed
1217            15.0, // 1-30: blow
1218            0.5,  // <1: bidle
1219        ]);
1220        let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1221        assert_eq!(b100, 1);
1222        assert_eq!(bhigh, 1);
1223        assert_eq!(bmed, 1);
1224        assert_eq!(blow, 1);
1225        assert_eq!(bidle, 1);
1226    }
1227
1228    #[test]
1229    fn test_core_histogram_brick_name() {
1230        let hist = CoreUtilizationHistogram::default();
1231        assert_eq!(hist.brick_name(), "core_utilization_histogram");
1232    }
1233
1234    #[test]
1235    fn test_core_histogram_measure() {
1236        let hist = CoreUtilizationHistogram::new(vec![50.0; 8]);
1237        let constraints = Constraints::tight(Size::new(40.0, 10.0));
1238        let size = hist.measure(constraints);
1239        assert!(size.width > 0.0);
1240    }
1241
1242    #[test]
1243    fn test_core_histogram_layout() {
1244        let mut hist = CoreUtilizationHistogram::new(vec![50.0; 4]);
1245        let bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1246        let result = hist.layout(bounds);
1247        assert_eq!(result.size.width, 40.0);
1248    }
1249
1250    #[test]
1251    fn test_core_histogram_paint() {
1252        let mut hist = CoreUtilizationHistogram::new(vec![50.0, 75.0, 25.0, 90.0]);
1253        hist.bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1254        let mut buffer = CellBuffer::new(40, 8);
1255        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1256        hist.paint(&mut canvas);
1257    }
1258
1259    #[test]
1260    fn test_core_histogram_children() {
1261        let hist = CoreUtilizationHistogram::default();
1262        assert!(hist.children().is_empty());
1263    }
1264
1265    // TrendSparkline tests
1266    #[test]
1267    fn test_trend_sparkline() {
1268        let mut trend = TrendSparkline::new("CPU", vec![50.0, 60.0, 70.0]);
1269        trend.push(80.0);
1270        let (current, min, max, _avg) = trend.stats();
1271        assert!((current - 80.0).abs() < 0.01);
1272        assert!((min - 50.0).abs() < 0.01);
1273        assert!((max - 80.0).abs() < 0.01);
1274    }
1275
1276    #[test]
1277    fn test_trend_sparkline_default() {
1278        let trend = TrendSparkline::default();
1279        assert!(trend.history.is_empty());
1280    }
1281
1282    #[test]
1283    fn test_trend_sparkline_empty_stats() {
1284        let trend = TrendSparkline::new("Empty", vec![]);
1285        let (current, min, max, avg) = trend.stats();
1286        assert_eq!(current, 0.0);
1287        assert_eq!(min, 0.0);
1288        assert_eq!(max, 0.0);
1289        assert_eq!(avg, 0.0);
1290    }
1291
1292    #[test]
1293    fn test_trend_sparkline_single_value() {
1294        let trend = TrendSparkline::new("Single", vec![42.0]);
1295        let (current, min, max, avg) = trend.stats();
1296        assert_eq!(current, 42.0);
1297        assert_eq!(min, 42.0);
1298        assert_eq!(max, 42.0);
1299        assert_eq!(avg, 42.0);
1300    }
1301
1302    #[test]
1303    fn test_trend_sparkline_push_overflow() {
1304        let mut trend = TrendSparkline::new("Test", vec![1.0; 120]);
1305        for i in 0..10 {
1306            trend.push(i as f64);
1307        }
1308        // Should maintain max length (120)
1309        assert!(trend.history.len() <= 120);
1310    }
1311
1312    #[test]
1313    fn test_trend_sparkline_brick_name() {
1314        let trend = TrendSparkline::default();
1315        assert_eq!(trend.brick_name(), "trend_sparkline");
1316    }
1317
1318    #[test]
1319    fn test_trend_sparkline_measure() {
1320        let trend = TrendSparkline::new("Test", vec![50.0; 10]);
1321        let constraints = Constraints::tight(Size::new(60.0, 5.0));
1322        let size = trend.measure(constraints);
1323        assert!(size.width > 0.0);
1324    }
1325
1326    #[test]
1327    fn test_trend_sparkline_layout() {
1328        let mut trend = TrendSparkline::new("Test", vec![50.0; 10]);
1329        let bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1330        let result = trend.layout(bounds);
1331        assert_eq!(result.size.width, 60.0);
1332    }
1333
1334    #[test]
1335    fn test_trend_sparkline_paint() {
1336        let mut trend = TrendSparkline::new("CPU", vec![30.0, 50.0, 70.0, 90.0]);
1337        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1338        let mut buffer = CellBuffer::new(60, 5);
1339        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1340        trend.paint(&mut canvas);
1341    }
1342
1343    #[test]
1344    fn test_trend_sparkline_children() {
1345        let trend = TrendSparkline::default();
1346        assert!(trend.children().is_empty());
1347    }
1348
1349    // SystemStatus tests
1350    #[test]
1351    fn test_system_status() {
1352        let status = SystemStatus::new(4.0, 3.0, 2.0, 8).with_thermal(65.0, 72.0);
1353        assert_eq!(status.load_status(), HealthLevel::Ok); // 4/8 = 0.5 per core (< 0.7)
1354        assert_eq!(status.thermal_status(), Some(HealthLevel::Moderate)); // 72°C
1355    }
1356
1357    #[test]
1358    fn test_system_status_default() {
1359        let status = SystemStatus::default();
1360        assert!((status.load_1m - 0.0).abs() < 0.01);
1361        assert_eq!(status.core_count, 1);
1362    }
1363
1364    #[test]
1365    fn test_system_status_load_critical() {
1366        // per_core > 1.5 = critical (load = 8.0, cores = 4, per_core = 2.0)
1367        let status = SystemStatus::new(8.0, 6.0, 4.0, 4);
1368        assert_eq!(status.load_status(), HealthLevel::Critical);
1369    }
1370
1371    #[test]
1372    fn test_system_status_load_high() {
1373        // per_core > 1.0 = high (load = 5.0, cores = 4, per_core = 1.25)
1374        let status = SystemStatus::new(5.0, 4.0, 3.0, 4);
1375        assert_eq!(status.load_status(), HealthLevel::High);
1376    }
1377
1378    #[test]
1379    fn test_system_status_load_moderate() {
1380        // per_core > 0.7 = moderate (load = 3.2, cores = 4, per_core = 0.8)
1381        let status = SystemStatus::new(3.2, 3.0, 2.8, 4);
1382        assert_eq!(status.load_status(), HealthLevel::Moderate);
1383    }
1384
1385    #[test]
1386    fn test_system_status_thermal_critical() {
1387        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(95.0, 100.0);
1388        assert_eq!(status.thermal_status(), Some(HealthLevel::Critical));
1389    }
1390
1391    #[test]
1392    fn test_system_status_thermal_high() {
1393        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(80.0, 85.0);
1394        assert_eq!(status.thermal_status(), Some(HealthLevel::High));
1395    }
1396
1397    #[test]
1398    fn test_system_status_thermal_ok() {
1399        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(50.0, 55.0);
1400        assert_eq!(status.thermal_status(), Some(HealthLevel::Ok));
1401    }
1402
1403    #[test]
1404    fn test_system_status_no_thermal() {
1405        let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1406        assert_eq!(status.thermal_status(), None);
1407    }
1408
1409    #[test]
1410    fn test_system_status_brick_name() {
1411        let status = SystemStatus::default();
1412        assert_eq!(status.brick_name(), "system_status");
1413    }
1414
1415    #[test]
1416    fn test_system_status_measure() {
1417        let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1418        let constraints = Constraints::tight(Size::new(80.0, 10.0));
1419        let size = status.measure(constraints);
1420        assert!(size.width > 0.0);
1421    }
1422
1423    #[test]
1424    fn test_system_status_layout() {
1425        let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1426        let bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1427        let result = status.layout(bounds);
1428        assert_eq!(result.size.width, 80.0);
1429    }
1430
1431    #[test]
1432    fn test_system_status_paint() {
1433        let mut status = SystemStatus::new(2.0, 1.5, 1.0, 4).with_thermal(60.0, 65.0);
1434        status.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1435        let mut buffer = CellBuffer::new(80, 10);
1436        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1437        status.paint(&mut canvas);
1438    }
1439
1440    #[test]
1441    fn test_system_status_children() {
1442        let status = SystemStatus::default();
1443        assert!(status.children().is_empty());
1444    }
1445
1446    // =========================================================================
1447    // Additional coverage tests
1448    // =========================================================================
1449
1450    // TopProcessesTable additional tests
1451    #[test]
1452    fn test_top_processes_table_type_id() {
1453        let table = TopProcessesTable::default();
1454        let id = Widget::type_id(&table);
1455        assert_eq!(id, TypeId::of::<TopProcessesTable>());
1456    }
1457
1458    #[test]
1459    fn test_top_processes_table_event() {
1460        let mut table = TopProcessesTable::default();
1461        let result = table.event(&Event::FocusIn);
1462        assert!(result.is_none());
1463    }
1464
1465    #[test]
1466    fn test_top_processes_table_children_mut() {
1467        let mut table = TopProcessesTable::default();
1468        assert!(table.children_mut().is_empty());
1469    }
1470
1471    #[test]
1472    fn test_top_processes_table_assertions() {
1473        let table = TopProcessesTable::default();
1474        assert!(!table.assertions().is_empty());
1475    }
1476
1477    #[test]
1478    fn test_top_processes_table_budget() {
1479        let table = TopProcessesTable::default();
1480        let budget = table.budget();
1481        assert!(budget.total_ms > 0);
1482    }
1483
1484    #[test]
1485    fn test_top_processes_table_to_html() {
1486        let table = TopProcessesTable::default();
1487        assert!(table.to_html().is_empty());
1488    }
1489
1490    #[test]
1491    fn test_top_processes_table_to_css() {
1492        let table = TopProcessesTable::default();
1493        assert!(table.to_css().is_empty());
1494    }
1495
1496    #[test]
1497    fn test_top_processes_table_paint_many_processes() {
1498        // Test with more processes than max_display
1499        let procs: Vec<CpuConsumer> = (0..20)
1500            .map(|i| CpuConsumer::new(i, i as f32 * 5.0, 1_000_000 * i as u64, format!("proc{i}")))
1501            .collect();
1502        let mut table = TopProcessesTable::new(procs, 190.0).with_max_display(5);
1503        table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1504        let mut buffer = CellBuffer::new(80, 10);
1505        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1506        table.paint(&mut canvas);
1507    }
1508
1509    #[test]
1510    fn test_top_processes_table_paint_long_name() {
1511        let proc = CpuConsumer::new(
1512            123,
1513            50.0,
1514            1_000_000_000,
1515            "this_is_a_very_long_process_name_that_should_be_truncated",
1516        );
1517        let mut table = TopProcessesTable::new(vec![proc], 50.0);
1518        table.bounds = Rect::new(0.0, 0.0, 50.0, 10.0);
1519        let mut buffer = CellBuffer::new(50, 10);
1520        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1521        table.paint(&mut canvas);
1522    }
1523
1524    #[test]
1525    fn test_top_processes_table_clone() {
1526        let table = TopProcessesTable::new(vec![CpuConsumer::new(1, 10.0, 100, "test")], 10.0);
1527        let cloned = table;
1528        assert_eq!(cloned.processes.len(), 1);
1529    }
1530
1531    #[test]
1532    fn test_top_processes_table_debug() {
1533        let table = TopProcessesTable::default();
1534        let debug = format!("{:?}", table);
1535        assert!(debug.contains("TopProcessesTable"));
1536    }
1537
1538    // CoreUtilizationHistogram additional tests
1539    #[test]
1540    fn test_core_histogram_type_id() {
1541        let hist = CoreUtilizationHistogram::default();
1542        let id = Widget::type_id(&hist);
1543        assert_eq!(id, TypeId::of::<CoreUtilizationHistogram>());
1544    }
1545
1546    #[test]
1547    fn test_core_histogram_event() {
1548        let mut hist = CoreUtilizationHistogram::default();
1549        let result = hist.event(&Event::FocusIn);
1550        assert!(result.is_none());
1551    }
1552
1553    #[test]
1554    fn test_core_histogram_children_mut() {
1555        let mut hist = CoreUtilizationHistogram::default();
1556        assert!(hist.children_mut().is_empty());
1557    }
1558
1559    #[test]
1560    fn test_core_histogram_set_percentages() {
1561        let mut hist = CoreUtilizationHistogram::default();
1562        hist.set_percentages(vec![25.0, 50.0, 75.0]);
1563        assert_eq!(hist.core_percentages.len(), 3);
1564    }
1565
1566    #[test]
1567    fn test_core_histogram_assertions() {
1568        let hist = CoreUtilizationHistogram::default();
1569        assert!(!hist.assertions().is_empty());
1570    }
1571
1572    #[test]
1573    fn test_core_histogram_budget() {
1574        let hist = CoreUtilizationHistogram::default();
1575        let budget = hist.budget();
1576        assert!(budget.total_ms > 0);
1577    }
1578
1579    #[test]
1580    fn test_core_histogram_to_html() {
1581        let hist = CoreUtilizationHistogram::default();
1582        assert!(hist.to_html().is_empty());
1583    }
1584
1585    #[test]
1586    fn test_core_histogram_to_css() {
1587        let hist = CoreUtilizationHistogram::default();
1588        assert!(hist.to_css().is_empty());
1589    }
1590
1591    #[test]
1592    fn test_core_histogram_clone() {
1593        let hist = CoreUtilizationHistogram::new(vec![50.0, 60.0]);
1594        let cloned = hist;
1595        assert_eq!(cloned.core_percentages.len(), 2);
1596    }
1597
1598    #[test]
1599    fn test_core_histogram_debug() {
1600        let hist = CoreUtilizationHistogram::default();
1601        let debug = format!("{:?}", hist);
1602        assert!(debug.contains("CoreUtilizationHistogram"));
1603    }
1604
1605    #[test]
1606    fn test_core_histogram_paint_all_buckets_populated() {
1607        // Create histogram where all 5 buckets have values
1608        let mut hist = CoreUtilizationHistogram::new(vec![
1609            99.0, 80.0, 50.0, 15.0, 0.5, // One in each bucket
1610        ]);
1611        hist.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
1612        let mut buffer = CellBuffer::new(60, 10);
1613        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1614        hist.paint(&mut canvas);
1615    }
1616
1617    // TrendSparkline additional tests
1618    #[test]
1619    fn test_trend_sparkline_type_id() {
1620        let trend = TrendSparkline::default();
1621        let id = Widget::type_id(&trend);
1622        assert_eq!(id, TypeId::of::<TrendSparkline>());
1623    }
1624
1625    #[test]
1626    fn test_trend_sparkline_event() {
1627        let mut trend = TrendSparkline::default();
1628        let result = trend.event(&Event::FocusIn);
1629        assert!(result.is_none());
1630    }
1631
1632    #[test]
1633    fn test_trend_sparkline_children_mut() {
1634        let mut trend = TrendSparkline::default();
1635        assert!(trend.children_mut().is_empty());
1636    }
1637
1638    #[test]
1639    fn test_trend_sparkline_normalized() {
1640        let trend = TrendSparkline::new("Test", vec![0.5]).normalized();
1641        assert!(!trend.is_percentage);
1642    }
1643
1644    #[test]
1645    fn test_trend_sparkline_set_history() {
1646        let mut trend = TrendSparkline::default();
1647        trend.set_history(vec![10.0, 20.0, 30.0]);
1648        assert_eq!(trend.history.len(), 3);
1649    }
1650
1651    #[test]
1652    fn test_trend_sparkline_stats_normalized() {
1653        let trend = TrendSparkline::new("Test", vec![0.5, 0.6, 0.7]).normalized();
1654        let (current, min, max, avg) = trend.stats();
1655        // Normalized values are multiplied by 100
1656        assert!((current - 70.0).abs() < 0.1);
1657        assert!((min - 50.0).abs() < 0.1);
1658        assert!((max - 70.0).abs() < 0.1);
1659        assert!((avg - 60.0).abs() < 0.1);
1660    }
1661
1662    #[test]
1663    fn test_trend_sparkline_assertions() {
1664        let trend = TrendSparkline::default();
1665        assert!(!trend.assertions().is_empty());
1666    }
1667
1668    #[test]
1669    fn test_trend_sparkline_budget() {
1670        let trend = TrendSparkline::default();
1671        let budget = trend.budget();
1672        assert!(budget.total_ms > 0);
1673    }
1674
1675    #[test]
1676    fn test_trend_sparkline_to_html() {
1677        let trend = TrendSparkline::default();
1678        assert!(trend.to_html().is_empty());
1679    }
1680
1681    #[test]
1682    fn test_trend_sparkline_to_css() {
1683        let trend = TrendSparkline::default();
1684        assert!(trend.to_css().is_empty());
1685    }
1686
1687    #[test]
1688    fn test_trend_sparkline_clone() {
1689        let trend = TrendSparkline::new("Test", vec![1.0, 2.0, 3.0]);
1690        let cloned = trend;
1691        assert_eq!(cloned.history.len(), 3);
1692        assert_eq!(cloned.title, "Test");
1693    }
1694
1695    #[test]
1696    fn test_trend_sparkline_debug() {
1697        let trend = TrendSparkline::default();
1698        let debug = format!("{:?}", trend);
1699        assert!(debug.contains("TrendSparkline"));
1700    }
1701
1702    #[test]
1703    fn test_trend_sparkline_paint_high_values() {
1704        let mut trend = TrendSparkline::new("CPU", vec![85.0, 90.0, 95.0]);
1705        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1706        let mut buffer = CellBuffer::new(60, 5);
1707        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1708        trend.paint(&mut canvas);
1709    }
1710
1711    #[test]
1712    fn test_trend_sparkline_paint_medium_values() {
1713        let mut trend = TrendSparkline::new("CPU", vec![55.0, 60.0, 65.0]);
1714        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1715        let mut buffer = CellBuffer::new(60, 5);
1716        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1717        trend.paint(&mut canvas);
1718    }
1719
1720    #[test]
1721    fn test_trend_sparkline_paint_normalized() {
1722        let mut trend = TrendSparkline::new("Mem", vec![0.3, 0.5, 0.7]).normalized();
1723        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1724        let mut buffer = CellBuffer::new(60, 5);
1725        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1726        trend.paint(&mut canvas);
1727    }
1728
1729    // SystemStatus additional tests
1730    #[test]
1731    fn test_system_status_type_id() {
1732        let status = SystemStatus::default();
1733        let id = Widget::type_id(&status);
1734        assert_eq!(id, TypeId::of::<SystemStatus>());
1735    }
1736
1737    #[test]
1738    fn test_system_status_event() {
1739        let mut status = SystemStatus::default();
1740        let result = status.event(&Event::FocusIn);
1741        assert!(result.is_none());
1742    }
1743
1744    #[test]
1745    fn test_system_status_children_mut() {
1746        let mut status = SystemStatus::default();
1747        assert!(status.children_mut().is_empty());
1748    }
1749
1750    #[test]
1751    fn test_system_status_set_load() {
1752        let mut status = SystemStatus::default();
1753        status.set_load(2.0, 1.5, 1.0);
1754        assert!((status.load_1m - 2.0).abs() < 0.01);
1755        assert!((status.load_5m - 1.5).abs() < 0.01);
1756        assert!((status.load_15m - 1.0).abs() < 0.01);
1757    }
1758
1759    #[test]
1760    fn test_system_status_set_thermal() {
1761        let mut status = SystemStatus::default();
1762        status.set_thermal(60.0, 70.0);
1763        assert_eq!(status.thermal, Some((60.0, 70.0)));
1764    }
1765
1766    #[test]
1767    fn test_system_status_assertions() {
1768        let status = SystemStatus::default();
1769        assert!(!status.assertions().is_empty());
1770    }
1771
1772    #[test]
1773    fn test_system_status_budget() {
1774        let status = SystemStatus::default();
1775        let budget = status.budget();
1776        assert!(budget.total_ms > 0);
1777    }
1778
1779    #[test]
1780    fn test_system_status_to_html() {
1781        let status = SystemStatus::default();
1782        assert!(status.to_html().is_empty());
1783    }
1784
1785    #[test]
1786    fn test_system_status_to_css() {
1787        let status = SystemStatus::default();
1788        assert!(status.to_css().is_empty());
1789    }
1790
1791    #[test]
1792    fn test_system_status_clone() {
1793        let status = SystemStatus::new(1.0, 2.0, 3.0, 4).with_thermal(50.0, 60.0);
1794        let cloned = status;
1795        assert!((cloned.load_1m - 1.0).abs() < 0.01);
1796        assert_eq!(cloned.core_count, 4);
1797        assert!(cloned.thermal.is_some());
1798    }
1799
1800    #[test]
1801    fn test_system_status_debug() {
1802        let status = SystemStatus::default();
1803        let debug = format!("{:?}", status);
1804        assert!(debug.contains("SystemStatus"));
1805    }
1806
1807    #[test]
1808    fn test_system_status_paint_no_thermal() {
1809        let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1810        status.bounds = Rect::new(0.0, 0.0, 80.0, 5.0);
1811        let mut buffer = CellBuffer::new(80, 5);
1812        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1813        status.paint(&mut canvas);
1814    }
1815
1816    #[test]
1817    fn test_system_status_measure_with_thermal() {
1818        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(60.0, 70.0);
1819        // Use loose constraints to get natural size
1820        let constraints = Constraints::loose(Size::new(80.0, 100.0));
1821        let size = status.measure(constraints);
1822        assert_eq!(size.height, 2.0); // Two lines with thermal
1823    }
1824
1825    #[test]
1826    fn test_system_status_measure_no_thermal() {
1827        let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1828        // Use loose constraints to get natural size
1829        let constraints = Constraints::loose(Size::new(80.0, 100.0));
1830        let size = status.measure(constraints);
1831        assert_eq!(size.height, 1.0); // One line without thermal
1832    }
1833
1834    // CpuConsumer additional tests
1835    #[test]
1836    fn test_cpu_consumer_clone() {
1837        let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1838        let cloned = proc;
1839        assert_eq!(cloned.pid, 123);
1840    }
1841
1842    #[test]
1843    fn test_cpu_consumer_debug() {
1844        let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1845        let debug = format!("{:?}", proc);
1846        assert!(debug.contains("CpuConsumer"));
1847    }
1848
1849    // HealthLevel additional tests
1850    #[test]
1851    fn test_health_level_clone() {
1852        let level = HealthLevel::Critical;
1853        let cloned = level;
1854        assert_eq!(cloned, HealthLevel::Critical);
1855    }
1856
1857    #[test]
1858    fn test_health_level_debug() {
1859        let level = HealthLevel::Ok;
1860        let debug = format!("{:?}", level);
1861        assert!(debug.contains("Ok"));
1862    }
1863
1864    #[test]
1865    fn test_health_level_copy() {
1866        let level = HealthLevel::High;
1867        let copied = level;
1868        assert_eq!(copied, HealthLevel::High);
1869    }
1870}