1use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
12use crate::bricks::collectors::gpu::GpuMetrics;
13use presentar_core::{Canvas, Point, Rect, TextStyle, Widget};
14use presentar_terminal::{BrailleGraph, GraphMode, Theme};
15use std::any::Any;
16
17pub struct GpuPanelBrick {
19 pub gpu_data: Vec<f64>,
21 pub theme: Theme,
23 pub current_metrics: Option<GpuMetrics>,
25 pub temperature_c: Option<u32>,
27 pub power_watts: Option<u32>,
29 pub power_limit_watts: Option<u32>,
31}
32
33impl GpuPanelBrick {
34 pub fn new() -> Self {
35 Self {
36 gpu_data: Vec::new(),
37 theme: Theme::tokyo_night(),
38 current_metrics: None,
39 temperature_c: None,
40 power_watts: None,
41 power_limit_watts: None,
42 }
43 }
44
45 pub fn update_from_metrics(&mut self, metrics: &GpuMetrics) {
47 self.gpu_data.push(metrics.utilization_gpu as f64);
49 if self.gpu_data.len() > 120 {
50 self.gpu_data.remove(0);
51 }
52 self.current_metrics = Some(metrics.clone());
53 }
54
55 pub fn update_temperature(&mut self, temp_c: u32) {
57 self.temperature_c = Some(temp_c);
58 }
59
60 pub fn update_power(&mut self, watts: u32, limit_watts: u32) {
62 self.power_watts = Some(watts);
63 self.power_limit_watts = Some(limit_watts);
64 }
65
66 pub fn paint(&self, canvas: &mut dyn Canvas, width: f32, _height: f32) {
67 let label_style = TextStyle {
68 color: self.theme.foreground,
69 ..Default::default()
70 };
71 let dim_style = TextStyle {
72 color: self.theme.dim,
73 ..Default::default()
74 };
75
76 canvas.draw_text("GPU Monitor", Point::new(2.0, 2.0), &label_style);
77
78 if !self.gpu_data.is_empty() {
80 let gpu_usage = self.gpu_data.last().copied().unwrap_or(0.0);
81 let mut graph = BrailleGraph::new(self.gpu_data.clone())
82 .with_color(self.theme.gpu_color(gpu_usage))
83 .with_range(0.0, 100.0)
84 .with_mode(GraphMode::Braille);
85 graph.layout(Rect::new(2.0, 3.0, width - 4.0, 8.0));
86 graph.paint(canvas);
87 }
88
89 let (device_name, vram_used_gb, vram_total_gb, util_pct) =
91 if let Some(ref m) = self.current_metrics {
92 (
93 m.device_name.as_str(),
94 m.memory_used_mb as f64 / 1024.0,
95 m.memory_total_mb as f64 / 1024.0,
96 m.utilization_gpu as f64,
97 )
98 } else {
99 ("No GPU detected", 0.0, 0.0, 0.0)
100 };
101
102 let data_source = if self.current_metrics.is_some() {
103 "CUPTI"
104 } else {
105 "none"
106 };
107 canvas.draw_text(
108 &format!("GPU Info ({})", data_source),
109 Point::new(2.0, 12.0),
110 &label_style,
111 );
112
113 canvas.draw_text("Device: ", Point::new(2.0, 13.0), &dim_style);
114 canvas.draw_text(device_name, Point::new(10.0, 13.0), &label_style);
115
116 canvas.draw_text("VRAM: ", Point::new(2.0, 14.0), &dim_style);
118 let vram_pct = if vram_total_gb > 0.0 {
119 (vram_used_gb / vram_total_gb) * 100.0
120 } else {
121 0.0
122 };
123 let vram_style = TextStyle {
124 color: self.theme.memory_color(vram_pct),
125 ..Default::default()
126 };
127 canvas.draw_text(
128 &format!(
129 "{:.1} / {:.1} GB ({:.0}%)",
130 vram_used_gb, vram_total_gb, vram_pct
131 ),
132 Point::new(8.0, 14.0),
133 &vram_style,
134 );
135
136 canvas.draw_text("Util: ", Point::new(2.0, 15.0), &dim_style);
138 let util_style = TextStyle {
139 color: self.theme.gpu_color(util_pct),
140 ..Default::default()
141 };
142 canvas.draw_text(
143 &format!("{:.0}%", util_pct),
144 Point::new(8.0, 15.0),
145 &util_style,
146 );
147
148 canvas.draw_text("Temp: ", Point::new(16.0, 15.0), &dim_style);
150 let temp = self.temperature_c.unwrap_or(0);
151 let temp_style = TextStyle {
152 color: self.theme.temp_color(temp as f64, 100.0),
153 ..Default::default()
154 };
155 canvas.draw_text(&format!("{} C", temp), Point::new(22.0, 15.0), &temp_style);
156
157 canvas.draw_text("Power: ", Point::new(2.0, 16.0), &dim_style);
159 let power = self.power_watts.unwrap_or(0);
160 let power_limit = self.power_limit_watts.unwrap_or(1);
161 let power_pct = (power as f64 / power_limit as f64) * 100.0;
162 let power_style = TextStyle {
163 color: self.theme.cpu_color(power_pct),
164 ..Default::default()
165 };
166 canvas.draw_text(
167 &format!("{}W / {}W ({:.0}%)", power, power_limit, power_pct),
168 Point::new(9.0, 16.0),
169 &power_style,
170 );
171 }
172}
173
174impl Brick for GpuPanelBrick {
175 fn brick_name(&self) -> &'static str {
176 "gpu_panel"
177 }
178
179 fn assertions(&self) -> Vec<BrickAssertion> {
180 vec![
181 BrickAssertion::MinWidth(40),
182 BrickAssertion::MinHeight(15),
183 BrickAssertion::max_latency_ms(8),
184 ]
185 }
186
187 fn budget(&self) -> BrickBudget {
188 BrickBudget::FRAME_60FPS
189 }
190
191 fn verify(&self) -> BrickVerification {
192 let mut v = BrickVerification::new();
193 for assertion in self.assertions() {
194 v.check(&assertion);
195 }
196 v
197 }
198
199 fn as_any(&self) -> &dyn Any {
200 self
201 }
202}
203
204impl Default for GpuPanelBrick {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use presentar_core::RecordingCanvas;
214 use std::time::Instant;
215
216 #[test]
217 fn test_gpu_panel_brick_name() {
218 let panel = GpuPanelBrick::new();
219 assert_eq!(panel.brick_name(), "gpu_panel");
220 }
221
222 #[test]
223 fn test_gpu_panel_paint_empty() {
224 let panel = GpuPanelBrick::new();
225 let mut canvas = RecordingCanvas::new();
226
227 panel.paint(&mut canvas, 80.0, 24.0);
228
229 assert!(!canvas.is_empty());
231 assert!(canvas.command_count() >= 1);
232 }
233
234 #[test]
235 fn test_gpu_panel_paint_with_data() {
236 let mut panel = GpuPanelBrick::new();
237
238 let metrics = GpuMetrics {
240 timestamp: Instant::now(),
241 device_index: 0,
242 device_name: "RTX 4090".to_string(),
243 utilization_gpu: 75,
244 memory_used_mb: 8192,
245 memory_total_mb: 24576,
246 };
247 panel.update_from_metrics(&metrics);
248 panel.update_temperature(72);
249 panel.update_power(250, 350);
250
251 let mut canvas = RecordingCanvas::new();
252 panel.paint(&mut canvas, 80.0, 24.0);
253
254 assert!(canvas.command_count() >= 5);
256 }
257
258 #[test]
259 fn test_gpu_panel_paint_with_graph() {
260 let mut panel = GpuPanelBrick::new();
261
262 for i in 0..10 {
264 let metrics = GpuMetrics {
265 timestamp: Instant::now(),
266 device_index: 0,
267 device_name: "Test GPU".to_string(),
268 utilization_gpu: (i * 10) as u32,
269 memory_used_mb: 1000,
270 memory_total_mb: 2000,
271 };
272 panel.update_from_metrics(&metrics);
273 }
274
275 let mut canvas = RecordingCanvas::new();
276 panel.paint(&mut canvas, 80.0, 24.0);
277
278 assert!(canvas.command_count() >= 5);
280 }
281
282 #[test]
283 fn test_gpu_panel_has_assertions() {
284 let panel = GpuPanelBrick::new();
285 assert!(!panel.assertions().is_empty());
286 }
287
288 #[test]
289 fn test_gpu_panel_update_from_metrics() {
290 let mut panel = GpuPanelBrick::new();
291
292 let metrics = GpuMetrics {
293 timestamp: Instant::now(),
294 device_index: 0,
295 device_name: "RTX 4090".to_string(),
296 utilization_gpu: 75,
297 memory_used_mb: 8192,
298 memory_total_mb: 24576,
299 };
300
301 panel.update_from_metrics(&metrics);
302
303 assert_eq!(panel.gpu_data.len(), 1);
304 assert_eq!(panel.gpu_data[0], 75.0);
305 assert!(panel.current_metrics.is_some());
306 let m = panel.current_metrics.as_ref().unwrap();
307 assert_eq!(m.device_name, "RTX 4090");
308 }
309
310 #[test]
311 fn test_gpu_panel_update_temperature() {
312 let mut panel = GpuPanelBrick::new();
313 panel.update_temperature(72);
314 assert_eq!(panel.temperature_c, Some(72));
315 }
316
317 #[test]
318 fn test_gpu_panel_update_power() {
319 let mut panel = GpuPanelBrick::new();
320 panel.update_power(250, 350);
321 assert_eq!(panel.power_watts, Some(250));
322 assert_eq!(panel.power_limit_watts, Some(350));
323 }
324
325 #[test]
326 fn test_gpu_panel_history_limit() {
327 let mut panel = GpuPanelBrick::new();
328
329 for i in 0..130 {
331 let metrics = GpuMetrics {
332 timestamp: Instant::now(),
333 device_index: 0,
334 device_name: "Test GPU".to_string(),
335 utilization_gpu: (i % 100) as u32,
336 memory_used_mb: 1000,
337 memory_total_mb: 2000,
338 };
339 panel.update_from_metrics(&metrics);
340 }
341
342 assert_eq!(panel.gpu_data.len(), 120);
343 assert_eq!(panel.gpu_data[0], 10.0);
345 assert_eq!(panel.gpu_data[119], 29.0);
346 }
347
348 #[test]
349 fn test_gpu_panel_default() {
350 let panel = GpuPanelBrick::default();
351 assert!(panel.gpu_data.is_empty());
352 assert!(panel.current_metrics.is_none());
353 assert!(panel.temperature_c.is_none());
354 assert!(panel.power_watts.is_none());
355 }
356}