Skip to main content

cbtop/bricks/panels/
thermal.rs

1//! Thermal panel brick (Layer 3)
2//!
3//! Displays CPU and GPU temperature monitoring with warning thresholds.
4
5use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
6use presentar_core::{Canvas, Color, Point, Rect, TextStyle, Widget};
7use presentar_terminal::{BrailleGraph, GraphMode, Meter, Theme};
8use std::any::Any;
9
10/// Temperature thresholds for thermal warnings
11#[derive(Debug, Clone, Copy)]
12pub struct ThermalThresholds {
13    /// Warning temperature (yellow)
14    pub warning: f64,
15    /// Critical temperature (red)
16    pub critical: f64,
17    /// Maximum safe temperature
18    pub max_safe: f64,
19}
20
21impl Default for ThermalThresholds {
22    fn default() -> Self {
23        Self {
24            warning: 70.0,
25            critical: 85.0,
26            max_safe: 100.0,
27        }
28    }
29}
30
31/// Thermal metrics for a single sensor
32#[derive(Debug, Clone, Default)]
33pub struct SensorReading {
34    /// Sensor name
35    pub name: String,
36    /// Current temperature in Celsius
37    pub temp_c: f64,
38}
39
40/// Thermal panel for temperature monitoring
41pub struct ThermalPanelBrick {
42    /// CPU temperature history
43    pub cpu_temp_history: Vec<f64>,
44    /// GPU temperature history
45    pub gpu_temp_history: Vec<f64>,
46    /// Current CPU temperature
47    pub cpu_temp: f64,
48    /// Current GPU temperature
49    pub gpu_temp: f64,
50    /// Additional sensor readings
51    pub sensors: Vec<SensorReading>,
52    /// Temperature thresholds
53    pub thresholds: ThermalThresholds,
54    /// Theme for rendering
55    pub theme: Theme,
56}
57
58impl ThermalPanelBrick {
59    /// Create a new thermal panel
60    pub fn new() -> Self {
61        Self {
62            cpu_temp_history: Vec::new(),
63            gpu_temp_history: Vec::new(),
64            cpu_temp: 0.0,
65            gpu_temp: 0.0,
66            sensors: Vec::new(),
67            thresholds: ThermalThresholds::default(),
68            theme: Theme::tokyo_night(),
69        }
70    }
71
72    /// Get color for temperature value based on thresholds
73    fn temp_color(&self, temp: f64) -> Color {
74        if temp >= self.thresholds.critical {
75            Color::new(1.0, 0.2, 0.2, 1.0) // Red
76        } else if temp >= self.thresholds.warning {
77            Color::new(1.0, 0.8, 0.2, 1.0) // Yellow/Orange
78        } else {
79            Color::new(0.3, 1.0, 0.5, 1.0) // Green
80        }
81    }
82
83    /// Get status string for temperature
84    fn temp_status(&self, temp: f64) -> &'static str {
85        if temp >= self.thresholds.critical {
86            "CRITICAL"
87        } else if temp >= self.thresholds.warning {
88            "WARNING"
89        } else {
90            "OK"
91        }
92    }
93
94    /// Paint the thermal panel
95    pub fn paint(&self, canvas: &mut dyn Canvas, width: f32, _height: f32) {
96        let label_style = TextStyle {
97            color: self.theme.foreground,
98            ..Default::default()
99        };
100        let dim_style = TextStyle {
101            color: self.theme.dim,
102            ..Default::default()
103        };
104
105        canvas.draw_text("Thermal Monitor", Point::new(2.0, 2.0), &label_style);
106
107        // CPU Temperature
108        canvas.draw_text("CPU:", Point::new(2.0, 4.0), &dim_style);
109        let cpu_color = self.temp_color(self.cpu_temp);
110        let cpu_status = self.temp_status(self.cpu_temp);
111        canvas.draw_text(
112            &format!("{:.1}°C [{}]", self.cpu_temp, cpu_status),
113            Point::new(8.0, 4.0),
114            &TextStyle {
115                color: cpu_color,
116                ..Default::default()
117            },
118        );
119
120        // CPU temperature gauge
121        let cpu_pct = (self.cpu_temp / self.thresholds.max_safe * 100.0).min(100.0);
122        let mut cpu_meter = Meter::new(cpu_pct, 100.0).with_color(cpu_color);
123        cpu_meter.layout(Rect::new(2.0, 5.0, width - 20.0, 1.0));
124        cpu_meter.paint(canvas);
125
126        // CPU temperature history graph
127        if !self.cpu_temp_history.is_empty() {
128            let mut graph = BrailleGraph::new(self.cpu_temp_history.clone())
129                .with_color(cpu_color)
130                .with_range(0.0, self.thresholds.max_safe)
131                .with_mode(GraphMode::Braille);
132            graph.layout(Rect::new(2.0, 6.0, width - 4.0, 4.0));
133            graph.paint(canvas);
134        }
135
136        // GPU Temperature
137        canvas.draw_text("GPU:", Point::new(2.0, 11.0), &dim_style);
138        let gpu_color = self.temp_color(self.gpu_temp);
139        let gpu_status = self.temp_status(self.gpu_temp);
140        canvas.draw_text(
141            &format!("{:.1}°C [{}]", self.gpu_temp, gpu_status),
142            Point::new(8.0, 11.0),
143            &TextStyle {
144                color: gpu_color,
145                ..Default::default()
146            },
147        );
148
149        // GPU temperature gauge
150        let gpu_pct = (self.gpu_temp / self.thresholds.max_safe * 100.0).min(100.0);
151        let mut gpu_meter = Meter::new(gpu_pct, 100.0).with_color(gpu_color);
152        gpu_meter.layout(Rect::new(2.0, 12.0, width - 20.0, 1.0));
153        gpu_meter.paint(canvas);
154
155        // GPU temperature history graph
156        if !self.gpu_temp_history.is_empty() {
157            let mut graph = BrailleGraph::new(self.gpu_temp_history.clone())
158                .with_color(gpu_color)
159                .with_range(0.0, self.thresholds.max_safe)
160                .with_mode(GraphMode::Braille);
161            graph.layout(Rect::new(2.0, 13.0, width - 4.0, 4.0));
162            graph.paint(canvas);
163        }
164
165        // Additional sensors
166        if !self.sensors.is_empty() {
167            canvas.draw_text("Other Sensors:", Point::new(2.0, 18.0), &dim_style);
168            for (i, sensor) in self.sensors.iter().enumerate().take(4) {
169                let y = 19.0 + i as f32;
170                let color = self.temp_color(sensor.temp_c);
171                canvas.draw_text(
172                    &format!("{}: {:.1}°C", sensor.name, sensor.temp_c),
173                    Point::new(2.0, y),
174                    &TextStyle {
175                        color,
176                        ..Default::default()
177                    },
178                );
179            }
180        }
181    }
182}
183
184impl Default for ThermalPanelBrick {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190impl Brick for ThermalPanelBrick {
191    fn brick_name(&self) -> &'static str {
192        "thermal_panel"
193    }
194
195    fn assertions(&self) -> Vec<BrickAssertion> {
196        vec![
197            BrickAssertion::MinWidth(40),
198            BrickAssertion::MinHeight(20),
199            BrickAssertion::max_latency_ms(8),
200        ]
201    }
202
203    fn budget(&self) -> BrickBudget {
204        BrickBudget::FRAME_60FPS
205    }
206
207    fn verify(&self) -> BrickVerification {
208        let mut v = BrickVerification::new();
209        for assertion in self.assertions() {
210            v.check(&assertion);
211        }
212        v
213    }
214
215    fn as_any(&self) -> &dyn Any {
216        self
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_thermal_panel_brick_name() {
226        let panel = ThermalPanelBrick::new();
227        assert_eq!(panel.brick_name(), "thermal_panel");
228    }
229
230    #[test]
231    fn test_thermal_panel_has_assertions() {
232        let panel = ThermalPanelBrick::new();
233        assert!(!panel.assertions().is_empty());
234    }
235
236    #[test]
237    fn test_temp_color_thresholds() {
238        let panel = ThermalPanelBrick::new();
239
240        // Below warning should be green-ish
241        let ok_color = panel.temp_color(50.0);
242        assert!(ok_color.g > ok_color.r);
243
244        // Warning should be yellow-ish
245        let warn_color = panel.temp_color(75.0);
246        assert!(warn_color.r > 0.5 && warn_color.g > 0.5);
247
248        // Critical should be red-ish
249        let crit_color = panel.temp_color(90.0);
250        assert!(crit_color.r > crit_color.g);
251    }
252
253    #[test]
254    fn test_temp_status() {
255        let panel = ThermalPanelBrick::new();
256        assert_eq!(panel.temp_status(50.0), "OK");
257        assert_eq!(panel.temp_status(75.0), "WARNING");
258        assert_eq!(panel.temp_status(90.0), "CRITICAL");
259    }
260}