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 = (count * bar_max).checked_div(total).unwrap_or(0);
485                let bar: String = "█".repeat(bar_w);
486                let pad: String = "░".repeat(bar_max - bar_w);
487
488                canvas.draw_text(
489                    label,
490                    Point::new(x, y),
491                    &TextStyle {
492                        color,
493                        ..Default::default()
494                    },
495                );
496                canvas.draw_text(
497                    &bar,
498                    Point::new(x + 10.0, y),
499                    &TextStyle {
500                        color,
501                        ..Default::default()
502                    },
503                );
504                canvas.draw_text(&pad, Point::new(x + 10.0 + bar_w as f32, y), &dim_style);
505                canvas.draw_text(
506                    &format!(" x{count}"),
507                    Point::new(x + 10.0 + bar_max as f32, y),
508                    &bright_style,
509                );
510            };
511
512        if b100 > 0 {
513            draw_bar(
514                canvas,
515                y,
516                "   100%",
517                b100,
518                Color {
519                    r: 1.0,
520                    g: 0.3,
521                    b: 0.3,
522                    a: 1.0,
523                },
524            );
525            y += 1.0;
526        }
527        if bhigh > 0 {
528            draw_bar(
529                canvas,
530                y,
531                " 70-95%",
532                bhigh,
533                Color {
534                    r: 1.0,
535                    g: 0.6,
536                    b: 0.3,
537                    a: 1.0,
538                },
539            );
540            y += 1.0;
541        }
542        if bmed > 0 {
543            draw_bar(
544                canvas,
545                y,
546                " 30-70%",
547                bmed,
548                Color {
549                    r: 1.0,
550                    g: 1.0,
551                    b: 0.4,
552                    a: 1.0,
553                },
554            );
555            y += 1.0;
556        }
557        if blow > 0 {
558            draw_bar(
559                canvas,
560                y,
561                "  1-30%",
562                blow,
563                Color {
564                    r: 0.5,
565                    g: 0.9,
566                    b: 0.5,
567                    a: 1.0,
568                },
569            );
570            y += 1.0;
571        }
572        if bidle > 0 {
573            draw_bar(
574                canvas,
575                y,
576                "   idle",
577                bidle,
578                Color {
579                    r: 0.4,
580                    g: 0.4,
581                    b: 0.5,
582                    a: 1.0,
583                },
584            );
585        }
586    }
587
588    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
589        None
590    }
591    fn children(&self) -> &[Box<dyn Widget>] {
592        &[]
593    }
594    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
595        &mut []
596    }
597}
598
599// =============================================================================
600// 3. TrendSparkline - Sparkline with context (min/max/avg/current)
601// =============================================================================
602
603/// Sparkline with statistical context
604#[derive(Debug, Clone)]
605pub struct TrendSparkline {
606    /// History values (0-1 normalized or 0-100 percentage)
607    history: Vec<f64>,
608    /// Title for the widget
609    title: String,
610    /// Whether values are percentages (0-100) or normalized (0-1)
611    is_percentage: bool,
612    /// Cached bounds
613    bounds: Rect,
614}
615
616impl Default for TrendSparkline {
617    fn default() -> Self {
618        Self::new("TREND", vec![])
619    }
620}
621
622impl TrendSparkline {
623    #[must_use]
624    pub fn new(title: impl Into<String>, history: Vec<f64>) -> Self {
625        Self {
626            history,
627            title: title.into(),
628            is_percentage: true,
629            bounds: Rect::default(),
630        }
631    }
632
633    /// Mark values as normalized (0-1) instead of percentage (0-100)
634    #[must_use]
635    pub fn normalized(mut self) -> Self {
636        self.is_percentage = false;
637        self
638    }
639
640    pub fn set_history(&mut self, history: Vec<f64>) {
641        self.history = history;
642    }
643
644    pub fn push(&mut self, value: f64) {
645        self.history.push(value);
646        if self.history.len() > 120 {
647            self.history.remove(0);
648        }
649    }
650
651    fn stats(&self) -> (f64, f64, f64, f64) {
652        if self.history.is_empty() {
653            return (0.0, 0.0, 0.0, 0.0);
654        }
655        let mult = if self.is_percentage { 1.0 } else { 100.0 };
656        let current = self.history.last().copied().unwrap_or(0.0) * mult;
657        let min = self.history.iter().copied().fold(f64::MAX, f64::min) * mult;
658        let max = self.history.iter().copied().fold(f64::MIN, f64::max) * mult;
659        let avg = self.history.iter().sum::<f64>() / self.history.len() as f64 * mult;
660        (current, min, max, avg)
661    }
662}
663
664impl Brick for TrendSparkline {
665    fn brick_name(&self) -> &'static str {
666        "trend_sparkline"
667    }
668    fn assertions(&self) -> &[BrickAssertion] {
669        static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
670        A
671    }
672    fn budget(&self) -> BrickBudget {
673        BrickBudget::uniform(16)
674    }
675    fn verify(&self) -> BrickVerification {
676        BrickVerification {
677            passed: self.assertions().to_vec(),
678            failed: vec![],
679            verification_time: Duration::from_micros(10),
680        }
681    }
682    fn to_html(&self) -> String {
683        String::new()
684    }
685    fn to_css(&self) -> String {
686        String::new()
687    }
688}
689
690impl Widget for TrendSparkline {
691    fn type_id(&self) -> TypeId {
692        TypeId::of::<Self>()
693    }
694
695    fn measure(&self, constraints: Constraints) -> Size {
696        constraints.constrain(Size::new(constraints.max_width, 4.0)) // header + sparkline + 2 stat lines
697    }
698
699    fn layout(&mut self, bounds: Rect) -> LayoutResult {
700        self.bounds = bounds;
701        LayoutResult {
702            size: Size::new(bounds.width, bounds.height),
703        }
704    }
705
706    fn paint(&self, canvas: &mut dyn Canvas) {
707        let x = self.bounds.x;
708        let mut y = self.bounds.y;
709        let w = self.bounds.width as usize;
710
711        let header_style = TextStyle {
712            color: Color {
713                r: 0.6,
714                g: 0.8,
715                b: 1.0,
716                a: 1.0,
717            },
718            ..Default::default()
719        };
720        let dim_style = TextStyle {
721            color: Color {
722                r: 0.5,
723                g: 0.5,
724                b: 0.6,
725                a: 1.0,
726            },
727            ..Default::default()
728        };
729        let bright_style = TextStyle {
730            color: Color {
731                r: 1.0,
732                g: 1.0,
733                b: 1.0,
734                a: 1.0,
735            },
736            ..Default::default()
737        };
738
739        canvas.draw_text(&self.title, Point::new(x, y), &header_style);
740        y += 1.0;
741
742        let (current, min, max, avg) = self.stats();
743
744        // Color based on current level
745        let trend_color = if current > 80.0 {
746            Color {
747                r: 1.0,
748                g: 0.4,
749                b: 0.4,
750                a: 1.0,
751            }
752        } else if current > 50.0 {
753            Color {
754                r: 1.0,
755                g: 0.8,
756                b: 0.4,
757                a: 1.0,
758            }
759        } else {
760            Color {
761                r: 0.5,
762                g: 0.9,
763                b: 0.5,
764                a: 1.0,
765            }
766        };
767
768        // Draw sparkline
769        let mult = if self.is_percentage { 1.0 } else { 100.0 };
770        let chars: String = self
771            .history
772            .iter()
773            .rev()
774            .take(w)
775            .rev()
776            .map(|&v| {
777                let pct = (v * mult).clamp(0.0, 100.0);
778                let idx = ((pct / 100.0) * 7.0).round() as usize;
779                ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][idx.min(7)]
780            })
781            .collect();
782
783        canvas.draw_text(
784            &chars,
785            Point::new(x, y),
786            &TextStyle {
787                color: trend_color,
788                ..Default::default()
789            },
790        );
791        y += 1.0;
792
793        // Stats
794        let now_avg = format!("Now: {current:.0}%  Avg: {avg:.0}%");
795        canvas.draw_text(
796            &now_avg[..now_avg.len().min(w)],
797            Point::new(x, y),
798            &bright_style,
799        );
800        y += 1.0;
801
802        let min_max = format!("Min: {min:.0}%  Max: {max:.0}%");
803        canvas.draw_text(
804            &min_max[..min_max.len().min(w)],
805            Point::new(x, y),
806            &dim_style,
807        );
808    }
809
810    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
811        None
812    }
813    fn children(&self) -> &[Box<dyn Widget>] {
814        &[]
815    }
816    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
817        &mut []
818    }
819}
820
821// =============================================================================
822// 4. SystemStatus - Load and thermal status with contextual coloring
823// =============================================================================
824
825/// System status display (load, thermals)
826#[derive(Debug, Clone)]
827pub struct SystemStatus {
828    /// Load averages (1, 5, 15 minute)
829    load_1m: f64,
830    load_5m: f64,
831    load_15m: f64,
832    /// Number of cores (for per-core load calculation)
833    core_count: usize,
834    /// Thermal data: (`avg_temp`, `max_temp`) in Celsius
835    thermal: Option<(f64, f64)>,
836    /// Cached bounds
837    bounds: Rect,
838}
839
840impl Default for SystemStatus {
841    fn default() -> Self {
842        Self::new(0.0, 0.0, 0.0, 1)
843    }
844}
845
846impl SystemStatus {
847    #[must_use]
848    pub fn new(load_1m: f64, load_5m: f64, load_15m: f64, core_count: usize) -> Self {
849        Self {
850            load_1m,
851            load_5m,
852            load_15m,
853            core_count: core_count.max(1),
854            thermal: None,
855            bounds: Rect::default(),
856        }
857    }
858
859    #[must_use]
860    pub fn with_thermal(mut self, avg_temp: f64, max_temp: f64) -> Self {
861        self.thermal = Some((avg_temp, max_temp));
862        self
863    }
864
865    pub fn set_load(&mut self, l1: f64, l5: f64, l15: f64) {
866        self.load_1m = l1;
867        self.load_5m = l5;
868        self.load_15m = l15;
869    }
870
871    pub fn set_thermal(&mut self, avg: f64, max: f64) {
872        self.thermal = Some((avg, max));
873    }
874
875    /// Get the health level for current load
876    pub fn load_status(&self) -> HealthLevel {
877        let per_core = self.load_1m / self.core_count as f64;
878        if per_core > 1.5 {
879            HealthLevel::Critical
880        } else if per_core > 1.0 {
881            HealthLevel::High
882        } else if per_core > 0.7 {
883            HealthLevel::Moderate
884        } else {
885            HealthLevel::Ok
886        }
887    }
888
889    /// Get the health level for thermal status
890    pub fn thermal_status(&self) -> Option<HealthLevel> {
891        self.thermal.map(|(_, max)| {
892            if max > 90.0 {
893                HealthLevel::Critical
894            } else if max > 80.0 {
895                HealthLevel::High
896            } else if max > 70.0 {
897                HealthLevel::Moderate
898            } else {
899                HealthLevel::Ok
900            }
901        })
902    }
903}
904
905impl Brick for SystemStatus {
906    fn brick_name(&self) -> &'static str {
907        "system_status"
908    }
909    fn assertions(&self) -> &[BrickAssertion] {
910        static A: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
911        A
912    }
913    fn budget(&self) -> BrickBudget {
914        BrickBudget::uniform(16)
915    }
916    fn verify(&self) -> BrickVerification {
917        BrickVerification {
918            passed: self.assertions().to_vec(),
919            failed: vec![],
920            verification_time: Duration::from_micros(10),
921        }
922    }
923    fn to_html(&self) -> String {
924        String::new()
925    }
926    fn to_css(&self) -> String {
927        String::new()
928    }
929}
930
931impl Widget for SystemStatus {
932    fn type_id(&self) -> TypeId {
933        TypeId::of::<Self>()
934    }
935
936    fn measure(&self, constraints: Constraints) -> Size {
937        let height = if self.thermal.is_some() { 2.0 } else { 1.0 };
938        constraints.constrain(Size::new(constraints.max_width, height))
939    }
940
941    fn layout(&mut self, bounds: Rect) -> LayoutResult {
942        self.bounds = bounds;
943        LayoutResult {
944            size: Size::new(bounds.width, bounds.height),
945        }
946    }
947
948    fn paint(&self, canvas: &mut dyn Canvas) {
949        let x = self.bounds.x;
950        let mut y = self.bounds.y;
951        let w = self.bounds.width as usize;
952
953        // Load line
954        let load_stat = self.load_status();
955        let per_core = self.load_1m / self.core_count as f64;
956        let load_line = format!(
957            "LOAD: {:.2} / {:.2} / {:.2}  ({:.2}/core) - {}",
958            self.load_1m,
959            self.load_5m,
960            self.load_15m,
961            per_core,
962            load_stat.as_str()
963        );
964        canvas.draw_text(
965            &load_line[..load_line.len().min(w)],
966            Point::new(x, y),
967            &TextStyle {
968                color: color_for_status(load_stat),
969                ..Default::default()
970            },
971        );
972
973        // Thermal line (only if data present)
974        if let Some((avg, max)) = self.thermal {
975            y += 1.0;
976            let therm_stat = self.thermal_status().unwrap_or(HealthLevel::Ok);
977            let therm_line = format!(
978                "THERMAL: {:.0}°C avg, {:.0}°C max - {}",
979                avg,
980                max,
981                therm_stat.as_str()
982            );
983            canvas.draw_text(
984                &therm_line[..therm_line.len().min(w)],
985                Point::new(x, y),
986                &TextStyle {
987                    color: color_for_status(therm_stat),
988                    ..Default::default()
989                },
990            );
991        }
992    }
993
994    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
995        None
996    }
997    fn children(&self) -> &[Box<dyn Widget>] {
998        &[]
999    }
1000    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1001        &mut []
1002    }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007    use super::*;
1008    use crate::direct::{CellBuffer, DirectTerminalCanvas};
1009
1010    // HealthLevel tests
1011    #[test]
1012    fn test_health_level_as_str() {
1013        assert_eq!(HealthLevel::Critical.as_str(), "CRITICAL");
1014        assert_eq!(HealthLevel::High.as_str(), "HIGH");
1015        assert_eq!(HealthLevel::Moderate.as_str(), "MODERATE");
1016        assert_eq!(HealthLevel::Ok.as_str(), "OK");
1017    }
1018
1019    // Color helper tests
1020    #[test]
1021    fn test_color_for_cpu_percent_high() {
1022        let color = color_for_cpu_percent(75.0);
1023        assert!(color.r > 0.9); // Red
1024    }
1025
1026    #[test]
1027    fn test_color_for_cpu_percent_medium() {
1028        let color = color_for_cpu_percent(30.0);
1029        assert!(color.r > 0.9 && color.g > 0.7); // Yellow
1030    }
1031
1032    #[test]
1033    fn test_color_for_cpu_percent_low() {
1034        let color = color_for_cpu_percent(5.0);
1035        assert!(color.g > 0.8); // Green
1036    }
1037
1038    #[test]
1039    fn test_color_for_cpu_percent_idle() {
1040        let color = color_for_cpu_percent(0.5);
1041        assert!(color.r < 0.6 && color.g < 0.6); // Dim
1042    }
1043
1044    #[test]
1045    fn test_color_for_status() {
1046        let critical = color_for_status(HealthLevel::Critical);
1047        assert!(critical.r > 0.9 && critical.g < 0.3);
1048
1049        let high = color_for_status(HealthLevel::High);
1050        assert!(high.r > 0.9);
1051
1052        let moderate = color_for_status(HealthLevel::Moderate);
1053        assert!(moderate.r > 0.9 && moderate.g > 0.7);
1054
1055        let ok = color_for_status(HealthLevel::Ok);
1056        assert!(ok.g > 0.8);
1057    }
1058
1059    // CpuConsumer tests
1060    #[test]
1061    fn test_cpu_consumer_new() {
1062        let proc = CpuConsumer::new(123, 45.5, 1_000_000_000, "firefox");
1063        assert_eq!(proc.pid, 123);
1064        assert!((proc.cpu_percent - 45.5).abs() < 0.01);
1065        assert_eq!(proc.memory_bytes, 1_000_000_000);
1066        assert_eq!(proc.name, "firefox");
1067    }
1068
1069    #[test]
1070    fn test_cpu_consumer_memory_display_gb() {
1071        let proc = CpuConsumer::new(1, 10.0, 2_000_000_000, "test");
1072        let display = proc.memory_display();
1073        assert!(display.contains('G'));
1074    }
1075
1076    #[test]
1077    fn test_cpu_consumer_memory_display_mb() {
1078        let proc = CpuConsumer::new(1, 10.0, 500_000_000, "test");
1079        let display = proc.memory_display();
1080        assert!(display.contains('M'));
1081    }
1082
1083    #[test]
1084    fn test_cpu_consumer_memory_display_kb() {
1085        let proc = CpuConsumer::new(1, 10.0, 500_000, "test");
1086        let display = proc.memory_display();
1087        assert!(display.contains('K'));
1088    }
1089
1090    // TopProcessesTable tests
1091    #[test]
1092    fn test_top_processes_table() {
1093        let procs = vec![
1094            CpuConsumer::new(123, 45.0, 1_000_000_000, "firefox"),
1095            CpuConsumer::new(456, 23.0, 500_000_000, "chrome"),
1096        ];
1097        let table = TopProcessesTable::new(procs, 68.0);
1098        assert!(table.verify().is_valid());
1099    }
1100
1101    #[test]
1102    fn test_top_processes_table_default() {
1103        let table = TopProcessesTable::default();
1104        assert!(table.processes.is_empty());
1105    }
1106
1107    #[test]
1108    fn test_top_processes_table_with_max_display() {
1109        let table = TopProcessesTable::new(vec![], 0.0).with_max_display(5);
1110        assert_eq!(table.max_display, 5);
1111    }
1112
1113    #[test]
1114    fn test_top_processes_table_set_processes() {
1115        let mut table = TopProcessesTable::new(vec![], 0.0);
1116        table.set_processes(
1117            vec![
1118                CpuConsumer::new(1, 20.0, 100, "a"),
1119                CpuConsumer::new(2, 50.0, 200, "b"),
1120            ],
1121            70.0,
1122        );
1123        assert_eq!(table.processes.len(), 2);
1124        assert_eq!(table.total_cpu, 70.0);
1125        // Should be sorted by CPU descending
1126        assert_eq!(table.processes[0].pid, 2);
1127    }
1128
1129    #[test]
1130    fn test_top_processes_table_sorts_by_cpu() {
1131        let procs = vec![
1132            CpuConsumer::new(1, 10.0, 100, "low"),
1133            CpuConsumer::new(2, 90.0, 100, "high"),
1134            CpuConsumer::new(3, 50.0, 100, "mid"),
1135        ];
1136        let table = TopProcessesTable::new(procs, 150.0);
1137        assert_eq!(table.processes[0].pid, 2); // high
1138        assert_eq!(table.processes[1].pid, 3); // mid
1139        assert_eq!(table.processes[2].pid, 1); // low
1140    }
1141
1142    #[test]
1143    fn test_top_processes_table_brick_name() {
1144        let table = TopProcessesTable::default();
1145        assert_eq!(table.brick_name(), "top_processes_table");
1146    }
1147
1148    #[test]
1149    fn test_top_processes_table_measure() {
1150        let table = TopProcessesTable::default();
1151        let constraints = Constraints::tight(Size::new(80.0, 40.0));
1152        let size = table.measure(constraints);
1153        assert!(size.height > 0.0);
1154    }
1155
1156    #[test]
1157    fn test_top_processes_table_layout() {
1158        let mut table = TopProcessesTable::default();
1159        let bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1160        let result = table.layout(bounds);
1161        assert_eq!(result.size.width, 80.0);
1162        assert_eq!(result.size.height, 20.0);
1163    }
1164
1165    #[test]
1166    fn test_top_processes_table_paint() {
1167        let mut table =
1168            TopProcessesTable::new(vec![CpuConsumer::new(1, 50.0, 1_000_000_000, "test")], 50.0);
1169        table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1170        let mut buffer = CellBuffer::new(80, 20);
1171        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1172        table.paint(&mut canvas);
1173    }
1174
1175    #[test]
1176    fn test_top_processes_table_children() {
1177        let table = TopProcessesTable::default();
1178        assert!(table.children().is_empty());
1179    }
1180
1181    // CoreUtilizationHistogram tests
1182    #[test]
1183    fn test_core_histogram() {
1184        let hist = CoreUtilizationHistogram::new(vec![95.0, 96.0, 50.0, 10.0, 0.5]);
1185        let (b100, _bhigh, bmed, blow, bidle) = hist.bucket_counts();
1186        assert_eq!(b100, 2);
1187        assert_eq!(bmed, 1);
1188        assert_eq!(blow, 1);
1189        assert_eq!(bidle, 1);
1190    }
1191
1192    #[test]
1193    fn test_core_histogram_default() {
1194        let hist = CoreUtilizationHistogram::default();
1195        assert!(hist.core_percentages.is_empty());
1196    }
1197
1198    #[test]
1199    fn test_core_histogram_bucket_counts_empty() {
1200        let hist = CoreUtilizationHistogram::new(vec![]);
1201        let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1202        assert_eq!(b100 + bhigh + bmed + blow + bidle, 0);
1203    }
1204
1205    #[test]
1206    fn test_core_histogram_all_buckets() {
1207        // Test all bucket ranges based on actual thresholds:
1208        // 95-100: b100, 70-95: bhigh, 30-70: bmed, 1-30: blow, <1: bidle
1209        let hist = CoreUtilizationHistogram::new(vec![
1210            97.0, // 95-100: b100
1211            80.0, // 70-95: bhigh
1212            50.0, // 30-70: bmed
1213            15.0, // 1-30: blow
1214            0.5,  // <1: bidle
1215        ]);
1216        let (b100, bhigh, bmed, blow, bidle) = hist.bucket_counts();
1217        assert_eq!(b100, 1);
1218        assert_eq!(bhigh, 1);
1219        assert_eq!(bmed, 1);
1220        assert_eq!(blow, 1);
1221        assert_eq!(bidle, 1);
1222    }
1223
1224    #[test]
1225    fn test_core_histogram_brick_name() {
1226        let hist = CoreUtilizationHistogram::default();
1227        assert_eq!(hist.brick_name(), "core_utilization_histogram");
1228    }
1229
1230    #[test]
1231    fn test_core_histogram_measure() {
1232        let hist = CoreUtilizationHistogram::new(vec![50.0; 8]);
1233        let constraints = Constraints::tight(Size::new(40.0, 10.0));
1234        let size = hist.measure(constraints);
1235        assert!(size.width > 0.0);
1236    }
1237
1238    #[test]
1239    fn test_core_histogram_layout() {
1240        let mut hist = CoreUtilizationHistogram::new(vec![50.0; 4]);
1241        let bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1242        let result = hist.layout(bounds);
1243        assert_eq!(result.size.width, 40.0);
1244    }
1245
1246    #[test]
1247    fn test_core_histogram_paint() {
1248        let mut hist = CoreUtilizationHistogram::new(vec![50.0, 75.0, 25.0, 90.0]);
1249        hist.bounds = Rect::new(0.0, 0.0, 40.0, 8.0);
1250        let mut buffer = CellBuffer::new(40, 8);
1251        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1252        hist.paint(&mut canvas);
1253    }
1254
1255    #[test]
1256    fn test_core_histogram_children() {
1257        let hist = CoreUtilizationHistogram::default();
1258        assert!(hist.children().is_empty());
1259    }
1260
1261    // TrendSparkline tests
1262    #[test]
1263    fn test_trend_sparkline() {
1264        let mut trend = TrendSparkline::new("CPU", vec![50.0, 60.0, 70.0]);
1265        trend.push(80.0);
1266        let (current, min, max, _avg) = trend.stats();
1267        assert!((current - 80.0).abs() < 0.01);
1268        assert!((min - 50.0).abs() < 0.01);
1269        assert!((max - 80.0).abs() < 0.01);
1270    }
1271
1272    #[test]
1273    fn test_trend_sparkline_default() {
1274        let trend = TrendSparkline::default();
1275        assert!(trend.history.is_empty());
1276    }
1277
1278    #[test]
1279    fn test_trend_sparkline_empty_stats() {
1280        let trend = TrendSparkline::new("Empty", vec![]);
1281        let (current, min, max, avg) = trend.stats();
1282        assert_eq!(current, 0.0);
1283        assert_eq!(min, 0.0);
1284        assert_eq!(max, 0.0);
1285        assert_eq!(avg, 0.0);
1286    }
1287
1288    #[test]
1289    fn test_trend_sparkline_single_value() {
1290        let trend = TrendSparkline::new("Single", vec![42.0]);
1291        let (current, min, max, avg) = trend.stats();
1292        assert_eq!(current, 42.0);
1293        assert_eq!(min, 42.0);
1294        assert_eq!(max, 42.0);
1295        assert_eq!(avg, 42.0);
1296    }
1297
1298    #[test]
1299    fn test_trend_sparkline_push_overflow() {
1300        let mut trend = TrendSparkline::new("Test", vec![1.0; 120]);
1301        for i in 0..10 {
1302            trend.push(i as f64);
1303        }
1304        // Should maintain max length (120)
1305        assert!(trend.history.len() <= 120);
1306    }
1307
1308    #[test]
1309    fn test_trend_sparkline_brick_name() {
1310        let trend = TrendSparkline::default();
1311        assert_eq!(trend.brick_name(), "trend_sparkline");
1312    }
1313
1314    #[test]
1315    fn test_trend_sparkline_measure() {
1316        let trend = TrendSparkline::new("Test", vec![50.0; 10]);
1317        let constraints = Constraints::tight(Size::new(60.0, 5.0));
1318        let size = trend.measure(constraints);
1319        assert!(size.width > 0.0);
1320    }
1321
1322    #[test]
1323    fn test_trend_sparkline_layout() {
1324        let mut trend = TrendSparkline::new("Test", vec![50.0; 10]);
1325        let bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1326        let result = trend.layout(bounds);
1327        assert_eq!(result.size.width, 60.0);
1328    }
1329
1330    #[test]
1331    fn test_trend_sparkline_paint() {
1332        let mut trend = TrendSparkline::new("CPU", vec![30.0, 50.0, 70.0, 90.0]);
1333        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1334        let mut buffer = CellBuffer::new(60, 5);
1335        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1336        trend.paint(&mut canvas);
1337    }
1338
1339    #[test]
1340    fn test_trend_sparkline_children() {
1341        let trend = TrendSparkline::default();
1342        assert!(trend.children().is_empty());
1343    }
1344
1345    // SystemStatus tests
1346    #[test]
1347    fn test_system_status() {
1348        let status = SystemStatus::new(4.0, 3.0, 2.0, 8).with_thermal(65.0, 72.0);
1349        assert_eq!(status.load_status(), HealthLevel::Ok); // 4/8 = 0.5 per core (< 0.7)
1350        assert_eq!(status.thermal_status(), Some(HealthLevel::Moderate)); // 72°C
1351    }
1352
1353    #[test]
1354    fn test_system_status_default() {
1355        let status = SystemStatus::default();
1356        assert!((status.load_1m - 0.0).abs() < 0.01);
1357        assert_eq!(status.core_count, 1);
1358    }
1359
1360    #[test]
1361    fn test_system_status_load_critical() {
1362        // per_core > 1.5 = critical (load = 8.0, cores = 4, per_core = 2.0)
1363        let status = SystemStatus::new(8.0, 6.0, 4.0, 4);
1364        assert_eq!(status.load_status(), HealthLevel::Critical);
1365    }
1366
1367    #[test]
1368    fn test_system_status_load_high() {
1369        // per_core > 1.0 = high (load = 5.0, cores = 4, per_core = 1.25)
1370        let status = SystemStatus::new(5.0, 4.0, 3.0, 4);
1371        assert_eq!(status.load_status(), HealthLevel::High);
1372    }
1373
1374    #[test]
1375    fn test_system_status_load_moderate() {
1376        // per_core > 0.7 = moderate (load = 3.2, cores = 4, per_core = 0.8)
1377        let status = SystemStatus::new(3.2, 3.0, 2.8, 4);
1378        assert_eq!(status.load_status(), HealthLevel::Moderate);
1379    }
1380
1381    #[test]
1382    fn test_system_status_thermal_critical() {
1383        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(95.0, 100.0);
1384        assert_eq!(status.thermal_status(), Some(HealthLevel::Critical));
1385    }
1386
1387    #[test]
1388    fn test_system_status_thermal_high() {
1389        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(80.0, 85.0);
1390        assert_eq!(status.thermal_status(), Some(HealthLevel::High));
1391    }
1392
1393    #[test]
1394    fn test_system_status_thermal_ok() {
1395        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(50.0, 55.0);
1396        assert_eq!(status.thermal_status(), Some(HealthLevel::Ok));
1397    }
1398
1399    #[test]
1400    fn test_system_status_no_thermal() {
1401        let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1402        assert_eq!(status.thermal_status(), None);
1403    }
1404
1405    #[test]
1406    fn test_system_status_brick_name() {
1407        let status = SystemStatus::default();
1408        assert_eq!(status.brick_name(), "system_status");
1409    }
1410
1411    #[test]
1412    fn test_system_status_measure() {
1413        let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1414        let constraints = Constraints::tight(Size::new(80.0, 10.0));
1415        let size = status.measure(constraints);
1416        assert!(size.width > 0.0);
1417    }
1418
1419    #[test]
1420    fn test_system_status_layout() {
1421        let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1422        let bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1423        let result = status.layout(bounds);
1424        assert_eq!(result.size.width, 80.0);
1425    }
1426
1427    #[test]
1428    fn test_system_status_paint() {
1429        let mut status = SystemStatus::new(2.0, 1.5, 1.0, 4).with_thermal(60.0, 65.0);
1430        status.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1431        let mut buffer = CellBuffer::new(80, 10);
1432        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1433        status.paint(&mut canvas);
1434    }
1435
1436    #[test]
1437    fn test_system_status_children() {
1438        let status = SystemStatus::default();
1439        assert!(status.children().is_empty());
1440    }
1441
1442    // =========================================================================
1443    // Additional coverage tests
1444    // =========================================================================
1445
1446    // TopProcessesTable additional tests
1447    #[test]
1448    fn test_top_processes_table_type_id() {
1449        let table = TopProcessesTable::default();
1450        let id = Widget::type_id(&table);
1451        assert_eq!(id, TypeId::of::<TopProcessesTable>());
1452    }
1453
1454    #[test]
1455    fn test_top_processes_table_event() {
1456        let mut table = TopProcessesTable::default();
1457        let result = table.event(&Event::FocusIn);
1458        assert!(result.is_none());
1459    }
1460
1461    #[test]
1462    fn test_top_processes_table_children_mut() {
1463        let mut table = TopProcessesTable::default();
1464        assert!(table.children_mut().is_empty());
1465    }
1466
1467    #[test]
1468    fn test_top_processes_table_assertions() {
1469        let table = TopProcessesTable::default();
1470        assert!(!table.assertions().is_empty());
1471    }
1472
1473    #[test]
1474    fn test_top_processes_table_budget() {
1475        let table = TopProcessesTable::default();
1476        let budget = table.budget();
1477        assert!(budget.total_ms > 0);
1478    }
1479
1480    #[test]
1481    fn test_top_processes_table_to_html() {
1482        let table = TopProcessesTable::default();
1483        assert!(table.to_html().is_empty());
1484    }
1485
1486    #[test]
1487    fn test_top_processes_table_to_css() {
1488        let table = TopProcessesTable::default();
1489        assert!(table.to_css().is_empty());
1490    }
1491
1492    #[test]
1493    fn test_top_processes_table_paint_many_processes() {
1494        // Test with more processes than max_display
1495        let procs: Vec<CpuConsumer> = (0..20)
1496            .map(|i| CpuConsumer::new(i, i as f32 * 5.0, 1_000_000 * i as u64, format!("proc{i}")))
1497            .collect();
1498        let mut table = TopProcessesTable::new(procs, 190.0).with_max_display(5);
1499        table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1500        let mut buffer = CellBuffer::new(80, 10);
1501        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1502        table.paint(&mut canvas);
1503    }
1504
1505    #[test]
1506    fn test_top_processes_table_paint_long_name() {
1507        let proc = CpuConsumer::new(
1508            123,
1509            50.0,
1510            1_000_000_000,
1511            "this_is_a_very_long_process_name_that_should_be_truncated",
1512        );
1513        let mut table = TopProcessesTable::new(vec![proc], 50.0);
1514        table.bounds = Rect::new(0.0, 0.0, 50.0, 10.0);
1515        let mut buffer = CellBuffer::new(50, 10);
1516        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1517        table.paint(&mut canvas);
1518    }
1519
1520    #[test]
1521    fn test_top_processes_table_clone() {
1522        let table = TopProcessesTable::new(vec![CpuConsumer::new(1, 10.0, 100, "test")], 10.0);
1523        let cloned = table;
1524        assert_eq!(cloned.processes.len(), 1);
1525    }
1526
1527    #[test]
1528    fn test_top_processes_table_debug() {
1529        let table = TopProcessesTable::default();
1530        let debug = format!("{:?}", table);
1531        assert!(debug.contains("TopProcessesTable"));
1532    }
1533
1534    // CoreUtilizationHistogram additional tests
1535    #[test]
1536    fn test_core_histogram_type_id() {
1537        let hist = CoreUtilizationHistogram::default();
1538        let id = Widget::type_id(&hist);
1539        assert_eq!(id, TypeId::of::<CoreUtilizationHistogram>());
1540    }
1541
1542    #[test]
1543    fn test_core_histogram_event() {
1544        let mut hist = CoreUtilizationHistogram::default();
1545        let result = hist.event(&Event::FocusIn);
1546        assert!(result.is_none());
1547    }
1548
1549    #[test]
1550    fn test_core_histogram_children_mut() {
1551        let mut hist = CoreUtilizationHistogram::default();
1552        assert!(hist.children_mut().is_empty());
1553    }
1554
1555    #[test]
1556    fn test_core_histogram_set_percentages() {
1557        let mut hist = CoreUtilizationHistogram::default();
1558        hist.set_percentages(vec![25.0, 50.0, 75.0]);
1559        assert_eq!(hist.core_percentages.len(), 3);
1560    }
1561
1562    #[test]
1563    fn test_core_histogram_assertions() {
1564        let hist = CoreUtilizationHistogram::default();
1565        assert!(!hist.assertions().is_empty());
1566    }
1567
1568    #[test]
1569    fn test_core_histogram_budget() {
1570        let hist = CoreUtilizationHistogram::default();
1571        let budget = hist.budget();
1572        assert!(budget.total_ms > 0);
1573    }
1574
1575    #[test]
1576    fn test_core_histogram_to_html() {
1577        let hist = CoreUtilizationHistogram::default();
1578        assert!(hist.to_html().is_empty());
1579    }
1580
1581    #[test]
1582    fn test_core_histogram_to_css() {
1583        let hist = CoreUtilizationHistogram::default();
1584        assert!(hist.to_css().is_empty());
1585    }
1586
1587    #[test]
1588    fn test_core_histogram_clone() {
1589        let hist = CoreUtilizationHistogram::new(vec![50.0, 60.0]);
1590        let cloned = hist;
1591        assert_eq!(cloned.core_percentages.len(), 2);
1592    }
1593
1594    #[test]
1595    fn test_core_histogram_debug() {
1596        let hist = CoreUtilizationHistogram::default();
1597        let debug = format!("{:?}", hist);
1598        assert!(debug.contains("CoreUtilizationHistogram"));
1599    }
1600
1601    #[test]
1602    fn test_core_histogram_paint_all_buckets_populated() {
1603        // Create histogram where all 5 buckets have values
1604        let mut hist = CoreUtilizationHistogram::new(vec![
1605            99.0, 80.0, 50.0, 15.0, 0.5, // One in each bucket
1606        ]);
1607        hist.bounds = Rect::new(0.0, 0.0, 60.0, 10.0);
1608        let mut buffer = CellBuffer::new(60, 10);
1609        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1610        hist.paint(&mut canvas);
1611    }
1612
1613    // TrendSparkline additional tests
1614    #[test]
1615    fn test_trend_sparkline_type_id() {
1616        let trend = TrendSparkline::default();
1617        let id = Widget::type_id(&trend);
1618        assert_eq!(id, TypeId::of::<TrendSparkline>());
1619    }
1620
1621    #[test]
1622    fn test_trend_sparkline_event() {
1623        let mut trend = TrendSparkline::default();
1624        let result = trend.event(&Event::FocusIn);
1625        assert!(result.is_none());
1626    }
1627
1628    #[test]
1629    fn test_trend_sparkline_children_mut() {
1630        let mut trend = TrendSparkline::default();
1631        assert!(trend.children_mut().is_empty());
1632    }
1633
1634    #[test]
1635    fn test_trend_sparkline_normalized() {
1636        let trend = TrendSparkline::new("Test", vec![0.5]).normalized();
1637        assert!(!trend.is_percentage);
1638    }
1639
1640    #[test]
1641    fn test_trend_sparkline_set_history() {
1642        let mut trend = TrendSparkline::default();
1643        trend.set_history(vec![10.0, 20.0, 30.0]);
1644        assert_eq!(trend.history.len(), 3);
1645    }
1646
1647    #[test]
1648    fn test_trend_sparkline_stats_normalized() {
1649        let trend = TrendSparkline::new("Test", vec![0.5, 0.6, 0.7]).normalized();
1650        let (current, min, max, avg) = trend.stats();
1651        // Normalized values are multiplied by 100
1652        assert!((current - 70.0).abs() < 0.1);
1653        assert!((min - 50.0).abs() < 0.1);
1654        assert!((max - 70.0).abs() < 0.1);
1655        assert!((avg - 60.0).abs() < 0.1);
1656    }
1657
1658    #[test]
1659    fn test_trend_sparkline_assertions() {
1660        let trend = TrendSparkline::default();
1661        assert!(!trend.assertions().is_empty());
1662    }
1663
1664    #[test]
1665    fn test_trend_sparkline_budget() {
1666        let trend = TrendSparkline::default();
1667        let budget = trend.budget();
1668        assert!(budget.total_ms > 0);
1669    }
1670
1671    #[test]
1672    fn test_trend_sparkline_to_html() {
1673        let trend = TrendSparkline::default();
1674        assert!(trend.to_html().is_empty());
1675    }
1676
1677    #[test]
1678    fn test_trend_sparkline_to_css() {
1679        let trend = TrendSparkline::default();
1680        assert!(trend.to_css().is_empty());
1681    }
1682
1683    #[test]
1684    fn test_trend_sparkline_clone() {
1685        let trend = TrendSparkline::new("Test", vec![1.0, 2.0, 3.0]);
1686        let cloned = trend;
1687        assert_eq!(cloned.history.len(), 3);
1688        assert_eq!(cloned.title, "Test");
1689    }
1690
1691    #[test]
1692    fn test_trend_sparkline_debug() {
1693        let trend = TrendSparkline::default();
1694        let debug = format!("{:?}", trend);
1695        assert!(debug.contains("TrendSparkline"));
1696    }
1697
1698    #[test]
1699    fn test_trend_sparkline_paint_high_values() {
1700        let mut trend = TrendSparkline::new("CPU", vec![85.0, 90.0, 95.0]);
1701        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1702        let mut buffer = CellBuffer::new(60, 5);
1703        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1704        trend.paint(&mut canvas);
1705    }
1706
1707    #[test]
1708    fn test_trend_sparkline_paint_medium_values() {
1709        let mut trend = TrendSparkline::new("CPU", vec![55.0, 60.0, 65.0]);
1710        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1711        let mut buffer = CellBuffer::new(60, 5);
1712        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1713        trend.paint(&mut canvas);
1714    }
1715
1716    #[test]
1717    fn test_trend_sparkline_paint_normalized() {
1718        let mut trend = TrendSparkline::new("Mem", vec![0.3, 0.5, 0.7]).normalized();
1719        trend.bounds = Rect::new(0.0, 0.0, 60.0, 5.0);
1720        let mut buffer = CellBuffer::new(60, 5);
1721        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1722        trend.paint(&mut canvas);
1723    }
1724
1725    // SystemStatus additional tests
1726    #[test]
1727    fn test_system_status_type_id() {
1728        let status = SystemStatus::default();
1729        let id = Widget::type_id(&status);
1730        assert_eq!(id, TypeId::of::<SystemStatus>());
1731    }
1732
1733    #[test]
1734    fn test_system_status_event() {
1735        let mut status = SystemStatus::default();
1736        let result = status.event(&Event::FocusIn);
1737        assert!(result.is_none());
1738    }
1739
1740    #[test]
1741    fn test_system_status_children_mut() {
1742        let mut status = SystemStatus::default();
1743        assert!(status.children_mut().is_empty());
1744    }
1745
1746    #[test]
1747    fn test_system_status_set_load() {
1748        let mut status = SystemStatus::default();
1749        status.set_load(2.0, 1.5, 1.0);
1750        assert!((status.load_1m - 2.0).abs() < 0.01);
1751        assert!((status.load_5m - 1.5).abs() < 0.01);
1752        assert!((status.load_15m - 1.0).abs() < 0.01);
1753    }
1754
1755    #[test]
1756    fn test_system_status_set_thermal() {
1757        let mut status = SystemStatus::default();
1758        status.set_thermal(60.0, 70.0);
1759        assert_eq!(status.thermal, Some((60.0, 70.0)));
1760    }
1761
1762    #[test]
1763    fn test_system_status_assertions() {
1764        let status = SystemStatus::default();
1765        assert!(!status.assertions().is_empty());
1766    }
1767
1768    #[test]
1769    fn test_system_status_budget() {
1770        let status = SystemStatus::default();
1771        let budget = status.budget();
1772        assert!(budget.total_ms > 0);
1773    }
1774
1775    #[test]
1776    fn test_system_status_to_html() {
1777        let status = SystemStatus::default();
1778        assert!(status.to_html().is_empty());
1779    }
1780
1781    #[test]
1782    fn test_system_status_to_css() {
1783        let status = SystemStatus::default();
1784        assert!(status.to_css().is_empty());
1785    }
1786
1787    #[test]
1788    fn test_system_status_clone() {
1789        let status = SystemStatus::new(1.0, 2.0, 3.0, 4).with_thermal(50.0, 60.0);
1790        let cloned = status;
1791        assert!((cloned.load_1m - 1.0).abs() < 0.01);
1792        assert_eq!(cloned.core_count, 4);
1793        assert!(cloned.thermal.is_some());
1794    }
1795
1796    #[test]
1797    fn test_system_status_debug() {
1798        let status = SystemStatus::default();
1799        let debug = format!("{:?}", status);
1800        assert!(debug.contains("SystemStatus"));
1801    }
1802
1803    #[test]
1804    fn test_system_status_paint_no_thermal() {
1805        let mut status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1806        status.bounds = Rect::new(0.0, 0.0, 80.0, 5.0);
1807        let mut buffer = CellBuffer::new(80, 5);
1808        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1809        status.paint(&mut canvas);
1810    }
1811
1812    #[test]
1813    fn test_system_status_measure_with_thermal() {
1814        let status = SystemStatus::new(1.0, 1.0, 1.0, 4).with_thermal(60.0, 70.0);
1815        // Use loose constraints to get natural size
1816        let constraints = Constraints::loose(Size::new(80.0, 100.0));
1817        let size = status.measure(constraints);
1818        assert_eq!(size.height, 2.0); // Two lines with thermal
1819    }
1820
1821    #[test]
1822    fn test_system_status_measure_no_thermal() {
1823        let status = SystemStatus::new(1.0, 1.0, 1.0, 4);
1824        // Use loose constraints to get natural size
1825        let constraints = Constraints::loose(Size::new(80.0, 100.0));
1826        let size = status.measure(constraints);
1827        assert_eq!(size.height, 1.0); // One line without thermal
1828    }
1829
1830    // CpuConsumer additional tests
1831    #[test]
1832    fn test_cpu_consumer_clone() {
1833        let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1834        let cloned = proc;
1835        assert_eq!(cloned.pid, 123);
1836    }
1837
1838    #[test]
1839    fn test_cpu_consumer_debug() {
1840        let proc = CpuConsumer::new(123, 50.0, 1_000_000, "test");
1841        let debug = format!("{:?}", proc);
1842        assert!(debug.contains("CpuConsumer"));
1843    }
1844
1845    // HealthLevel additional tests
1846    #[test]
1847    fn test_health_level_clone() {
1848        let level = HealthLevel::Critical;
1849        let cloned = level;
1850        assert_eq!(cloned, HealthLevel::Critical);
1851    }
1852
1853    #[test]
1854    fn test_health_level_debug() {
1855        let level = HealthLevel::Ok;
1856        let debug = format!("{:?}", level);
1857        assert!(debug.contains("Ok"));
1858    }
1859
1860    #[test]
1861    fn test_health_level_copy() {
1862        let level = HealthLevel::High;
1863        let copied = level;
1864        assert_eq!(copied, HealthLevel::High);
1865    }
1866}