1use 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#[derive(Debug, Clone, Copy)]
12pub struct ThermalThresholds {
13 pub warning: f64,
15 pub critical: f64,
17 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#[derive(Debug, Clone, Default)]
33pub struct SensorReading {
34 pub name: String,
36 pub temp_c: f64,
38}
39
40pub struct ThermalPanelBrick {
42 pub cpu_temp_history: Vec<f64>,
44 pub gpu_temp_history: Vec<f64>,
46 pub cpu_temp: f64,
48 pub gpu_temp: f64,
50 pub sensors: Vec<SensorReading>,
52 pub thresholds: ThermalThresholds,
54 pub theme: Theme,
56}
57
58impl ThermalPanelBrick {
59 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 fn temp_color(&self, temp: f64) -> Color {
74 if temp >= self.thresholds.critical {
75 Color::new(1.0, 0.2, 0.2, 1.0) } else if temp >= self.thresholds.warning {
77 Color::new(1.0, 0.8, 0.2, 1.0) } else {
79 Color::new(0.3, 1.0, 0.5, 1.0) }
81 }
82
83 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 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 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 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 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 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 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 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 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 let ok_color = panel.temp_color(50.0);
242 assert!(ok_color.g > ok_color.r);
243
244 let warn_color = panel.temp_color(75.0);
246 assert!(warn_color.r > 0.5 && warn_color.g > 0.5);
247
248 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}