Skip to main content

presentar_terminal/widgets/
cpu_exploded.rs

1//! CPU Exploded View Widgets (CB-INPUT-006 Framework-First)
2//!
3//! Specialized widgets that only appear in CPU panel's exploded (fullscreen) view.
4//! These provide detailed CPU analysis that wouldn't fit in the condensed dashboard.
5//!
6//! # Widgets
7//!
8//! - [`PerCoreSparklineGrid`]: Individual sparkline history for each CPU core
9//! - [`CpuStateBreakdown`]: User/system/idle/iowait stacked bars per core
10//! - [`TopProcessesMini`]: Top 5 CPU consumers with live updates
11//! - [`FreqTempHeatmap`]: Frequency and temperature heatmap per core
12//! - [`LoadAverageTimeline`]: 1/5/15 minute load average sparklines
13
14use crate::theme::Gradient;
15use presentar_core::{
16    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
17    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
18};
19use std::any::Any;
20use std::time::Duration;
21
22// =============================================================================
23// 1. PerCoreSparklineGrid - Individual sparkline per core
24// =============================================================================
25
26/// Grid of sparklines showing per-core CPU history.
27///
28/// In condensed view, only the aggregate sparkline is shown.
29/// In exploded view, each core gets its own sparkline with 60-second history.
30#[derive(Debug, Clone)]
31pub struct PerCoreSparklineGrid {
32    /// Per-core history buffers (each is 60 samples = 60 seconds at 1s refresh)
33    pub core_histories: Vec<Vec<f64>>,
34    /// Gradient for coloring (low→high utilization)
35    gradient: Gradient,
36    /// Number of columns for grid layout
37    columns: Option<usize>,
38    /// Whether to show core labels
39    show_labels: bool,
40    /// Cached layout bounds
41    bounds: Rect,
42}
43
44impl Default for PerCoreSparklineGrid {
45    fn default() -> Self {
46        Self::new(vec![])
47    }
48}
49
50impl PerCoreSparklineGrid {
51    /// Create a new per-core sparkline grid.
52    #[must_use]
53    pub fn new(core_histories: Vec<Vec<f64>>) -> Self {
54        Self {
55            core_histories,
56            gradient: Gradient::from_hex(&["#7aa2f7", "#e0af68", "#f7768e"]),
57            columns: None,
58            show_labels: true,
59            bounds: Rect::default(),
60        }
61    }
62
63    /// Set explicit column count.
64    #[must_use]
65    pub fn with_columns(mut self, cols: usize) -> Self {
66        self.columns = Some(cols);
67        self
68    }
69
70    /// Set custom gradient.
71    #[must_use]
72    pub fn with_gradient(mut self, gradient: Gradient) -> Self {
73        self.gradient = gradient;
74        self
75    }
76
77    /// Disable core labels.
78    #[must_use]
79    pub fn without_labels(mut self) -> Self {
80        self.show_labels = false;
81        self
82    }
83
84    /// Update core histories.
85    pub fn set_histories(&mut self, histories: Vec<Vec<f64>>) {
86        self.core_histories = histories;
87    }
88
89    /// Get optimal grid dimensions.
90    fn optimal_grid(&self, max_width: usize, cell_width: usize) -> (usize, usize) {
91        let count = self.core_histories.len();
92        if count == 0 {
93            return (0, 0);
94        }
95
96        let max_cols = (max_width / cell_width).max(1);
97        let cols = self.columns.unwrap_or_else(|| {
98            // For sparklines, prefer fewer columns (wider sparklines)
99            let ideal = (count as f64).sqrt().ceil() as usize;
100            ideal.min(max_cols).min(4).max(1)
101        });
102
103        let rows = count.div_ceil(cols);
104        (cols, rows)
105    }
106}
107
108impl Brick for PerCoreSparklineGrid {
109    fn brick_name(&self) -> &'static str {
110        "per_core_sparkline_grid"
111    }
112
113    fn assertions(&self) -> &[BrickAssertion] {
114        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
115        ASSERTIONS
116    }
117
118    fn budget(&self) -> BrickBudget {
119        BrickBudget::uniform(16)
120    }
121
122    fn verify(&self) -> BrickVerification {
123        BrickVerification {
124            passed: self.assertions().to_vec(),
125            failed: vec![],
126            verification_time: Duration::from_micros(10),
127        }
128    }
129
130    fn to_html(&self) -> String {
131        String::new()
132    }
133    fn to_css(&self) -> String {
134        String::new()
135    }
136}
137
138impl Widget for PerCoreSparklineGrid {
139    fn type_id(&self) -> TypeId {
140        TypeId::of::<Self>()
141    }
142
143    fn measure(&self, constraints: Constraints) -> Size {
144        let cell_width = 20; // 3 chars label + 15 chars sparkline + 2 spacing
145        let cell_height = 2; // sparkline + label row
146        let (cols, rows) = self.optimal_grid(constraints.max_width as usize, cell_width);
147
148        let width = (cols * cell_width) as f32;
149        let height = (rows * cell_height) as f32;
150
151        constraints.constrain(Size::new(width, height))
152    }
153
154    fn layout(&mut self, bounds: Rect) -> LayoutResult {
155        self.bounds = bounds;
156        LayoutResult {
157            size: Size::new(bounds.width, bounds.height),
158        }
159    }
160
161    fn paint(&self, canvas: &mut dyn Canvas) {
162        if self.core_histories.is_empty() {
163            return;
164        }
165
166        let cell_width = 20_usize;
167        let cell_height = 2_usize;
168        let (cols, _) = self.optimal_grid(self.bounds.width as usize, cell_width);
169        if cols == 0 {
170            return;
171        }
172
173        for (i, history) in self.core_histories.iter().enumerate() {
174            let col = i % cols;
175            let row = i / cols;
176
177            let x = self.bounds.x + (col * cell_width) as f32;
178            let y = self.bounds.y + (row * cell_height) as f32;
179
180            // Get current value for color
181            let current = history.last().copied().unwrap_or(0.0);
182            let color = self.gradient.for_percent(current);
183
184            // Draw label
185            if self.show_labels {
186                let label = format!("{i:2}");
187                canvas.draw_text(
188                    &label,
189                    Point::new(x, y),
190                    &TextStyle {
191                        color,
192                        ..Default::default()
193                    },
194                );
195            }
196
197            // Draw mini sparkline
198            let sparkline_x = x + if self.show_labels { 3.0 } else { 0.0 };
199            let sparkline_width = (cell_width as f32 - 5.0).max(8.0);
200
201            // Convert history to sparkline chars
202            let chars: String = history
203                .iter()
204                .rev()
205                .take(sparkline_width as usize)
206                .rev()
207                .map(|&v| {
208                    let idx = ((v / 100.0) * 7.0).round() as usize;
209                    ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][idx.min(7)]
210                })
211                .collect();
212
213            canvas.draw_text(
214                &chars,
215                Point::new(sparkline_x, y),
216                &TextStyle {
217                    color,
218                    ..Default::default()
219                },
220            );
221
222            // Draw current percentage
223            let pct_str = format!("{current:3.0}%");
224            canvas.draw_text(
225                &pct_str,
226                Point::new(sparkline_x + sparkline_width, y),
227                &TextStyle {
228                    color,
229                    ..Default::default()
230                },
231            );
232        }
233    }
234
235    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
236        None
237    }
238
239    fn children(&self) -> &[Box<dyn Widget>] {
240        &[]
241    }
242
243    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
244        &mut []
245    }
246}
247
248// =============================================================================
249// 2. CpuStateBreakdown - User/System/Idle/IOWait stacked bars
250// =============================================================================
251
252/// CPU state breakdown showing user/system/idle/iowait as stacked bars.
253#[derive(Debug, Clone)]
254pub struct CpuStateBreakdown {
255    /// Per-core CPU states: (user%, system%, idle%, iowait%, irq%, softirq%)
256    pub core_states: Vec<CpuCoreState>,
257    /// Cached layout bounds
258    bounds: Rect,
259}
260
261/// CPU state percentages for a single core.
262#[derive(Debug, Clone, Default)]
263pub struct CpuCoreState {
264    pub user: f64,
265    pub system: f64,
266    pub idle: f64,
267    pub iowait: f64,
268    pub irq: f64,
269    pub softirq: f64,
270}
271
272impl CpuCoreState {
273    /// Create from percentages.
274    pub fn new(user: f64, system: f64, idle: f64, iowait: f64, irq: f64, softirq: f64) -> Self {
275        Self {
276            user,
277            system,
278            idle,
279            iowait,
280            irq,
281            softirq,
282        }
283    }
284}
285
286impl Default for CpuStateBreakdown {
287    fn default() -> Self {
288        Self::new(vec![])
289    }
290}
291
292impl CpuStateBreakdown {
293    /// Create a new CPU state breakdown widget.
294    #[must_use]
295    pub fn new(core_states: Vec<CpuCoreState>) -> Self {
296        Self {
297            core_states,
298            bounds: Rect::default(),
299        }
300    }
301
302    /// Update core states.
303    pub fn set_states(&mut self, states: Vec<CpuCoreState>) {
304        self.core_states = states;
305    }
306}
307
308// State colors (Tokyo Night inspired) - helper function
309fn state_colors() -> [Color; 6] {
310    [
311        Color::rgb(0.478, 0.635, 0.969), // USER: Blue #7aa2f7
312        Color::rgb(0.878, 0.686, 0.408), // SYSTEM: Yellow #e0af68
313        Color::rgb(0.969, 0.463, 0.557), // IOWAIT: Red #f7768e
314        Color::rgb(0.733, 0.604, 0.969), // IRQ: Purple #bb9af7
315        Color::rgb(0.620, 0.808, 0.416), // SOFTIRQ: Green #9ece6a
316        Color::rgb(0.337, 0.373, 0.537), // IDLE: Gray #565f89
317    ]
318}
319
320impl Brick for CpuStateBreakdown {
321    fn brick_name(&self) -> &'static str {
322        "cpu_state_breakdown"
323    }
324
325    fn assertions(&self) -> &[BrickAssertion] {
326        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
327        ASSERTIONS
328    }
329
330    fn budget(&self) -> BrickBudget {
331        BrickBudget::uniform(16)
332    }
333
334    fn verify(&self) -> BrickVerification {
335        BrickVerification {
336            passed: self.assertions().to_vec(),
337            failed: vec![],
338            verification_time: Duration::from_micros(10),
339        }
340    }
341
342    fn to_html(&self) -> String {
343        String::new()
344    }
345    fn to_css(&self) -> String {
346        String::new()
347    }
348}
349
350impl Widget for CpuStateBreakdown {
351    fn type_id(&self) -> TypeId {
352        TypeId::of::<Self>()
353    }
354
355    fn measure(&self, constraints: Constraints) -> Size {
356        let height = self.core_states.len().min(constraints.max_height as usize) as f32;
357        constraints.constrain(Size::new(constraints.max_width, height))
358    }
359
360    fn layout(&mut self, bounds: Rect) -> LayoutResult {
361        self.bounds = bounds;
362        LayoutResult {
363            size: Size::new(bounds.width, bounds.height),
364        }
365    }
366
367    fn paint(&self, canvas: &mut dyn Canvas) {
368        let bar_width = (self.bounds.width as usize).saturating_sub(20); // Reserve for labels
369
370        for (i, state) in self.core_states.iter().enumerate() {
371            let y = self.bounds.y + i as f32;
372            if y >= self.bounds.y + self.bounds.height {
373                break;
374            }
375
376            // Draw label
377            let label = format!("{i:2}:");
378            canvas.draw_text(&label, Point::new(self.bounds.x, y), &TextStyle::default());
379
380            // Calculate bar segments
381            let total =
382                state.user + state.system + state.idle + state.iowait + state.irq + state.softirq;
383            if total <= 0.0 {
384                continue;
385            }
386
387            let mut x = self.bounds.x + 4.0;
388            let colors = state_colors();
389            let segments = [
390                (state.user, colors[0], '█'),    // USER
391                (state.system, colors[1], '▓'),  // SYSTEM
392                (state.iowait, colors[2], '░'),  // IOWAIT
393                (state.irq, colors[3], '▒'),     // IRQ
394                (state.softirq, colors[4], '░'), // SOFTIRQ
395                (state.idle, colors[5], '·'),    // IDLE
396            ];
397
398            for (pct, color, ch) in segments {
399                let width = ((pct / total) * bar_width as f64).round() as usize;
400                if width > 0 {
401                    let bar: String = std::iter::repeat_n(ch, width).collect();
402                    canvas.draw_text(
403                        &bar,
404                        Point::new(x, y),
405                        &TextStyle {
406                            color,
407                            ..Default::default()
408                        },
409                    );
410                    x += width as f32;
411                }
412            }
413
414            // Draw percentage summary
415            let summary = format!(
416                " u:{:2.0} s:{:2.0} w:{:2.0}",
417                state.user, state.system, state.iowait
418            );
419            canvas.draw_text(&summary, Point::new(x + 1.0, y), &TextStyle::default());
420        }
421    }
422
423    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
424        None
425    }
426
427    fn children(&self) -> &[Box<dyn Widget>] {
428        &[]
429    }
430
431    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
432        &mut []
433    }
434}
435
436// =============================================================================
437// 3. TopProcessesMini - Top 5 CPU consumers
438// =============================================================================
439
440/// Mini table showing top 5 CPU-consuming processes.
441#[derive(Debug, Clone)]
442pub struct TopProcessesMini {
443    /// Top processes: (pid, cpu%, name)
444    pub processes: Vec<TopProcess>,
445    /// Cached layout bounds
446    bounds: Rect,
447}
448
449/// A top CPU-consuming process.
450#[derive(Debug, Clone)]
451pub struct TopProcess {
452    pub pid: u32,
453    pub cpu_percent: f32,
454    pub name: String,
455}
456
457impl TopProcess {
458    pub fn new(pid: u32, cpu_percent: f32, name: impl Into<String>) -> Self {
459        Self {
460            pid,
461            cpu_percent,
462            name: name.into(),
463        }
464    }
465}
466
467impl Default for TopProcessesMini {
468    fn default() -> Self {
469        Self::new(vec![])
470    }
471}
472
473impl TopProcessesMini {
474    /// Create a new top processes widget.
475    #[must_use]
476    pub fn new(processes: Vec<TopProcess>) -> Self {
477        Self {
478            processes,
479            bounds: Rect::default(),
480        }
481    }
482
483    /// Update processes list.
484    pub fn set_processes(&mut self, processes: Vec<TopProcess>) {
485        self.processes = processes;
486    }
487}
488
489impl Brick for TopProcessesMini {
490    fn brick_name(&self) -> &'static str {
491        "top_processes_mini"
492    }
493
494    fn assertions(&self) -> &[BrickAssertion] {
495        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
496        ASSERTIONS
497    }
498
499    fn budget(&self) -> BrickBudget {
500        BrickBudget::uniform(16)
501    }
502
503    fn verify(&self) -> BrickVerification {
504        BrickVerification {
505            passed: self.assertions().to_vec(),
506            failed: vec![],
507            verification_time: Duration::from_micros(10),
508        }
509    }
510
511    fn to_html(&self) -> String {
512        String::new()
513    }
514    fn to_css(&self) -> String {
515        String::new()
516    }
517}
518
519impl Widget for TopProcessesMini {
520    fn type_id(&self) -> TypeId {
521        TypeId::of::<Self>()
522    }
523
524    fn measure(&self, constraints: Constraints) -> Size {
525        let height = (self.processes.len().min(5) + 1) as f32; // +1 for header
526        constraints.constrain(Size::new(constraints.max_width, height))
527    }
528
529    fn layout(&mut self, bounds: Rect) -> LayoutResult {
530        self.bounds = bounds;
531        LayoutResult {
532            size: Size::new(bounds.width, bounds.height),
533        }
534    }
535
536    fn paint(&self, canvas: &mut dyn Canvas) {
537        // Header
538        let header = " PID    %CPU  Command";
539        canvas.draw_text(
540            header,
541            Point::new(self.bounds.x, self.bounds.y),
542            &TextStyle {
543                color: Color::rgb(0.663, 0.694, 0.839), // Gray #a9b1d6
544                ..Default::default()
545            },
546        );
547
548        // Gradient for CPU usage
549        let gradient = Gradient::from_hex(&["#7aa2f7", "#e0af68", "#f7768e"]);
550
551        for (i, proc) in self.processes.iter().take(5).enumerate() {
552            let y = self.bounds.y + (i + 1) as f32;
553            if y >= self.bounds.y + self.bounds.height {
554                break;
555            }
556
557            let color = gradient.for_percent(proc.cpu_percent as f64);
558            let max_name_len = (self.bounds.width as usize).saturating_sub(15);
559            let name = if proc.name.len() > max_name_len {
560                format!("{}...", &proc.name[..max_name_len.saturating_sub(3)])
561            } else {
562                proc.name.clone()
563            };
564
565            let line = format!("{:5} {:5.1}  {}", proc.pid, proc.cpu_percent, name);
566            canvas.draw_text(
567                &line,
568                Point::new(self.bounds.x, y),
569                &TextStyle {
570                    color,
571                    ..Default::default()
572                },
573            );
574        }
575    }
576
577    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
578        None
579    }
580
581    fn children(&self) -> &[Box<dyn Widget>] {
582        &[]
583    }
584
585    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
586        &mut []
587    }
588}
589
590// =============================================================================
591// 4. FreqTempHeatmap - Frequency and Temperature per core
592// =============================================================================
593
594/// Heatmap showing frequency and temperature per core.
595#[derive(Debug, Clone)]
596pub struct FreqTempHeatmap {
597    /// Per-core frequencies in MHz
598    pub frequencies: Vec<u32>,
599    /// Per-core max frequencies in MHz
600    pub max_frequencies: Vec<u32>,
601    /// Per-core temperatures in Celsius (optional)
602    pub temperatures: Option<Vec<f32>>,
603    /// Cached layout bounds
604    bounds: Rect,
605}
606
607impl Default for FreqTempHeatmap {
608    fn default() -> Self {
609        Self::new(vec![], vec![])
610    }
611}
612
613impl FreqTempHeatmap {
614    /// Create a new frequency/temperature heatmap.
615    #[must_use]
616    pub fn new(frequencies: Vec<u32>, max_frequencies: Vec<u32>) -> Self {
617        Self {
618            frequencies,
619            max_frequencies,
620            temperatures: None,
621            bounds: Rect::default(),
622        }
623    }
624
625    /// Add temperature data.
626    #[must_use]
627    pub fn with_temperatures(mut self, temps: Vec<f32>) -> Self {
628        self.temperatures = Some(temps);
629        self
630    }
631
632    /// Update data.
633    pub fn set_data(&mut self, freqs: Vec<u32>, max_freqs: Vec<u32>, temps: Option<Vec<f32>>) {
634        self.frequencies = freqs;
635        self.max_frequencies = max_freqs;
636        self.temperatures = temps;
637    }
638}
639
640impl Brick for FreqTempHeatmap {
641    fn brick_name(&self) -> &'static str {
642        "freq_temp_heatmap"
643    }
644
645    fn assertions(&self) -> &[BrickAssertion] {
646        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
647        ASSERTIONS
648    }
649
650    fn budget(&self) -> BrickBudget {
651        BrickBudget::uniform(16)
652    }
653
654    fn verify(&self) -> BrickVerification {
655        BrickVerification {
656            passed: self.assertions().to_vec(),
657            failed: vec![],
658            verification_time: Duration::from_micros(10),
659        }
660    }
661
662    fn to_html(&self) -> String {
663        String::new()
664    }
665    fn to_css(&self) -> String {
666        String::new()
667    }
668}
669
670impl Widget for FreqTempHeatmap {
671    fn type_id(&self) -> TypeId {
672        TypeId::of::<Self>()
673    }
674
675    fn measure(&self, constraints: Constraints) -> Size {
676        let has_temps = self.temperatures.is_some();
677        let height = if has_temps { 4.0 } else { 2.0 }; // 2 rows per heatmap
678        constraints.constrain(Size::new(constraints.max_width, height))
679    }
680
681    fn layout(&mut self, bounds: Rect) -> LayoutResult {
682        self.bounds = bounds;
683        LayoutResult {
684            size: Size::new(bounds.width, bounds.height),
685        }
686    }
687
688    fn paint(&self, canvas: &mut dyn Canvas) {
689        if self.frequencies.is_empty() {
690            return;
691        }
692
693        let freq_gradient = Gradient::from_hex(&["#565f89", "#7aa2f7", "#9ece6a"]); // Gray→Blue→Green
694        let temp_gradient = Gradient::from_hex(&["#9ece6a", "#e0af68", "#f7768e"]); // Green→Yellow→Red
695
696        // Calculate cell width
697        let cell_width = 4_usize;
698        let cores_per_row = (self.bounds.width as usize / cell_width).max(1);
699
700        // Frequency row
701        canvas.draw_text(
702            "Freq:",
703            Point::new(self.bounds.x, self.bounds.y),
704            &TextStyle::default(),
705        );
706        for (i, (&freq, &max_freq)) in self
707            .frequencies
708            .iter()
709            .zip(self.max_frequencies.iter())
710            .enumerate()
711        {
712            let col = i % cores_per_row;
713            let row = i / cores_per_row;
714            let x = self.bounds.x + 6.0 + (col * cell_width) as f32;
715            let y = self.bounds.y + row as f32;
716
717            let pct = if max_freq > 0 {
718                (freq as f64 / max_freq as f64) * 100.0
719            } else {
720                0.0
721            };
722            let color = freq_gradient.for_percent(pct);
723            let level = ((pct / 100.0) * 7.0).round() as usize;
724            let ch = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][level.min(7)];
725
726            canvas.draw_text(
727                &ch.to_string(),
728                Point::new(x, y),
729                &TextStyle {
730                    color,
731                    ..Default::default()
732                },
733            );
734        }
735
736        // Temperature row (if available)
737        if let Some(ref temps) = self.temperatures {
738            let temp_y = self.bounds.y + 2.0;
739            canvas.draw_text(
740                "Temp:",
741                Point::new(self.bounds.x, temp_y),
742                &TextStyle::default(),
743            );
744
745            for (i, &temp) in temps.iter().enumerate() {
746                let col = i % cores_per_row;
747                let row = i / cores_per_row;
748                let x = self.bounds.x + 6.0 + (col * cell_width) as f32;
749                let y = temp_y + row as f32;
750
751                // Map temperature 30-100°C to 0-100%
752                let pct = ((temp - 30.0) / 70.0 * 100.0).clamp(0.0, 100.0) as f64;
753                let color = temp_gradient.for_percent(pct);
754                let level = ((pct / 100.0) * 7.0).round() as usize;
755                let ch = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][level.min(7)];
756
757                canvas.draw_text(
758                    &ch.to_string(),
759                    Point::new(x, y),
760                    &TextStyle {
761                        color,
762                        ..Default::default()
763                    },
764                );
765            }
766        }
767    }
768
769    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
770        None
771    }
772
773    fn children(&self) -> &[Box<dyn Widget>] {
774        &[]
775    }
776
777    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
778        &mut []
779    }
780}
781
782// =============================================================================
783// 5. LoadAverageTimeline - 1/5/15 minute load average sparklines
784// =============================================================================
785
786/// Three sparklines showing load average history.
787#[derive(Debug, Clone)]
788pub struct LoadAverageTimeline {
789    /// 1-minute load average history
790    pub load_1m: Vec<f64>,
791    /// 5-minute load average history
792    pub load_5m: Vec<f64>,
793    /// 15-minute load average history
794    pub load_15m: Vec<f64>,
795    /// Number of CPU cores (for relative scaling)
796    pub core_count: usize,
797    /// Cached layout bounds
798    bounds: Rect,
799}
800
801impl Default for LoadAverageTimeline {
802    fn default() -> Self {
803        Self::new(1)
804    }
805}
806
807impl LoadAverageTimeline {
808    /// Create a new load average timeline.
809    #[must_use]
810    pub fn new(core_count: usize) -> Self {
811        Self {
812            load_1m: Vec::new(),
813            load_5m: Vec::new(),
814            load_15m: Vec::new(),
815            core_count: core_count.max(1),
816            bounds: Rect::default(),
817        }
818    }
819
820    /// Push new load average values.
821    pub fn push(&mut self, load_1m: f64, load_5m: f64, load_15m: f64) {
822        const MAX_HISTORY: usize = 60;
823
824        self.load_1m.push(load_1m);
825        self.load_5m.push(load_5m);
826        self.load_15m.push(load_15m);
827
828        if self.load_1m.len() > MAX_HISTORY {
829            self.load_1m.remove(0);
830        }
831        if self.load_5m.len() > MAX_HISTORY {
832            self.load_5m.remove(0);
833        }
834        if self.load_15m.len() > MAX_HISTORY {
835            self.load_15m.remove(0);
836        }
837    }
838
839    /// Set core count for relative scaling.
840    pub fn set_core_count(&mut self, count: usize) {
841        self.core_count = count.max(1);
842    }
843}
844
845impl Brick for LoadAverageTimeline {
846    fn brick_name(&self) -> &'static str {
847        "load_average_timeline"
848    }
849
850    fn assertions(&self) -> &[BrickAssertion] {
851        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
852        ASSERTIONS
853    }
854
855    fn budget(&self) -> BrickBudget {
856        BrickBudget::uniform(16)
857    }
858
859    fn verify(&self) -> BrickVerification {
860        BrickVerification {
861            passed: self.assertions().to_vec(),
862            failed: vec![],
863            verification_time: Duration::from_micros(10),
864        }
865    }
866
867    fn to_html(&self) -> String {
868        String::new()
869    }
870    fn to_css(&self) -> String {
871        String::new()
872    }
873}
874
875impl Widget for LoadAverageTimeline {
876    fn type_id(&self) -> TypeId {
877        TypeId::of::<Self>()
878    }
879
880    fn measure(&self, constraints: Constraints) -> Size {
881        constraints.constrain(Size::new(constraints.max_width, 3.0))
882    }
883
884    fn layout(&mut self, bounds: Rect) -> LayoutResult {
885        self.bounds = bounds;
886        LayoutResult {
887            size: Size::new(bounds.width, bounds.height),
888        }
889    }
890
891    fn paint(&self, canvas: &mut dyn Canvas) {
892        let gradient = Gradient::from_hex(&["#9ece6a", "#e0af68", "#f7768e"]); // Green→Yellow→Red
893        let sparkline_width = (self.bounds.width as usize).saturating_sub(20);
894
895        let loads = [
896            (" 1m:", &self.load_1m),
897            (" 5m:", &self.load_5m),
898            ("15m:", &self.load_15m),
899        ];
900
901        for (row, (label, history)) in loads.iter().enumerate() {
902            let y = self.bounds.y + row as f32;
903            if y >= self.bounds.y + self.bounds.height {
904                break;
905            }
906
907            // Draw label
908            canvas.draw_text(label, Point::new(self.bounds.x, y), &TextStyle::default());
909
910            if history.is_empty() {
911                continue;
912            }
913
914            // Current value
915            let current = history.last().copied().unwrap_or(0.0);
916            let load_pct = (current / self.core_count as f64 * 100.0).min(200.0);
917            let color = gradient.for_percent(load_pct.min(100.0));
918
919            // Draw sparkline
920            let chars: String = history
921                .iter()
922                .rev()
923                .take(sparkline_width)
924                .rev()
925                .map(|&v| {
926                    let pct = (v / self.core_count as f64 * 100.0).min(200.0);
927                    let idx = ((pct / 200.0) * 7.0).round() as usize;
928                    ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'][idx.min(7)]
929                })
930                .collect();
931
932            canvas.draw_text(
933                &chars,
934                Point::new(self.bounds.x + 5.0, y),
935                &TextStyle {
936                    color,
937                    ..Default::default()
938                },
939            );
940
941            // Current value
942            let value_str = format!(" ({current:.2})");
943            canvas.draw_text(
944                &value_str,
945                Point::new(self.bounds.x + 5.0 + sparkline_width as f32, y),
946                &TextStyle {
947                    color,
948                    ..Default::default()
949                },
950            );
951        }
952    }
953
954    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
955        None
956    }
957
958    fn children(&self) -> &[Box<dyn Widget>] {
959        &[]
960    }
961
962    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
963        &mut []
964    }
965}
966
967#[cfg(test)]
968#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
969mod tests {
970    use super::*;
971    use crate::direct::{CellBuffer, DirectTerminalCanvas};
972
973    // ==================== PerCoreSparklineGrid Tests ====================
974
975    #[test]
976    fn test_per_core_sparkline_grid_new() {
977        let grid = PerCoreSparklineGrid::new(vec![vec![10.0, 20.0, 30.0]; 4]);
978        assert_eq!(grid.core_histories.len(), 4);
979    }
980
981    #[test]
982    fn test_per_core_sparkline_grid_default() {
983        let grid = PerCoreSparklineGrid::default();
984        assert!(grid.core_histories.is_empty());
985    }
986
987    #[test]
988    fn test_per_core_sparkline_grid_with_columns() {
989        let grid = PerCoreSparklineGrid::new(vec![vec![10.0]; 8]).with_columns(4);
990        assert_eq!(grid.columns, Some(4));
991    }
992
993    #[test]
994    fn test_per_core_sparkline_grid_with_gradient() {
995        let custom_gradient = Gradient::from_hex(&["#ff0000", "#00ff00"]);
996        let grid = PerCoreSparklineGrid::new(vec![vec![10.0]; 4]).with_gradient(custom_gradient);
997        // Gradient is set (can't easily test internal value, but no panic)
998        assert!(!grid.core_histories.is_empty());
999    }
1000
1001    #[test]
1002    fn test_per_core_sparkline_grid_without_labels() {
1003        let grid = PerCoreSparklineGrid::new(vec![vec![10.0]; 4]).without_labels();
1004        assert!(!grid.show_labels);
1005    }
1006
1007    #[test]
1008    fn test_per_core_sparkline_grid_set_histories() {
1009        let mut grid = PerCoreSparklineGrid::new(vec![]);
1010        assert!(grid.core_histories.is_empty());
1011        grid.set_histories(vec![vec![10.0, 20.0], vec![30.0, 40.0]]);
1012        assert_eq!(grid.core_histories.len(), 2);
1013    }
1014
1015    #[test]
1016    fn test_per_core_sparkline_grid_optimal_grid_empty() {
1017        let grid = PerCoreSparklineGrid::new(vec![]);
1018        let (cols, rows) = grid.optimal_grid(80, 20);
1019        assert_eq!((cols, rows), (0, 0));
1020    }
1021
1022    #[test]
1023    fn test_per_core_sparkline_grid_optimal_grid_with_explicit_cols() {
1024        let grid = PerCoreSparklineGrid::new(vec![vec![10.0]; 8]).with_columns(2);
1025        let (cols, rows) = grid.optimal_grid(80, 20);
1026        assert_eq!(cols, 2);
1027        assert_eq!(rows, 4);
1028    }
1029
1030    #[test]
1031    fn test_per_core_sparkline_grid_paint() {
1032        let mut grid = PerCoreSparklineGrid::new(vec![
1033            vec![10.0, 20.0, 30.0, 40.0, 50.0],
1034            vec![60.0, 70.0, 80.0, 90.0, 100.0],
1035            vec![50.0, 50.0, 50.0, 50.0, 50.0],
1036            vec![5.0, 10.0, 15.0, 20.0, 25.0],
1037        ]);
1038        let mut buffer = CellBuffer::new(80, 20);
1039        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1040        grid.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1041        grid.paint(&mut canvas);
1042    }
1043
1044    #[test]
1045    fn test_per_core_sparkline_grid_paint_empty() {
1046        let mut grid = PerCoreSparklineGrid::new(vec![]);
1047        let mut buffer = CellBuffer::new(80, 20);
1048        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1049        grid.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1050        grid.paint(&mut canvas); // Should not panic
1051    }
1052
1053    #[test]
1054    fn test_per_core_sparkline_grid_paint_without_labels() {
1055        let mut grid = PerCoreSparklineGrid::new(vec![vec![50.0; 10]; 4]).without_labels();
1056        let mut buffer = CellBuffer::new(80, 20);
1057        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1058        grid.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1059        grid.paint(&mut canvas);
1060    }
1061
1062    #[test]
1063    fn test_per_core_sparkline_grid_paint_with_empty_history() {
1064        let mut grid = PerCoreSparklineGrid::new(vec![vec![], vec![]]);
1065        let mut buffer = CellBuffer::new(80, 20);
1066        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1067        grid.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1068        grid.paint(&mut canvas);
1069    }
1070
1071    #[test]
1072    fn test_per_core_sparkline_grid_measure() {
1073        let grid = PerCoreSparklineGrid::new(vec![vec![10.0]; 4]);
1074        let size = grid.measure(Constraints {
1075            min_width: 0.0,
1076            max_width: 80.0,
1077            min_height: 0.0,
1078            max_height: 40.0,
1079        });
1080        assert!(size.width > 0.0);
1081    }
1082
1083    #[test]
1084    fn test_per_core_sparkline_grid_measure_empty() {
1085        let grid = PerCoreSparklineGrid::new(vec![]);
1086        let size = grid.measure(Constraints {
1087            min_width: 0.0,
1088            max_width: 80.0,
1089            min_height: 0.0,
1090            max_height: 40.0,
1091        });
1092        assert!((size.width - 0.0).abs() < f32::EPSILON);
1093    }
1094
1095    #[test]
1096    fn test_per_core_sparkline_grid_event() {
1097        let mut grid = PerCoreSparklineGrid::default();
1098        let event = Event::FocusIn;
1099        assert!(grid.event(&event).is_none());
1100    }
1101
1102    #[test]
1103    fn test_per_core_sparkline_grid_children() {
1104        let grid = PerCoreSparklineGrid::default();
1105        assert!(grid.children().is_empty());
1106    }
1107
1108    #[test]
1109    fn test_per_core_sparkline_grid_children_mut() {
1110        let mut grid = PerCoreSparklineGrid::default();
1111        assert!(grid.children_mut().is_empty());
1112    }
1113
1114    #[test]
1115    fn test_per_core_sparkline_grid_to_html_css() {
1116        let grid = PerCoreSparklineGrid::default();
1117        assert!(grid.to_html().is_empty());
1118        assert!(grid.to_css().is_empty());
1119    }
1120
1121    #[test]
1122    fn test_per_core_sparkline_grid_budget() {
1123        let grid = PerCoreSparklineGrid::default();
1124        let budget = grid.budget();
1125        assert!(budget.measure_ms > 0);
1126    }
1127
1128    #[test]
1129    fn test_per_core_sparkline_grid_type_id() {
1130        let grid = PerCoreSparklineGrid::default();
1131        let type_id = Widget::type_id(&grid);
1132        assert_eq!(type_id, TypeId::of::<PerCoreSparklineGrid>());
1133    }
1134
1135    #[test]
1136    fn test_per_core_sparkline_grid_clone() {
1137        let grid = PerCoreSparklineGrid::new(vec![vec![10.0, 20.0]]);
1138        let cloned = grid;
1139        assert_eq!(cloned.core_histories.len(), 1);
1140    }
1141
1142    #[test]
1143    fn test_per_core_sparkline_grid_debug() {
1144        let grid = PerCoreSparklineGrid::new(vec![vec![10.0]]);
1145        let debug = format!("{:?}", grid);
1146        assert!(debug.contains("PerCoreSparklineGrid"));
1147    }
1148
1149    // ==================== CpuStateBreakdown Tests ====================
1150
1151    #[test]
1152    fn test_cpu_state_breakdown_new() {
1153        let breakdown =
1154            CpuStateBreakdown::new(vec![CpuCoreState::new(50.0, 10.0, 35.0, 5.0, 0.0, 0.0)]);
1155        assert_eq!(breakdown.core_states.len(), 1);
1156    }
1157
1158    #[test]
1159    fn test_cpu_state_breakdown_default() {
1160        let breakdown = CpuStateBreakdown::default();
1161        assert!(breakdown.core_states.is_empty());
1162    }
1163
1164    #[test]
1165    fn test_cpu_state_breakdown_set_states() {
1166        let mut breakdown = CpuStateBreakdown::default();
1167        breakdown.set_states(vec![
1168            CpuCoreState::new(50.0, 10.0, 35.0, 5.0, 0.0, 0.0),
1169            CpuCoreState::new(30.0, 20.0, 45.0, 5.0, 0.0, 0.0),
1170        ]);
1171        assert_eq!(breakdown.core_states.len(), 2);
1172    }
1173
1174    #[test]
1175    fn test_cpu_state_breakdown_paint() {
1176        let mut breakdown = CpuStateBreakdown::new(vec![
1177            CpuCoreState::new(50.0, 10.0, 35.0, 5.0, 0.0, 0.0),
1178            CpuCoreState::new(70.0, 15.0, 10.0, 5.0, 0.0, 0.0),
1179            CpuCoreState::new(30.0, 5.0, 60.0, 5.0, 0.0, 0.0),
1180        ]);
1181        let mut buffer = CellBuffer::new(80, 20);
1182        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1183        breakdown.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1184        breakdown.paint(&mut canvas);
1185    }
1186
1187    #[test]
1188    fn test_cpu_state_breakdown_paint_empty() {
1189        let mut breakdown = CpuStateBreakdown::default();
1190        let mut buffer = CellBuffer::new(80, 20);
1191        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1192        breakdown.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1193        breakdown.paint(&mut canvas);
1194    }
1195
1196    #[test]
1197    fn test_cpu_state_breakdown_paint_zero_total() {
1198        let mut breakdown =
1199            CpuStateBreakdown::new(vec![CpuCoreState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)]);
1200        let mut buffer = CellBuffer::new(80, 20);
1201        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1202        breakdown.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1203        breakdown.paint(&mut canvas);
1204    }
1205
1206    #[test]
1207    fn test_cpu_state_breakdown_paint_with_irq() {
1208        let mut breakdown =
1209            CpuStateBreakdown::new(vec![CpuCoreState::new(30.0, 10.0, 40.0, 5.0, 10.0, 5.0)]);
1210        let mut buffer = CellBuffer::new(80, 20);
1211        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1212        breakdown.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1213        breakdown.paint(&mut canvas);
1214    }
1215
1216    #[test]
1217    fn test_cpu_state_breakdown_measure() {
1218        let breakdown =
1219            CpuStateBreakdown::new(vec![CpuCoreState::default(), CpuCoreState::default()]);
1220        let size = breakdown.measure(Constraints {
1221            min_width: 0.0,
1222            max_width: 80.0,
1223            min_height: 0.0,
1224            max_height: 40.0,
1225        });
1226        assert!(size.height >= 2.0);
1227    }
1228
1229    #[test]
1230    fn test_cpu_state_breakdown_event() {
1231        let mut breakdown = CpuStateBreakdown::default();
1232        assert!(breakdown.event(&Event::FocusIn).is_none());
1233    }
1234
1235    #[test]
1236    fn test_cpu_state_breakdown_children() {
1237        let breakdown = CpuStateBreakdown::default();
1238        assert!(breakdown.children().is_empty());
1239    }
1240
1241    #[test]
1242    fn test_cpu_state_breakdown_to_html_css() {
1243        let breakdown = CpuStateBreakdown::default();
1244        assert!(breakdown.to_html().is_empty());
1245        assert!(breakdown.to_css().is_empty());
1246    }
1247
1248    #[test]
1249    fn test_cpu_state_breakdown_type_id() {
1250        let breakdown = CpuStateBreakdown::default();
1251        assert_eq!(
1252            Widget::type_id(&breakdown),
1253            TypeId::of::<CpuStateBreakdown>()
1254        );
1255    }
1256
1257    #[test]
1258    fn test_cpu_state_breakdown_clone() {
1259        let breakdown = CpuStateBreakdown::new(vec![CpuCoreState::default()]);
1260        let cloned = breakdown;
1261        assert_eq!(cloned.core_states.len(), 1);
1262    }
1263
1264    #[test]
1265    fn test_cpu_state_breakdown_debug() {
1266        let breakdown = CpuStateBreakdown::default();
1267        let debug = format!("{:?}", breakdown);
1268        assert!(debug.contains("CpuStateBreakdown"));
1269    }
1270
1271    // ==================== CpuCoreState Tests ====================
1272
1273    #[test]
1274    fn test_cpu_core_state_fields() {
1275        let state = CpuCoreState::new(50.0, 10.0, 35.0, 5.0, 0.0, 0.0);
1276        assert!((state.user - 50.0).abs() < 0.001);
1277        assert!((state.system - 10.0).abs() < 0.001);
1278        assert!((state.idle - 35.0).abs() < 0.001);
1279        assert!((state.iowait - 5.0).abs() < 0.001);
1280    }
1281
1282    #[test]
1283    fn test_cpu_core_state_default() {
1284        let state = CpuCoreState::default();
1285        assert!((state.user - 0.0).abs() < 0.001);
1286        assert!((state.system - 0.0).abs() < 0.001);
1287        assert!((state.idle - 0.0).abs() < 0.001);
1288        assert!((state.iowait - 0.0).abs() < 0.001);
1289        assert!((state.irq - 0.0).abs() < 0.001);
1290        assert!((state.softirq - 0.0).abs() < 0.001);
1291    }
1292
1293    #[test]
1294    fn test_cpu_core_state_clone() {
1295        let state = CpuCoreState::new(50.0, 10.0, 35.0, 5.0, 0.0, 0.0);
1296        let cloned = state;
1297        assert!((cloned.user - 50.0).abs() < 0.001);
1298    }
1299
1300    #[test]
1301    fn test_cpu_core_state_debug() {
1302        let state = CpuCoreState::new(50.0, 10.0, 35.0, 5.0, 0.0, 0.0);
1303        let debug = format!("{:?}", state);
1304        assert!(debug.contains("CpuCoreState"));
1305        assert!(debug.contains("50"));
1306    }
1307
1308    // ==================== TopProcessesMini Tests ====================
1309
1310    #[test]
1311    fn test_top_processes_mini_new() {
1312        let top = TopProcessesMini::new(vec![TopProcess::new(1234, 45.2, "firefox")]);
1313        assert_eq!(top.processes.len(), 1);
1314    }
1315
1316    #[test]
1317    fn test_top_processes_mini_default() {
1318        let top = TopProcessesMini::default();
1319        assert!(top.processes.is_empty());
1320    }
1321
1322    #[test]
1323    fn test_top_processes_mini_set_processes() {
1324        let mut top = TopProcessesMini::default();
1325        top.set_processes(vec![
1326            TopProcess::new(1234, 45.2, "firefox"),
1327            TopProcess::new(5678, 30.0, "chrome"),
1328        ]);
1329        assert_eq!(top.processes.len(), 2);
1330    }
1331
1332    #[test]
1333    fn test_top_processes_mini_paint() {
1334        let mut top = TopProcessesMini::new(vec![
1335            TopProcess::new(1234, 45.2, "firefox"),
1336            TopProcess::new(5678, 30.0, "chrome"),
1337            TopProcess::new(9012, 15.5, "vscode"),
1338        ]);
1339        let mut buffer = CellBuffer::new(40, 10);
1340        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1341        top.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
1342        top.paint(&mut canvas);
1343    }
1344
1345    #[test]
1346    fn test_top_processes_mini_paint_empty() {
1347        let mut top = TopProcessesMini::default();
1348        let mut buffer = CellBuffer::new(40, 10);
1349        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1350        top.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
1351        top.paint(&mut canvas);
1352    }
1353
1354    #[test]
1355    fn test_top_processes_mini_paint_long_name_truncation() {
1356        let mut top = TopProcessesMini::new(vec![TopProcess::new(
1357            1234,
1358            45.2,
1359            "this_is_a_very_long_process_name_that_should_be_truncated",
1360        )]);
1361        let mut buffer = CellBuffer::new(30, 10);
1362        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1363        top.layout(Rect::new(0.0, 0.0, 30.0, 10.0));
1364        top.paint(&mut canvas);
1365    }
1366
1367    #[test]
1368    fn test_top_processes_mini_paint_more_than_five() {
1369        let mut top = TopProcessesMini::new(vec![
1370            TopProcess::new(1, 90.0, "proc1"),
1371            TopProcess::new(2, 80.0, "proc2"),
1372            TopProcess::new(3, 70.0, "proc3"),
1373            TopProcess::new(4, 60.0, "proc4"),
1374            TopProcess::new(5, 50.0, "proc5"),
1375            TopProcess::new(6, 40.0, "proc6"),
1376            TopProcess::new(7, 30.0, "proc7"),
1377        ]);
1378        let mut buffer = CellBuffer::new(40, 10);
1379        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1380        top.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
1381        top.paint(&mut canvas); // Should only show first 5
1382    }
1383
1384    #[test]
1385    fn test_top_processes_mini_measure() {
1386        let top = TopProcessesMini::new(vec![
1387            TopProcess::new(1234, 45.2, "firefox"),
1388            TopProcess::new(5678, 30.0, "chrome"),
1389        ]);
1390        let size = top.measure(Constraints {
1391            min_width: 0.0,
1392            max_width: 40.0,
1393            min_height: 0.0,
1394            max_height: 20.0,
1395        });
1396        assert_eq!(size.height, 3.0); // header + 2 processes
1397    }
1398
1399    #[test]
1400    fn test_top_processes_mini_event() {
1401        let mut top = TopProcessesMini::default();
1402        assert!(top.event(&Event::FocusIn).is_none());
1403    }
1404
1405    #[test]
1406    fn test_top_processes_mini_children() {
1407        let top = TopProcessesMini::default();
1408        assert!(top.children().is_empty());
1409    }
1410
1411    #[test]
1412    fn test_top_processes_mini_to_html_css() {
1413        let top = TopProcessesMini::default();
1414        assert!(top.to_html().is_empty());
1415        assert!(top.to_css().is_empty());
1416    }
1417
1418    #[test]
1419    fn test_top_processes_mini_type_id() {
1420        let top = TopProcessesMini::default();
1421        assert_eq!(Widget::type_id(&top), TypeId::of::<TopProcessesMini>());
1422    }
1423
1424    #[test]
1425    fn test_top_processes_mini_clone() {
1426        let top = TopProcessesMini::new(vec![TopProcess::new(1234, 45.2, "firefox")]);
1427        let cloned = top;
1428        assert_eq!(cloned.processes.len(), 1);
1429    }
1430
1431    #[test]
1432    fn test_top_processes_mini_debug() {
1433        let top = TopProcessesMini::default();
1434        let debug = format!("{:?}", top);
1435        assert!(debug.contains("TopProcessesMini"));
1436    }
1437
1438    // ==================== TopProcess Tests ====================
1439
1440    #[test]
1441    fn test_top_process_creation() {
1442        let proc = TopProcess::new(1234, 45.2, "firefox");
1443        assert_eq!(proc.pid, 1234);
1444        assert!((proc.cpu_percent - 45.2).abs() < 0.001);
1445        assert_eq!(proc.name, "firefox");
1446    }
1447
1448    #[test]
1449    fn test_top_process_clone() {
1450        let proc = TopProcess::new(1234, 45.2, "firefox");
1451        let cloned = proc;
1452        assert_eq!(cloned.pid, 1234);
1453        assert_eq!(cloned.name, "firefox");
1454    }
1455
1456    #[test]
1457    fn test_top_process_debug() {
1458        let proc = TopProcess::new(1234, 45.2, "firefox");
1459        let debug = format!("{:?}", proc);
1460        assert!(debug.contains("TopProcess"));
1461        assert!(debug.contains("1234"));
1462    }
1463
1464    // ==================== FreqTempHeatmap Tests ====================
1465
1466    #[test]
1467    fn test_freq_temp_heatmap_new() {
1468        let heatmap = FreqTempHeatmap::new(vec![3600, 3200], vec![4000, 4000]);
1469        assert_eq!(heatmap.frequencies.len(), 2);
1470    }
1471
1472    #[test]
1473    fn test_freq_temp_heatmap_default() {
1474        let heatmap = FreqTempHeatmap::default();
1475        assert!(heatmap.frequencies.is_empty());
1476    }
1477
1478    #[test]
1479    fn test_freq_temp_heatmap_with_temperatures() {
1480        let heatmap = FreqTempHeatmap::new(vec![3600, 3200], vec![4000, 4000])
1481            .with_temperatures(vec![65.0, 70.0]);
1482        assert_eq!(heatmap.temperatures.as_ref().unwrap().len(), 2);
1483    }
1484
1485    #[test]
1486    fn test_freq_temp_heatmap_set_data() {
1487        let mut heatmap = FreqTempHeatmap::default();
1488        heatmap.set_data(vec![3600, 3200], vec![4000, 4000], Some(vec![65.0, 70.0]));
1489        assert_eq!(heatmap.frequencies.len(), 2);
1490        assert!(heatmap.temperatures.is_some());
1491    }
1492
1493    #[test]
1494    fn test_freq_temp_heatmap_set_data_no_temps() {
1495        let mut heatmap = FreqTempHeatmap::default();
1496        heatmap.set_data(vec![3600], vec![4000], None);
1497        assert!(heatmap.temperatures.is_none());
1498    }
1499
1500    #[test]
1501    fn test_freq_temp_heatmap_paint() {
1502        let mut heatmap =
1503            FreqTempHeatmap::new(vec![3600, 3200, 3000, 2800], vec![4000, 4000, 4000, 4000])
1504                .with_temperatures(vec![65.0, 70.0, 75.0, 80.0]);
1505        let mut buffer = CellBuffer::new(60, 15);
1506        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1507        heatmap.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
1508        heatmap.paint(&mut canvas);
1509    }
1510
1511    #[test]
1512    fn test_freq_temp_heatmap_paint_empty() {
1513        let mut heatmap = FreqTempHeatmap::default();
1514        let mut buffer = CellBuffer::new(60, 15);
1515        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1516        heatmap.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
1517        heatmap.paint(&mut canvas);
1518    }
1519
1520    #[test]
1521    fn test_freq_temp_heatmap_paint_no_temps() {
1522        let mut heatmap = FreqTempHeatmap::new(vec![3600, 3200], vec![4000, 4000]);
1523        let mut buffer = CellBuffer::new(60, 15);
1524        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1525        heatmap.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
1526        heatmap.paint(&mut canvas);
1527    }
1528
1529    #[test]
1530    fn test_freq_temp_heatmap_paint_zero_max_freq() {
1531        let mut heatmap = FreqTempHeatmap::new(vec![3600], vec![0]);
1532        let mut buffer = CellBuffer::new(60, 15);
1533        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1534        heatmap.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
1535        heatmap.paint(&mut canvas);
1536    }
1537
1538    #[test]
1539    fn test_freq_temp_heatmap_paint_extreme_temps() {
1540        let mut heatmap =
1541            FreqTempHeatmap::new(vec![3600], vec![4000]).with_temperatures(vec![20.0]); // Below normal range
1542        let mut buffer = CellBuffer::new(60, 15);
1543        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1544        heatmap.layout(Rect::new(0.0, 0.0, 60.0, 15.0));
1545        heatmap.paint(&mut canvas);
1546    }
1547
1548    #[test]
1549    fn test_freq_temp_heatmap_measure_no_temps() {
1550        let heatmap = FreqTempHeatmap::new(vec![3600], vec![4000]);
1551        let size = heatmap.measure(Constraints {
1552            min_width: 0.0,
1553            max_width: 60.0,
1554            min_height: 0.0,
1555            max_height: 20.0,
1556        });
1557        assert_eq!(size.height, 2.0);
1558    }
1559
1560    #[test]
1561    fn test_freq_temp_heatmap_measure_with_temps() {
1562        let heatmap = FreqTempHeatmap::new(vec![3600], vec![4000]).with_temperatures(vec![65.0]);
1563        let size = heatmap.measure(Constraints {
1564            min_width: 0.0,
1565            max_width: 60.0,
1566            min_height: 0.0,
1567            max_height: 20.0,
1568        });
1569        assert_eq!(size.height, 4.0);
1570    }
1571
1572    #[test]
1573    fn test_freq_temp_heatmap_event() {
1574        let mut heatmap = FreqTempHeatmap::default();
1575        assert!(heatmap.event(&Event::FocusIn).is_none());
1576    }
1577
1578    #[test]
1579    fn test_freq_temp_heatmap_children() {
1580        let heatmap = FreqTempHeatmap::default();
1581        assert!(heatmap.children().is_empty());
1582    }
1583
1584    #[test]
1585    fn test_freq_temp_heatmap_to_html_css() {
1586        let heatmap = FreqTempHeatmap::default();
1587        assert!(heatmap.to_html().is_empty());
1588        assert!(heatmap.to_css().is_empty());
1589    }
1590
1591    #[test]
1592    fn test_freq_temp_heatmap_type_id() {
1593        let heatmap = FreqTempHeatmap::default();
1594        assert_eq!(Widget::type_id(&heatmap), TypeId::of::<FreqTempHeatmap>());
1595    }
1596
1597    #[test]
1598    fn test_freq_temp_heatmap_clone() {
1599        let heatmap = FreqTempHeatmap::new(vec![3600], vec![4000]);
1600        let cloned = heatmap;
1601        assert_eq!(cloned.frequencies.len(), 1);
1602    }
1603
1604    #[test]
1605    fn test_freq_temp_heatmap_debug() {
1606        let heatmap = FreqTempHeatmap::default();
1607        let debug = format!("{:?}", heatmap);
1608        assert!(debug.contains("FreqTempHeatmap"));
1609    }
1610
1611    // ==================== LoadAverageTimeline Tests ====================
1612
1613    #[test]
1614    fn test_load_average_timeline_new() {
1615        let timeline = LoadAverageTimeline::new(8);
1616        assert_eq!(timeline.core_count, 8);
1617        assert!(timeline.load_1m.is_empty());
1618    }
1619
1620    #[test]
1621    fn test_load_average_timeline_new_zero_cores() {
1622        let timeline = LoadAverageTimeline::new(0);
1623        assert_eq!(timeline.core_count, 1); // Clamped to 1
1624    }
1625
1626    #[test]
1627    fn test_load_average_timeline_default() {
1628        let timeline = LoadAverageTimeline::default();
1629        assert_eq!(timeline.core_count, 1);
1630    }
1631
1632    #[test]
1633    fn test_load_average_timeline_push() {
1634        let mut timeline = LoadAverageTimeline::new(8);
1635        timeline.push(1.5, 1.2, 1.0);
1636        timeline.push(2.0, 1.5, 1.1);
1637        assert_eq!(timeline.load_1m.len(), 2);
1638        assert_eq!(timeline.load_5m.len(), 2);
1639        assert_eq!(timeline.load_15m.len(), 2);
1640    }
1641
1642    #[test]
1643    fn test_load_average_timeline_push_exceeds_max_history() {
1644        let mut timeline = LoadAverageTimeline::new(8);
1645        // Push 65 values (exceeds MAX_HISTORY of 60)
1646        for i in 0..65 {
1647            timeline.push(i as f64, i as f64, i as f64);
1648        }
1649        assert_eq!(timeline.load_1m.len(), 60);
1650        assert_eq!(timeline.load_5m.len(), 60);
1651        assert_eq!(timeline.load_15m.len(), 60);
1652        // First values should be trimmed
1653        assert!((timeline.load_1m[0] - 5.0).abs() < f64::EPSILON);
1654    }
1655
1656    #[test]
1657    fn test_load_average_timeline_set_core_count() {
1658        let mut timeline = LoadAverageTimeline::new(8);
1659        timeline.set_core_count(16);
1660        assert_eq!(timeline.core_count, 16);
1661    }
1662
1663    #[test]
1664    fn test_load_average_timeline_set_core_count_zero() {
1665        let mut timeline = LoadAverageTimeline::new(8);
1666        timeline.set_core_count(0);
1667        assert_eq!(timeline.core_count, 1); // Clamped to 1
1668    }
1669
1670    #[test]
1671    fn test_load_average_timeline_paint() {
1672        let mut timeline = LoadAverageTimeline::new(8);
1673        for i in 0..30 {
1674            timeline.push(1.0 + i as f64 * 0.1, 1.0, 0.8);
1675        }
1676        let mut buffer = CellBuffer::new(60, 10);
1677        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1678        timeline.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
1679        timeline.paint(&mut canvas);
1680    }
1681
1682    #[test]
1683    fn test_load_average_timeline_paint_empty() {
1684        let mut timeline = LoadAverageTimeline::new(8);
1685        let mut buffer = CellBuffer::new(60, 10);
1686        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1687        timeline.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
1688        timeline.paint(&mut canvas);
1689    }
1690
1691    #[test]
1692    fn test_load_average_timeline_paint_high_load() {
1693        let mut timeline = LoadAverageTimeline::new(4);
1694        // Push high load values (above core count)
1695        for i in 0..30 {
1696            timeline.push(8.0 + i as f64, 7.0, 6.0);
1697        }
1698        let mut buffer = CellBuffer::new(60, 10);
1699        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1700        timeline.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
1701        timeline.paint(&mut canvas);
1702    }
1703
1704    #[test]
1705    fn test_load_average_timeline_measure() {
1706        let timeline = LoadAverageTimeline::new(8);
1707        let size = timeline.measure(Constraints {
1708            min_width: 0.0,
1709            max_width: 60.0,
1710            min_height: 0.0,
1711            max_height: 20.0,
1712        });
1713        assert_eq!(size.height, 3.0);
1714    }
1715
1716    #[test]
1717    fn test_load_average_timeline_event() {
1718        let mut timeline = LoadAverageTimeline::default();
1719        assert!(timeline.event(&Event::FocusIn).is_none());
1720    }
1721
1722    #[test]
1723    fn test_load_average_timeline_children() {
1724        let timeline = LoadAverageTimeline::default();
1725        assert!(timeline.children().is_empty());
1726    }
1727
1728    #[test]
1729    fn test_load_average_timeline_to_html_css() {
1730        let timeline = LoadAverageTimeline::default();
1731        assert!(timeline.to_html().is_empty());
1732        assert!(timeline.to_css().is_empty());
1733    }
1734
1735    #[test]
1736    fn test_load_average_timeline_type_id() {
1737        let timeline = LoadAverageTimeline::default();
1738        assert_eq!(
1739            Widget::type_id(&timeline),
1740            TypeId::of::<LoadAverageTimeline>()
1741        );
1742    }
1743
1744    #[test]
1745    fn test_load_average_timeline_clone() {
1746        let mut timeline = LoadAverageTimeline::new(8);
1747        timeline.push(1.0, 2.0, 3.0);
1748        let cloned = timeline.clone();
1749        assert_eq!(cloned.load_1m.len(), 1);
1750    }
1751
1752    #[test]
1753    fn test_load_average_timeline_debug() {
1754        let timeline = LoadAverageTimeline::default();
1755        let debug = format!("{:?}", timeline);
1756        assert!(debug.contains("LoadAverageTimeline"));
1757    }
1758
1759    // ==================== General Brick/Widget Tests ====================
1760
1761    #[test]
1762    fn test_all_widgets_verify() {
1763        assert!(PerCoreSparklineGrid::default().verify().is_valid());
1764        assert!(CpuStateBreakdown::default().verify().is_valid());
1765        assert!(TopProcessesMini::default().verify().is_valid());
1766        assert!(FreqTempHeatmap::default().verify().is_valid());
1767        assert!(LoadAverageTimeline::default().verify().is_valid());
1768    }
1769
1770    #[test]
1771    fn test_all_widgets_brick_names() {
1772        assert_eq!(
1773            PerCoreSparklineGrid::default().brick_name(),
1774            "per_core_sparkline_grid"
1775        );
1776        assert_eq!(
1777            CpuStateBreakdown::default().brick_name(),
1778            "cpu_state_breakdown"
1779        );
1780        assert_eq!(
1781            TopProcessesMini::default().brick_name(),
1782            "top_processes_mini"
1783        );
1784        assert_eq!(FreqTempHeatmap::default().brick_name(), "freq_temp_heatmap");
1785        assert_eq!(
1786            LoadAverageTimeline::default().brick_name(),
1787            "load_average_timeline"
1788        );
1789    }
1790
1791    #[test]
1792    fn test_all_widgets_assertions() {
1793        assert!(!PerCoreSparklineGrid::default().assertions().is_empty());
1794        assert!(!CpuStateBreakdown::default().assertions().is_empty());
1795        assert!(!TopProcessesMini::default().assertions().is_empty());
1796        assert!(!FreqTempHeatmap::default().assertions().is_empty());
1797        assert!(!LoadAverageTimeline::default().assertions().is_empty());
1798    }
1799
1800    #[test]
1801    fn test_all_widgets_budget() {
1802        assert!(PerCoreSparklineGrid::default().budget().measure_ms > 0);
1803        assert!(CpuStateBreakdown::default().budget().measure_ms > 0);
1804        assert!(TopProcessesMini::default().budget().measure_ms > 0);
1805        assert!(FreqTempHeatmap::default().budget().measure_ms > 0);
1806        assert!(LoadAverageTimeline::default().budget().measure_ms > 0);
1807    }
1808
1809    // ==================== state_colors() Test ====================
1810
1811    #[test]
1812    fn test_state_colors() {
1813        let colors = state_colors();
1814        assert_eq!(colors.len(), 6);
1815        // Verify USER color is blue-ish
1816        assert!(colors[0].r > 0.4 && colors[0].r < 0.6);
1817        // Verify IDLE color is gray-ish
1818        assert!(colors[5].r < 0.4);
1819    }
1820}