Skip to main content

presentar_terminal/widgets/
cpu_grid.rs

1//! `CpuGrid` widget for dense per-core CPU visualization.
2//!
3//! Displays N CPU cores in a compact grid with gradient-colored meters.
4//! Reference: btop/ttop per-core CPU displays.
5//!
6//! # Features
7//!
8//! - Per-core utilization with gradient coloring
9//! - Optional frequency scaling indicators (⚡↑→↓·)
10//! - CPU governor display (performance, powersave, etc.)
11//! - Compact and percentage display modes
12
13use crate::compute_block::{CpuGovernor, FrequencyScalingState};
14use crate::theme::Gradient;
15use presentar_core::{
16    Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Constraints, Event,
17    LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
18};
19use std::any::Any;
20use std::time::Duration;
21
22/// Block characters for single-row meters (8 levels).
23const METER_CHARS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
24
25/// Frequency scaling state indicator characters.
26const FREQ_INDICATORS: [char; 5] = ['⚡', '↑', '→', '↓', '·'];
27
28/// Per-core CPU grid with gradient-colored meters.
29#[derive(Debug, Clone)]
30#[allow(clippy::struct_excessive_bools)]
31pub struct CpuGrid {
32    /// Per-core utilization (0.0-100.0).
33    pub core_usage: Vec<f64>,
34    /// Gradient for coloring (low→high).
35    gradient: Gradient,
36    /// Number of columns (auto-calculated if None).
37    columns: Option<usize>,
38    /// Show core labels (0, 1, 2...).
39    show_labels: bool,
40    /// Show numeric percentages instead of meter chars.
41    show_percentages: bool,
42    /// Compact mode (minimal spacing).
43    compact: bool,
44    /// Cached bounds.
45    bounds: Rect,
46    /// Per-core current frequencies in MHz.
47    frequencies: Option<Vec<u32>>,
48    /// Per-core max frequencies in MHz.
49    max_frequencies: Option<Vec<u32>>,
50    /// Current CPU governor (affects all cores).
51    governor: Option<CpuGovernor>,
52    /// Show frequency scaling indicators.
53    show_freq_indicators: bool,
54}
55
56impl Default for CpuGrid {
57    fn default() -> Self {
58        Self::new(vec![])
59    }
60}
61
62impl CpuGrid {
63    /// Create a new CPU grid with core usage data.
64    #[must_use]
65    pub fn new(core_usage: Vec<f64>) -> Self {
66        Self {
67            core_usage,
68            gradient: Gradient::from_hex(&["#7aa2f7", "#e0af68", "#f7768e"]), // Tokyo Night CPU
69            columns: None,
70            show_labels: true,
71            show_percentages: false,
72            compact: false,
73            bounds: Rect::default(),
74            frequencies: None,
75            max_frequencies: None,
76            governor: None,
77            show_freq_indicators: false,
78        }
79    }
80
81    /// Set the gradient for coloring.
82    #[must_use]
83    pub fn with_gradient(mut self, gradient: Gradient) -> Self {
84        self.gradient = gradient;
85        self
86    }
87
88    /// Set explicit column count.
89    #[must_use]
90    pub fn with_columns(mut self, cols: usize) -> Self {
91        debug_assert!(cols > 0, "column count must be positive");
92        self.columns = Some(cols);
93        self
94    }
95
96    /// Enable compact mode (minimal spacing).
97    #[must_use]
98    pub fn compact(mut self) -> Self {
99        self.compact = true;
100        self
101    }
102
103    /// Disable core labels.
104    #[must_use]
105    pub fn without_labels(mut self) -> Self {
106        self.show_labels = false;
107        self
108    }
109
110    /// Show numeric percentages instead of meter characters.
111    /// Format: "12 45%" or " 5 99%" (like ttop).
112    #[must_use]
113    pub fn with_percentages(mut self) -> Self {
114        self.show_percentages = true;
115        self
116    }
117
118    /// Enable frequency scaling indicators (⚡↑→↓·).
119    ///
120    /// Requires frequencies to be set via `with_frequencies()`.
121    /// Indicators show scaling state:
122    /// - ⚡ Turbo (>100% of base)
123    /// - ↑ High (80-100% of max)
124    /// - → Normal (50-80% of max)
125    /// - ↓ Scaled (20-50% of max)
126    /// - · Idle (<20% of max)
127    #[must_use]
128    pub fn with_freq_indicators(mut self) -> Self {
129        self.show_freq_indicators = true;
130        self
131    }
132
133    /// Set per-core frequency data (current and max frequencies in MHz).
134    ///
135    /// Both vectors should have the same length as `core_usage`.
136    #[must_use]
137    pub fn with_frequencies(mut self, current: Vec<u32>, max: Vec<u32>) -> Self {
138        // Provability: frequency vectors must have consistent lengths
139        debug_assert_eq!(
140            current.len(),
141            max.len(),
142            "current and max frequencies must have same length"
143        );
144        self.frequencies = Some(current);
145        self.max_frequencies = Some(max);
146        self
147    }
148
149    /// Set the CPU governor (affects display title/info).
150    #[must_use]
151    pub fn with_governor(mut self, governor: CpuGovernor) -> Self {
152        self.governor = Some(governor);
153        self
154    }
155
156    /// Update core usage data.
157    pub fn set_usage(&mut self, usage: Vec<f64>) {
158        self.core_usage = usage;
159    }
160
161    /// Update frequency data.
162    pub fn set_frequencies(&mut self, current: Vec<u32>, max: Vec<u32>) {
163        self.frequencies = Some(current);
164        self.max_frequencies = Some(max);
165    }
166
167    /// Update CPU governor.
168    pub fn set_governor(&mut self, governor: CpuGovernor) {
169        self.governor = Some(governor);
170    }
171
172    /// Get the current CPU governor, if set.
173    #[must_use]
174    pub fn governor(&self) -> Option<&CpuGovernor> {
175        self.governor.as_ref()
176    }
177
178    /// Get the governor display string for UI titles.
179    #[must_use]
180    pub fn governor_display(&self) -> &'static str {
181        self.governor.as_ref().map_or("", CpuGovernor::as_str)
182    }
183
184    /// Get the number of cores.
185    #[must_use]
186    pub fn core_count(&self) -> usize {
187        self.core_usage.len()
188    }
189
190    /// Calculate optimal grid dimensions for N cores in given width.
191    fn optimal_grid(&self, max_width: usize) -> (usize, usize) {
192        let count = self.core_usage.len();
193        if count == 0 {
194            return (0, 0);
195        }
196
197        let cell_width = self.cell_width();
198
199        let max_cols = (max_width / cell_width).max(1);
200        let cols = self.columns.unwrap_or_else(|| {
201            // Try to make a reasonably square grid
202            let sqrt = (count as f64).sqrt().ceil() as usize;
203            sqrt.min(max_cols).max(1)
204        });
205
206        let rows = count.div_ceil(cols);
207        (cols, rows)
208    }
209
210    /// Get cell width based on current display mode.
211    fn cell_width(&self) -> usize {
212        let freq_extra = usize::from(self.show_freq_indicators);
213
214        if self.show_percentages {
215            // Format: "12 45%⚡" = 6 chars + freq indicator + space
216            if self.compact {
217                7 + freq_extra
218            } else {
219                8 + freq_extra
220            }
221        } else if self.show_labels {
222            // Format: "12▆⚡" = 3 chars + freq indicator + space
223            if self.compact {
224                4 + freq_extra
225            } else {
226                5 + freq_extra
227            }
228        } else if self.compact {
229            2 + freq_extra
230        } else {
231            3 + freq_extra
232        }
233    }
234
235    /// Get meter character for percentage (0-100).
236    fn meter_char(pct: f64) -> char {
237        let idx = ((pct / 100.0) * 8.0).round() as usize;
238        METER_CHARS[idx.min(8)]
239    }
240
241    /// Get frequency scaling state for a core.
242    fn freq_scaling_state(&self, core_idx: usize) -> Option<FrequencyScalingState> {
243        let frequencies = self.frequencies.as_ref()?;
244        let max_frequencies = self.max_frequencies.as_ref()?;
245
246        let current = *frequencies.get(core_idx)?;
247        let max = *max_frequencies.get(core_idx)?;
248
249        if max == 0 {
250            return Some(FrequencyScalingState::Idle);
251        }
252
253        let ratio = current as f64 / max as f64;
254
255        Some(if ratio > 1.0 {
256            FrequencyScalingState::Turbo
257        } else if ratio >= 0.8 {
258            FrequencyScalingState::High
259        } else if ratio >= 0.5 {
260            FrequencyScalingState::Normal
261        } else if ratio >= 0.2 {
262            FrequencyScalingState::Scaled
263        } else {
264            FrequencyScalingState::Idle
265        })
266    }
267
268    /// Get the frequency indicator character for a core.
269    fn freq_indicator(&self, core_idx: usize) -> Option<char> {
270        if !self.show_freq_indicators {
271            return None;
272        }
273
274        self.freq_scaling_state(core_idx).map(|state| match state {
275            FrequencyScalingState::Turbo => FREQ_INDICATORS[0], // ⚡
276            FrequencyScalingState::High => FREQ_INDICATORS[1],  // ↑
277            FrequencyScalingState::Normal => FREQ_INDICATORS[2], // →
278            FrequencyScalingState::Scaled => FREQ_INDICATORS[3], // ↓
279            FrequencyScalingState::Idle => FREQ_INDICATORS[4],  // ·
280        })
281    }
282
283    /// Get the average frequency across all cores in GHz.
284    #[must_use]
285    pub fn avg_frequency_ghz(&self) -> Option<f64> {
286        let frequencies = self.frequencies.as_ref()?;
287        if frequencies.is_empty() {
288            return None;
289        }
290        let sum: u64 = frequencies.iter().map(|&f| f as u64).sum();
291        Some(sum as f64 / frequencies.len() as f64 / 1000.0)
292    }
293}
294
295impl Brick for CpuGrid {
296    fn brick_name(&self) -> &'static str {
297        "cpu_grid"
298    }
299
300    fn assertions(&self) -> &[BrickAssertion] {
301        static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
302        ASSERTIONS
303    }
304
305    fn budget(&self) -> BrickBudget {
306        BrickBudget::uniform(16)
307    }
308
309    fn verify(&self) -> BrickVerification {
310        BrickVerification {
311            passed: self.assertions().to_vec(),
312            failed: vec![],
313            verification_time: Duration::from_micros(10),
314        }
315    }
316
317    fn to_html(&self) -> String {
318        String::new()
319    }
320
321    fn to_css(&self) -> String {
322        String::new()
323    }
324}
325
326impl Widget for CpuGrid {
327    fn type_id(&self) -> TypeId {
328        TypeId::of::<Self>()
329    }
330
331    fn measure(&self, constraints: Constraints) -> Size {
332        let max_width = constraints.max_width as usize;
333        let (cols, rows) = self.optimal_grid(max_width);
334
335        let cell_width = self.cell_width() as f32;
336
337        let width = (cols as f32 * cell_width).min(constraints.max_width);
338        let height = (rows as f32).min(constraints.max_height);
339
340        constraints.constrain(Size::new(width, height))
341    }
342
343    fn layout(&mut self, bounds: Rect) -> LayoutResult {
344        self.bounds = bounds;
345        LayoutResult {
346            size: Size::new(bounds.width, bounds.height),
347        }
348    }
349
350    fn paint(&self, canvas: &mut dyn Canvas) {
351        if self.core_usage.is_empty() {
352            return;
353        }
354
355        let (cols, _rows) = self.optimal_grid(self.bounds.width as usize);
356        if cols == 0 {
357            return;
358        }
359
360        let cell_width = self.cell_width() as f32;
361
362        for (i, &usage) in self.core_usage.iter().enumerate() {
363            let col = i % cols;
364            let row = i / cols;
365
366            let x = self.bounds.x + col as f32 * cell_width;
367            let y = self.bounds.y + row as f32;
368
369            // Skip drawing if row exceeds bounds (clip to panel)
370            if y >= self.bounds.y + self.bounds.height {
371                break;
372            }
373
374            let usage_clamped = usage.clamp(0.0, 100.0);
375            let color = self.gradient.for_percent(usage_clamped);
376
377            let style = TextStyle {
378                color,
379                ..Default::default()
380            };
381
382            // Get frequency indicator if enabled
383            let freq_char = self.freq_indicator(i).unwrap_or(' ');
384            let freq_suffix = if self.show_freq_indicators {
385                freq_char.to_string()
386            } else {
387                String::new()
388            };
389
390            if self.show_percentages {
391                // Format: "12 45%⚡" or " 5 99%↑" (ttop style with numeric percentages)
392                let label = if self.compact {
393                    format!("{i:2}{usage_clamped:3.0}%{freq_suffix}")
394                } else {
395                    format!("{i:2} {usage_clamped:3.0}%{freq_suffix}")
396                };
397                canvas.draw_text(&label, Point::new(x, y), &style);
398            } else if self.show_labels {
399                // Format: "12▆⚡" or " 5▄→"
400                let meter = Self::meter_char(usage_clamped);
401                let label = if self.compact {
402                    format!("{i:2}{meter}{freq_suffix}")
403                } else {
404                    format!("{i:2} {meter}{freq_suffix}")
405                };
406                canvas.draw_text(&label, Point::new(x, y), &style);
407            } else {
408                let meter = Self::meter_char(usage_clamped);
409                let label = format!("{meter}{freq_suffix}");
410                canvas.draw_text(&label, Point::new(x, y), &style);
411            }
412        }
413    }
414
415    fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
416        None
417    }
418
419    fn children(&self) -> &[Box<dyn Widget>] {
420        &[]
421    }
422
423    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
424        &mut []
425    }
426}
427
428#[cfg(test)]
429#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_cpu_grid_new() {
435        let grid = CpuGrid::new(vec![10.0, 50.0, 90.0]);
436        assert_eq!(grid.core_count(), 3);
437    }
438
439    #[test]
440    fn test_cpu_grid_empty() {
441        let grid = CpuGrid::new(vec![]);
442        assert_eq!(grid.core_count(), 0);
443    }
444
445    #[test]
446    fn test_cpu_grid_with_columns() {
447        let grid = CpuGrid::new(vec![10.0; 48]).with_columns(8);
448        assert_eq!(grid.columns, Some(8));
449    }
450
451    #[test]
452    fn test_cpu_grid_compact() {
453        let grid = CpuGrid::new(vec![10.0; 48]).compact();
454        assert!(grid.compact);
455    }
456
457    #[test]
458    fn test_cpu_grid_without_labels() {
459        let grid = CpuGrid::new(vec![10.0; 48]).without_labels();
460        assert!(!grid.show_labels);
461    }
462
463    #[test]
464    fn test_meter_char() {
465        assert_eq!(CpuGrid::meter_char(0.0), ' ');
466        assert_eq!(CpuGrid::meter_char(50.0), '▄');
467        assert_eq!(CpuGrid::meter_char(100.0), '█');
468    }
469
470    #[test]
471    fn test_optimal_grid_48_cores() {
472        let grid = CpuGrid::new(vec![10.0; 48]);
473        let (cols, rows) = grid.optimal_grid(80);
474        assert!(cols > 0);
475        assert!(rows > 0);
476        assert!(cols * rows >= 48);
477    }
478
479    #[test]
480    fn test_optimal_grid_explicit_columns() {
481        let grid = CpuGrid::new(vec![10.0; 48]).with_columns(8);
482        let (cols, rows) = grid.optimal_grid(80);
483        assert_eq!(cols, 8);
484        assert_eq!(rows, 6);
485    }
486
487    #[test]
488    fn test_cpu_grid_measure() {
489        let grid = CpuGrid::new(vec![10.0; 8]);
490        let size = grid.measure(Constraints::new(0.0, 80.0, 0.0, 20.0));
491        assert!(size.width > 0.0);
492        assert!(size.height > 0.0);
493    }
494
495    #[test]
496    fn test_cpu_grid_set_usage() {
497        let mut grid = CpuGrid::new(vec![]);
498        assert_eq!(grid.core_count(), 0);
499        grid.set_usage(vec![10.0, 20.0, 30.0]);
500        assert_eq!(grid.core_count(), 3);
501    }
502
503    #[test]
504    fn test_cpu_grid_verify() {
505        let grid = CpuGrid::new(vec![50.0; 8]);
506        let v = grid.verify();
507        assert!(v.is_valid());
508    }
509
510    #[test]
511    fn test_cpu_grid_type_id() {
512        let grid = CpuGrid::new(vec![]);
513        let _ = Widget::type_id(&grid);
514    }
515
516    #[test]
517    fn test_cpu_grid_default() {
518        let grid = CpuGrid::default();
519        assert_eq!(grid.core_count(), 0);
520    }
521
522    #[test]
523    fn test_cpu_grid_brick_name() {
524        let grid = CpuGrid::new(vec![]);
525        assert_eq!(grid.brick_name(), "cpu_grid");
526    }
527
528    #[test]
529    fn test_cpu_grid_layout() {
530        let mut grid = CpuGrid::new(vec![50.0; 8]);
531        let result = grid.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
532        assert_eq!(result.size.width, 80.0);
533        assert_eq!(result.size.height, 10.0);
534    }
535
536    #[test]
537    fn test_cpu_grid_event() {
538        let mut grid = CpuGrid::new(vec![50.0]);
539        let event = Event::Resize {
540            width: 80.0,
541            height: 24.0,
542        };
543        assert!(grid.event(&event).is_none());
544    }
545
546    #[test]
547    fn test_cpu_grid_children() {
548        let grid = CpuGrid::new(vec![50.0]);
549        assert!(grid.children().is_empty());
550    }
551
552    #[test]
553    fn test_cpu_grid_children_mut() {
554        let mut grid = CpuGrid::new(vec![50.0]);
555        assert!(grid.children_mut().is_empty());
556    }
557
558    #[test]
559    fn test_cpu_grid_assertions() {
560        let grid = CpuGrid::new(vec![50.0]);
561        assert!(!grid.assertions().is_empty());
562    }
563
564    #[test]
565    fn test_cpu_grid_budget() {
566        let grid = CpuGrid::new(vec![50.0]);
567        let budget = grid.budget();
568        assert!(budget.layout_ms > 0);
569    }
570
571    #[test]
572    fn test_cpu_grid_to_html() {
573        let grid = CpuGrid::new(vec![50.0]);
574        assert!(grid.to_html().is_empty());
575    }
576
577    #[test]
578    fn test_cpu_grid_to_css() {
579        let grid = CpuGrid::new(vec![50.0]);
580        assert!(grid.to_css().is_empty());
581    }
582
583    #[test]
584    fn test_cpu_grid_clone() {
585        let grid = CpuGrid::new(vec![50.0, 75.0]).with_columns(4);
586        let cloned = grid.clone();
587        assert_eq!(cloned.core_usage.len(), grid.core_usage.len());
588        assert_eq!(cloned.columns, grid.columns);
589    }
590
591    #[test]
592    fn test_cpu_grid_debug() {
593        let grid = CpuGrid::new(vec![50.0]);
594        let debug = format!("{grid:?}");
595        assert!(debug.contains("CpuGrid"));
596    }
597
598    #[test]
599    fn test_cpu_grid_with_gradient() {
600        let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
601        let grid = CpuGrid::new(vec![50.0]).with_gradient(gradient);
602        // Gradient is private, just test it doesn't panic
603        assert_eq!(grid.core_count(), 1);
604    }
605
606    #[test]
607    fn test_cpu_grid_paint() {
608        use crate::{CellBuffer, DirectTerminalCanvas};
609
610        let mut grid = CpuGrid::new(vec![10.0, 50.0, 90.0]);
611        let mut buffer = CellBuffer::new(40, 10);
612        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
613
614        grid.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
615        grid.paint(&mut canvas);
616    }
617
618    #[test]
619    fn test_cpu_grid_paint_empty() {
620        use crate::{CellBuffer, DirectTerminalCanvas};
621
622        let grid = CpuGrid::new(vec![]);
623        let mut buffer = CellBuffer::new(40, 10);
624        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
625
626        grid.paint(&mut canvas);
627    }
628
629    #[test]
630    fn test_cpu_grid_paint_compact() {
631        use crate::{CellBuffer, DirectTerminalCanvas};
632
633        let mut grid = CpuGrid::new(vec![10.0, 50.0, 90.0]).compact();
634        let mut buffer = CellBuffer::new(40, 10);
635        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
636
637        grid.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
638        grid.paint(&mut canvas);
639    }
640
641    #[test]
642    fn test_cpu_grid_paint_without_labels() {
643        use crate::{CellBuffer, DirectTerminalCanvas};
644
645        let mut grid = CpuGrid::new(vec![10.0, 50.0, 90.0]).without_labels();
646        let mut buffer = CellBuffer::new(40, 10);
647        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
648
649        grid.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
650        grid.paint(&mut canvas);
651    }
652
653    #[test]
654    fn test_cpu_grid_paint_compact_without_labels() {
655        use crate::{CellBuffer, DirectTerminalCanvas};
656
657        let mut grid = CpuGrid::new(vec![10.0, 50.0, 90.0])
658            .compact()
659            .without_labels();
660        let mut buffer = CellBuffer::new(40, 10);
661        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
662
663        grid.layout(Rect::new(0.0, 0.0, 40.0, 10.0));
664        grid.paint(&mut canvas);
665    }
666
667    #[test]
668    fn test_cpu_grid_optimal_grid_empty() {
669        let grid = CpuGrid::new(vec![]);
670        let (cols, rows) = grid.optimal_grid(80);
671        assert_eq!(cols, 0);
672        assert_eq!(rows, 0);
673    }
674
675    #[test]
676    fn test_cpu_grid_measure_compact() {
677        let grid = CpuGrid::new(vec![10.0; 8]).compact();
678        let size = grid.measure(Constraints::new(0.0, 80.0, 0.0, 20.0));
679        assert!(size.width > 0.0);
680    }
681
682    #[test]
683    fn test_cpu_grid_measure_without_labels() {
684        let grid = CpuGrid::new(vec![10.0; 8]).without_labels();
685        let size = grid.measure(Constraints::new(0.0, 80.0, 0.0, 20.0));
686        assert!(size.width > 0.0);
687    }
688
689    #[test]
690    fn test_cpu_grid_measure_compact_without_labels() {
691        let grid = CpuGrid::new(vec![10.0; 8]).compact().without_labels();
692        let size = grid.measure(Constraints::new(0.0, 80.0, 0.0, 20.0));
693        assert!(size.width > 0.0);
694    }
695
696    #[test]
697    fn test_cpu_grid_meter_char_edge_cases() {
698        assert_eq!(CpuGrid::meter_char(-10.0), ' ');
699        assert_eq!(CpuGrid::meter_char(150.0), '█');
700        assert_eq!(CpuGrid::meter_char(12.5), '▁');
701        assert_eq!(CpuGrid::meter_char(25.0), '▂');
702        assert_eq!(CpuGrid::meter_char(37.5), '▃');
703        assert_eq!(CpuGrid::meter_char(62.5), '▅');
704        assert_eq!(CpuGrid::meter_char(75.0), '▆');
705        assert_eq!(CpuGrid::meter_char(87.5), '▇');
706    }
707
708    // ===== Frequency and Governor Tests (SPEC-024 Section 15) =====
709
710    #[test]
711    fn test_cpu_grid_with_frequencies() {
712        let grid = CpuGrid::new(vec![50.0; 4])
713            .with_frequencies(vec![3600, 3200, 2800, 1000], vec![4000, 4000, 4000, 4000]);
714
715        assert!(grid.frequencies.is_some());
716        assert!(grid.max_frequencies.is_some());
717        assert_eq!(grid.frequencies.as_ref().unwrap().len(), 4);
718    }
719
720    #[test]
721    fn test_cpu_grid_with_freq_indicators() {
722        let grid = CpuGrid::new(vec![50.0; 4])
723            .with_frequencies(vec![3600, 3200, 2800, 1000], vec![4000, 4000, 4000, 4000])
724            .with_freq_indicators();
725
726        assert!(grid.show_freq_indicators);
727    }
728
729    #[test]
730    fn test_cpu_grid_freq_scaling_state() {
731        let grid = CpuGrid::new(vec![50.0; 4]).with_frequencies(
732            vec![4200, 3500, 2500, 500],  // current
733            vec![4000, 4000, 4000, 4000], // max
734        );
735
736        // 4200/4000 = 105% -> Turbo
737        assert_eq!(
738            grid.freq_scaling_state(0),
739            Some(FrequencyScalingState::Turbo)
740        );
741        // 3500/4000 = 87.5% -> High
742        assert_eq!(
743            grid.freq_scaling_state(1),
744            Some(FrequencyScalingState::High)
745        );
746        // 2500/4000 = 62.5% -> Normal
747        assert_eq!(
748            grid.freq_scaling_state(2),
749            Some(FrequencyScalingState::Normal)
750        );
751        // 500/4000 = 12.5% -> Idle
752        assert_eq!(
753            grid.freq_scaling_state(3),
754            Some(FrequencyScalingState::Idle)
755        );
756    }
757
758    #[test]
759    fn test_cpu_grid_freq_scaling_state_scaled() {
760        let grid = CpuGrid::new(vec![50.0; 1]).with_frequencies(vec![1200], vec![4000]);
761        // 1200/4000 = 30% -> Scaled
762        assert_eq!(
763            grid.freq_scaling_state(0),
764            Some(FrequencyScalingState::Scaled)
765        );
766    }
767
768    #[test]
769    fn test_cpu_grid_freq_scaling_state_no_frequencies() {
770        let grid = CpuGrid::new(vec![50.0; 4]);
771        assert_eq!(grid.freq_scaling_state(0), None);
772    }
773
774    #[test]
775    fn test_cpu_grid_freq_scaling_state_zero_max() {
776        let grid = CpuGrid::new(vec![50.0; 1]).with_frequencies(vec![3000], vec![0]);
777        assert_eq!(
778            grid.freq_scaling_state(0),
779            Some(FrequencyScalingState::Idle)
780        );
781    }
782
783    #[test]
784    fn test_cpu_grid_freq_indicator() {
785        let grid = CpuGrid::new(vec![50.0; 4])
786            .with_frequencies(vec![4200, 3500, 2500, 500], vec![4000, 4000, 4000, 4000])
787            .with_freq_indicators();
788
789        assert_eq!(grid.freq_indicator(0), Some('⚡')); // Turbo
790        assert_eq!(grid.freq_indicator(1), Some('↑')); // High
791        assert_eq!(grid.freq_indicator(2), Some('→')); // Normal
792        assert_eq!(grid.freq_indicator(3), Some('·')); // Idle
793    }
794
795    #[test]
796    fn test_cpu_grid_freq_indicator_disabled() {
797        let grid = CpuGrid::new(vec![50.0; 4])
798            .with_frequencies(vec![4200, 3500, 2500, 500], vec![4000, 4000, 4000, 4000]);
799        // freq indicators not enabled
800        assert_eq!(grid.freq_indicator(0), None);
801    }
802
803    #[test]
804    fn test_cpu_grid_with_governor() {
805        let grid = CpuGrid::new(vec![50.0; 4]).with_governor(CpuGovernor::Performance);
806        assert_eq!(grid.governor(), Some(&CpuGovernor::Performance));
807        assert_eq!(grid.governor_display(), "performance");
808    }
809
810    #[test]
811    fn test_cpu_grid_governor_powersave() {
812        let grid = CpuGrid::new(vec![50.0; 4]).with_governor(CpuGovernor::Powersave);
813        assert_eq!(grid.governor(), Some(&CpuGovernor::Powersave));
814        assert_eq!(grid.governor_display(), "powersave");
815    }
816
817    #[test]
818    fn test_cpu_grid_governor_schedutil() {
819        let grid = CpuGrid::new(vec![50.0; 4]).with_governor(CpuGovernor::Schedutil);
820        assert_eq!(grid.governor(), Some(&CpuGovernor::Schedutil));
821        assert_eq!(grid.governor_display(), "schedutil");
822    }
823
824    #[test]
825    fn test_cpu_grid_no_governor() {
826        let grid = CpuGrid::new(vec![50.0; 4]);
827        assert_eq!(grid.governor(), None);
828        assert_eq!(grid.governor_display(), "");
829    }
830
831    #[test]
832    fn test_cpu_grid_set_frequencies() {
833        let mut grid = CpuGrid::new(vec![50.0; 4]);
834        assert!(grid.frequencies.is_none());
835
836        grid.set_frequencies(vec![3600, 3200, 2800, 1000], vec![4000, 4000, 4000, 4000]);
837        assert!(grid.frequencies.is_some());
838        assert!(grid.max_frequencies.is_some());
839    }
840
841    #[test]
842    fn test_cpu_grid_set_governor() {
843        let mut grid = CpuGrid::new(vec![50.0; 4]);
844        assert!(grid.governor().is_none());
845
846        grid.set_governor(CpuGovernor::Ondemand);
847        assert_eq!(grid.governor(), Some(&CpuGovernor::Ondemand));
848    }
849
850    #[test]
851    fn test_cpu_grid_avg_frequency_ghz() {
852        let grid = CpuGrid::new(vec![50.0; 4])
853            .with_frequencies(vec![3600, 3200, 2800, 2400], vec![4000, 4000, 4000, 4000]);
854
855        let avg = grid.avg_frequency_ghz().unwrap();
856        assert!((avg - 3.0).abs() < 0.01); // Average of 3.6+3.2+2.8+2.4 / 4 = 3.0 GHz
857    }
858
859    #[test]
860    fn test_cpu_grid_avg_frequency_ghz_none() {
861        let grid = CpuGrid::new(vec![50.0; 4]);
862        assert!(grid.avg_frequency_ghz().is_none());
863    }
864
865    #[test]
866    fn test_cpu_grid_avg_frequency_ghz_empty() {
867        let grid = CpuGrid::new(vec![]).with_frequencies(vec![], vec![]);
868        assert!(grid.avg_frequency_ghz().is_none());
869    }
870
871    #[test]
872    fn test_cpu_grid_paint_with_freq_indicators() {
873        use crate::{CellBuffer, DirectTerminalCanvas};
874
875        let mut grid = CpuGrid::new(vec![50.0, 80.0, 20.0, 10.0])
876            .with_frequencies(vec![4200, 3500, 2500, 500], vec![4000, 4000, 4000, 4000])
877            .with_freq_indicators();
878
879        let mut buffer = CellBuffer::new(60, 10);
880        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
881
882        grid.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
883        grid.paint(&mut canvas);
884    }
885
886    #[test]
887    fn test_cpu_grid_paint_with_percentages_and_freq() {
888        use crate::{CellBuffer, DirectTerminalCanvas};
889
890        let mut grid = CpuGrid::new(vec![50.0, 80.0])
891            .with_percentages()
892            .with_frequencies(vec![4200, 3500], vec![4000, 4000])
893            .with_freq_indicators();
894
895        let mut buffer = CellBuffer::new(60, 10);
896        let mut canvas = DirectTerminalCanvas::new(&mut buffer);
897
898        grid.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
899        grid.paint(&mut canvas);
900    }
901
902    #[test]
903    fn test_cpu_grid_cell_width_with_freq_indicators() {
904        let grid_no_freq = CpuGrid::new(vec![50.0; 4]);
905        let grid_with_freq = CpuGrid::new(vec![50.0; 4]).with_freq_indicators();
906
907        // Frequency indicators add 1 char
908        assert_eq!(grid_with_freq.cell_width(), grid_no_freq.cell_width() + 1);
909    }
910
911    #[test]
912    fn test_cpu_grid_cell_width_percentages_with_freq() {
913        let grid = CpuGrid::new(vec![50.0; 4])
914            .with_percentages()
915            .with_freq_indicators();
916
917        // Percentages (8) + freq indicator (1) = 9
918        assert_eq!(grid.cell_width(), 9);
919    }
920
921    #[test]
922    fn test_cpu_grid_governor_all_variants() {
923        // Test all governor variants for display
924        assert_eq!(
925            CpuGrid::new(vec![])
926                .with_governor(CpuGovernor::Performance)
927                .governor_display(),
928            "performance"
929        );
930        assert_eq!(
931            CpuGrid::new(vec![])
932                .with_governor(CpuGovernor::Powersave)
933                .governor_display(),
934            "powersave"
935        );
936        assert_eq!(
937            CpuGrid::new(vec![])
938                .with_governor(CpuGovernor::Ondemand)
939                .governor_display(),
940            "ondemand"
941        );
942        assert_eq!(
943            CpuGrid::new(vec![])
944                .with_governor(CpuGovernor::Conservative)
945                .governor_display(),
946            "conservative"
947        );
948        assert_eq!(
949            CpuGrid::new(vec![])
950                .with_governor(CpuGovernor::Schedutil)
951                .governor_display(),
952            "schedutil"
953        );
954        assert_eq!(
955            CpuGrid::new(vec![])
956                .with_governor(CpuGovernor::Userspace)
957                .governor_display(),
958            "userspace"
959        );
960        assert_eq!(
961            CpuGrid::new(vec![])
962                .with_governor(CpuGovernor::Unknown)
963                .governor_display(),
964            "unknown"
965        );
966    }
967}