1#![allow(dead_code)] use presentar_core::{
9 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
10 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
11};
12use std::any::Any;
13use std::time::Duration;
14
15const BAR_CHARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum GpuVendor {
21 Nvidia,
22 Amd,
23 Intel,
24 #[default]
25 Unknown,
26}
27
28impl GpuVendor {
29 pub fn as_str(&self) -> &'static str {
31 match self {
32 Self::Nvidia => "NVIDIA",
33 Self::Amd => "AMD",
34 Self::Intel => "Intel",
35 Self::Unknown => "GPU",
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct GpuDevice {
43 pub index: u32,
45 pub name: String,
47 pub vendor: GpuVendor,
49 pub utilization: f32,
51 pub temperature: Option<f32>,
53 pub vram_total: u64,
55 pub vram_used: u64,
57 pub power_draw: Option<f32>,
59 pub power_limit: Option<f32>,
61 pub fan_speed: Option<u32>,
63}
64
65impl Default for GpuDevice {
66 fn default() -> Self {
67 Self {
68 index: 0,
69 name: "Unknown GPU".to_string(),
70 vendor: GpuVendor::Unknown,
71 utilization: 0.0,
72 temperature: None,
73 vram_total: 0,
74 vram_used: 0,
75 power_draw: None,
76 power_limit: None,
77 fan_speed: None,
78 }
79 }
80}
81
82impl GpuDevice {
83 #[must_use]
85 pub fn new(name: impl Into<String>) -> Self {
86 Self {
87 name: name.into(),
88 ..Default::default()
89 }
90 }
91
92 #[must_use]
94 pub fn with_vendor(mut self, vendor: GpuVendor) -> Self {
95 self.vendor = vendor;
96 self
97 }
98
99 #[must_use]
101 pub fn with_utilization(mut self, util: f32) -> Self {
102 self.utilization = util;
103 self
104 }
105
106 #[must_use]
108 pub fn with_temperature(mut self, temp: f32) -> Self {
109 self.temperature = Some(temp);
110 self
111 }
112
113 #[must_use]
115 pub fn with_vram(mut self, used: u64, total: u64) -> Self {
116 self.vram_used = used;
117 self.vram_total = total;
118 self
119 }
120
121 #[must_use]
123 pub fn with_power(mut self, draw: f32, limit: Option<f32>) -> Self {
124 self.power_draw = Some(draw);
125 self.power_limit = limit;
126 self
127 }
128
129 #[must_use]
131 pub fn with_fan(mut self, speed: u32) -> Self {
132 self.fan_speed = Some(speed);
133 self
134 }
135
136 pub fn vram_percent(&self) -> f32 {
138 if self.vram_total > 0 {
139 (self.vram_used as f64 / self.vram_total as f64 * 100.0) as f32
140 } else {
141 0.0
142 }
143 }
144
145 pub fn vram_display(&self) -> String {
147 let used_gb = self.vram_used as f64 / 1_073_741_824.0;
148 let total_gb = self.vram_total as f64 / 1_073_741_824.0;
149 format!("{used_gb:.1}G / {total_gb:.1}G")
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct GpuProcess {
156 pub name: String,
158 pub pid: u32,
160 pub vram_used: u64,
162}
163
164impl GpuProcess {
165 #[must_use]
167 pub fn new(name: impl Into<String>, pid: u32, vram: u64) -> Self {
168 Self {
169 name: name.into(),
170 pid,
171 vram_used: vram,
172 }
173 }
174
175 pub fn vram_display(&self) -> String {
177 let mb = self.vram_used / (1024 * 1024);
178 if mb >= 1024 {
179 format!("{:.1}G", mb as f64 / 1024.0)
180 } else {
181 format!("{mb}M")
182 }
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct GpuPanel {
189 device: GpuDevice,
191 processes: Vec<GpuProcess>,
193 bar_color: Color,
195 temp_color: Color,
197 show_processes: bool,
199 max_processes: usize,
201 bounds: Rect,
203}
204
205impl Default for GpuPanel {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211impl GpuPanel {
212 #[must_use]
214 pub fn new() -> Self {
215 Self {
216 device: GpuDevice::default(),
217 processes: Vec::new(),
218 bar_color: Color::new(0.4, 0.8, 0.4, 1.0), temp_color: Color::new(1.0, 0.8, 0.2, 1.0), show_processes: true,
221 max_processes: 3,
222 bounds: Rect::default(),
223 }
224 }
225
226 #[must_use]
228 pub fn with_device(mut self, device: GpuDevice) -> Self {
229 self.device = device;
230 self
231 }
232
233 #[must_use]
235 pub fn with_processes(mut self, processes: Vec<GpuProcess>) -> Self {
236 self.processes = processes;
237 self
238 }
239
240 pub fn add_process(&mut self, process: GpuProcess) {
242 self.processes.push(process);
243 }
244
245 #[must_use]
247 pub fn with_bar_color(mut self, color: Color) -> Self {
248 self.bar_color = color;
249 self
250 }
251
252 #[must_use]
254 pub fn show_processes(mut self, show: bool) -> Self {
255 self.show_processes = show;
256 self
257 }
258
259 #[must_use]
261 pub fn max_processes(mut self, max: usize) -> Self {
262 self.max_processes = max;
263 self
264 }
265
266 fn draw_util_bar(&self, canvas: &mut dyn Canvas, y: f32, width: f32) {
268 use std::fmt::Write;
269 let util = self.device.utilization;
270 let bar_width = (width - 6.0) as usize; let filled = ((util / 100.0) * bar_width as f32) as usize;
272
273 let mut bar = String::new();
274 for i in 0..bar_width {
275 if i < filled {
276 bar.push('█');
277 } else {
278 bar.push('░');
279 }
280 }
281 write!(bar, " {util:3.0}%").ok();
282
283 canvas.draw_text(
284 &bar,
285 Point::new(self.bounds.x, y),
286 &TextStyle {
287 color: self.bar_color,
288 ..Default::default()
289 },
290 );
291 }
292
293 fn draw_info(&self, canvas: &mut dyn Canvas, start_y: f32) -> f32 {
295 use std::fmt::Write as _;
296 let mut y = start_y;
297 let x = self.bounds.x;
298
299 let mut info_line = String::new();
301 if let Some(temp) = self.device.temperature {
302 write!(info_line, "Temp: {temp:3.0}°C").ok();
303 }
304 if let Some(power) = self.device.power_draw {
305 if !info_line.is_empty() {
306 info_line.push_str(" ");
307 }
308 write!(info_line, "Power: {power:3.0}W").ok();
309 }
310 if !info_line.is_empty() {
311 canvas.draw_text(
312 &info_line,
313 Point::new(x, y),
314 &TextStyle {
315 color: Color::WHITE,
316 ..Default::default()
317 },
318 );
319 y += 1.0;
320 }
321
322 if self.device.vram_total > 0 {
324 let vram_line = format!(
325 "VRAM: {} ({:.0}%)",
326 self.device.vram_display(),
327 self.device.vram_percent()
328 );
329 canvas.draw_text(
330 &vram_line,
331 Point::new(x, y),
332 &TextStyle {
333 color: Color::WHITE,
334 ..Default::default()
335 },
336 );
337 y += 1.0;
338 }
339
340 if let Some(fan) = self.device.fan_speed {
342 canvas.draw_text(
343 &format!("Fan: {fan}%"),
344 Point::new(x, y),
345 &TextStyle {
346 color: Color::WHITE,
347 ..Default::default()
348 },
349 );
350 y += 1.0;
351 }
352
353 y
354 }
355
356 fn draw_processes(&self, canvas: &mut dyn Canvas, start_y: f32) {
358 if !self.show_processes || self.processes.is_empty() {
359 return;
360 }
361
362 let x = self.bounds.x;
363 let mut y = start_y;
364
365 let mut sorted: Vec<_> = self.processes.iter().collect();
367 sorted.sort_by(|a, b| b.vram_used.cmp(&a.vram_used));
368
369 for proc in sorted.iter().take(self.max_processes) {
370 let max_name_len = 12;
372 let name: String = if proc.name.len() > max_name_len {
373 format!("{}...", &proc.name[..max_name_len - 3])
374 } else {
375 proc.name.clone()
376 };
377
378 let line = format!("{:<12} {:>6}", name, proc.vram_display());
379 canvas.draw_text(
380 &line,
381 Point::new(x, y),
382 &TextStyle {
383 color: Color::new(0.7, 0.7, 0.7, 1.0),
384 ..Default::default()
385 },
386 );
387 y += 1.0;
388 }
389 }
390}
391
392impl Brick for GpuPanel {
393 fn brick_name(&self) -> &'static str {
394 "gpu_panel"
395 }
396
397 fn assertions(&self) -> &[BrickAssertion] {
398 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(8)];
399 ASSERTIONS
400 }
401
402 fn budget(&self) -> BrickBudget {
403 BrickBudget::uniform(8)
404 }
405
406 fn verify(&self) -> BrickVerification {
407 BrickVerification {
408 passed: vec![BrickAssertion::max_latency_ms(8)],
409 failed: vec![],
410 verification_time: Duration::from_micros(25),
411 }
412 }
413
414 fn to_html(&self) -> String {
415 String::new()
416 }
417
418 fn to_css(&self) -> String {
419 String::new()
420 }
421}
422
423impl Widget for GpuPanel {
424 fn type_id(&self) -> TypeId {
425 TypeId::of::<Self>()
426 }
427
428 fn measure(&self, constraints: Constraints) -> Size {
429 let height = 8.0_f32.min(constraints.max_height);
431 Size::new(constraints.max_width, height)
432 }
433
434 fn layout(&mut self, bounds: Rect) -> LayoutResult {
435 self.bounds = bounds;
436 LayoutResult {
437 size: Size::new(bounds.width, bounds.height),
438 }
439 }
440
441 fn paint(&self, canvas: &mut dyn Canvas) {
442 if self.bounds.width < 10.0 || self.bounds.height < 3.0 {
443 return;
444 }
445
446 let mut y = self.bounds.y;
447
448 self.draw_util_bar(canvas, y, self.bounds.width);
450 y += 1.0;
451
452 y = self.draw_info(canvas, y);
454
455 self.draw_processes(canvas, y);
457 }
458
459 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
460 None
461 }
462
463 fn children(&self) -> &[Box<dyn Widget>] {
464 &[]
465 }
466
467 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
468 &mut []
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn test_gpu_device_vram_percent() {
478 let device = GpuDevice::default().with_vram(4 * 1024 * 1024 * 1024, 8 * 1024 * 1024 * 1024);
479 assert!((device.vram_percent() - 50.0).abs() < 0.1);
480 }
481
482 #[test]
483 fn test_gpu_device_vram_display() {
484 let device = GpuDevice::default().with_vram(4 * 1024 * 1024 * 1024, 8 * 1024 * 1024 * 1024);
485 assert_eq!(device.vram_display(), "4.0G / 8.0G");
486 }
487
488 #[test]
489 fn test_gpu_process_vram_display() {
490 let proc_mb = GpuProcess::new("test", 1234, 512 * 1024 * 1024);
491 assert_eq!(proc_mb.vram_display(), "512M");
492
493 let proc_gb = GpuProcess::new("test", 1234, 2 * 1024 * 1024 * 1024);
494 assert_eq!(proc_gb.vram_display(), "2.0G");
495 }
496
497 #[test]
498 fn test_panel_default() {
499 let panel = GpuPanel::new();
500 assert!(panel.show_processes);
501 assert_eq!(panel.max_processes, 3);
502 }
503
504 #[test]
505 fn test_panel_builder() {
506 let device = GpuDevice::new("RTX 3080")
507 .with_vendor(GpuVendor::Nvidia)
508 .with_utilization(80.0)
509 .with_temperature(72.0)
510 .with_vram(8 * 1024 * 1024 * 1024, 10 * 1024 * 1024 * 1024)
511 .with_power(220.0, Some(320.0))
512 .with_fan(65);
513
514 let panel = GpuPanel::new().with_device(device).max_processes(5);
515
516 assert_eq!(panel.max_processes, 5);
517 assert_eq!(panel.device.name, "RTX 3080");
518 }
519
520 #[test]
521 fn test_gpu_vendor_as_str() {
522 assert_eq!(GpuVendor::Nvidia.as_str(), "NVIDIA");
523 assert_eq!(GpuVendor::Amd.as_str(), "AMD");
524 assert_eq!(GpuVendor::Intel.as_str(), "Intel");
525 assert_eq!(GpuVendor::Unknown.as_str(), "GPU");
526 }
527
528 #[test]
529 fn test_gpu_device_default() {
530 let device = GpuDevice::default();
531 assert_eq!(device.index, 0);
532 assert_eq!(device.vendor, GpuVendor::Unknown);
533 assert_eq!(device.utilization, 0.0);
534 assert!(device.temperature.is_none());
535 }
536
537 #[test]
538 fn test_gpu_device_zero_vram() {
539 let device = GpuDevice::default();
540 assert_eq!(device.vram_percent(), 0.0);
541 }
542
543 #[test]
544 fn test_gpu_panel_with_bar_color() {
545 let panel = GpuPanel::new().with_bar_color(Color::RED);
546 assert_eq!(
547 format!("{:?}", panel.bar_color),
548 format!("{:?}", Color::RED)
549 );
550 }
551
552 #[test]
553 fn test_gpu_panel_show_processes() {
554 let panel = GpuPanel::new().show_processes(false);
555 assert!(!panel.show_processes);
556 }
557
558 #[test]
559 fn test_gpu_panel_with_processes() {
560 let processes = vec![
561 GpuProcess::new("python", 1234, 1024 * 1024 * 1024),
562 GpuProcess::new("chrome", 5678, 512 * 1024 * 1024),
563 ];
564 let panel = GpuPanel::new().with_processes(processes);
565 assert_eq!(panel.processes.len(), 2);
566 }
567
568 #[test]
569 fn test_gpu_panel_add_process() {
570 let mut panel = GpuPanel::new();
571 panel.add_process(GpuProcess::new("test", 100, 256 * 1024 * 1024));
572 assert_eq!(panel.processes.len(), 1);
573 }
574
575 #[test]
576 fn test_gpu_panel_brick_traits() {
577 let panel = GpuPanel::new();
578 assert_eq!(panel.brick_name(), "gpu_panel");
579 assert!(!panel.assertions().is_empty());
580 assert!(panel.budget().paint_ms > 0);
581 assert!(panel.verify().is_valid());
582 assert!(panel.to_html().is_empty());
583 assert!(panel.to_css().is_empty());
584 }
585
586 #[test]
587 fn test_gpu_panel_widget_traits() {
588 let mut panel = GpuPanel::new();
589
590 let size = panel.measure(Constraints {
592 min_width: 0.0,
593 min_height: 0.0,
594 max_width: 80.0,
595 max_height: 20.0,
596 });
597 assert!(size.width > 0.0);
598 assert!(size.height > 0.0);
599
600 let result = panel.layout(Rect::new(0.0, 0.0, 80.0, 10.0));
602 assert_eq!(result.size.width, 80.0);
603
604 assert_eq!(Widget::type_id(&panel), TypeId::of::<GpuPanel>());
606
607 assert!(panel
609 .event(&Event::key_down(presentar_core::Key::Enter))
610 .is_none());
611
612 assert!(panel.children().is_empty());
614 assert!(panel.children_mut().is_empty());
615 }
616
617 #[test]
618 #[allow(clippy::identity_op)]
619 fn test_gpu_panel_paint_full() {
620 use crate::direct::{CellBuffer, DirectTerminalCanvas};
621
622 let device = GpuDevice::new("RTX 3080")
623 .with_vendor(GpuVendor::Nvidia)
624 .with_utilization(75.0)
625 .with_temperature(68.0)
626 .with_vram(6 * 1024 * 1024 * 1024, 10 * 1024 * 1024 * 1024)
627 .with_power(180.0, Some(320.0))
628 .with_fan(55);
629
630 let processes = vec![
631 GpuProcess::new("python3", 1234, 2 * 1024 * 1024 * 1024),
632 GpuProcess::new("verylongprocessname", 5678, 1 * 1024 * 1024 * 1024),
633 GpuProcess::new("chrome", 9012, 512 * 1024 * 1024),
634 ];
635
636 let mut panel = GpuPanel::new()
637 .with_device(device)
638 .with_processes(processes);
639
640 panel.layout(Rect::new(0.0, 0.0, 60.0, 12.0));
641
642 let mut buffer = CellBuffer::new(60, 12);
643 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
644 panel.paint(&mut canvas);
645 }
646
647 #[test]
648 fn test_gpu_panel_paint_without_processes() {
649 use crate::direct::{CellBuffer, DirectTerminalCanvas};
650
651 let device = GpuDevice::new("RTX 3080")
652 .with_utilization(50.0)
653 .with_temperature(60.0)
654 .with_vram(4 * 1024 * 1024 * 1024, 8 * 1024 * 1024 * 1024);
655
656 let mut panel = GpuPanel::new().with_device(device).show_processes(false);
657
658 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
659
660 let mut buffer = CellBuffer::new(60, 10);
661 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
662 panel.paint(&mut canvas);
663 }
664
665 #[test]
666 fn test_gpu_panel_paint_minimal_info() {
667 use crate::direct::{CellBuffer, DirectTerminalCanvas};
668
669 let device = GpuDevice::new("Generic GPU").with_utilization(30.0);
671
672 let mut panel = GpuPanel::new().with_device(device);
673 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
674
675 let mut buffer = CellBuffer::new(60, 10);
676 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
677 panel.paint(&mut canvas);
678 }
679
680 #[test]
681 fn test_gpu_panel_paint_small_bounds() {
682 use crate::direct::{CellBuffer, DirectTerminalCanvas};
683
684 let mut panel = GpuPanel::new();
685 panel.layout(Rect::new(0.0, 0.0, 5.0, 2.0)); let mut buffer = CellBuffer::new(5, 2);
688 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
689 panel.paint(&mut canvas); }
691
692 #[test]
693 fn test_gpu_panel_paint_only_power() {
694 use crate::direct::{CellBuffer, DirectTerminalCanvas};
695
696 let device = GpuDevice::new("GPU")
698 .with_utilization(40.0)
699 .with_power(150.0, None);
700
701 let mut panel = GpuPanel::new().with_device(device);
702 panel.layout(Rect::new(0.0, 0.0, 60.0, 10.0));
703
704 let mut buffer = CellBuffer::new(60, 10);
705 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
706 panel.paint(&mut canvas);
707 }
708
709 #[test]
710 fn test_gpu_panel_default_trait() {
711 let panel = GpuPanel::default();
712 assert!(panel.show_processes);
713 assert_eq!(panel.max_processes, 3);
714 }
715
716 #[test]
717 fn test_gpu_process_new() {
718 let proc = GpuProcess::new("test_process", 12345, 100 * 1024 * 1024);
719 assert_eq!(proc.name, "test_process");
720 assert_eq!(proc.pid, 12345);
721 assert_eq!(proc.vram_used, 100 * 1024 * 1024);
722 }
723
724 #[test]
725 fn test_gpu_device_all_builders() {
726 let device = GpuDevice::new("Test GPU")
727 .with_vendor(GpuVendor::Amd)
728 .with_utilization(95.0)
729 .with_temperature(85.0)
730 .with_vram(8 * 1024 * 1024 * 1024, 16 * 1024 * 1024 * 1024)
731 .with_power(250.0, Some(350.0))
732 .with_fan(80);
733
734 assert_eq!(device.vendor, GpuVendor::Amd);
735 assert_eq!(device.utilization, 95.0);
736 assert_eq!(device.temperature, Some(85.0));
737 assert_eq!(device.vram_used, 8 * 1024 * 1024 * 1024);
738 assert_eq!(device.vram_total, 16 * 1024 * 1024 * 1024);
739 assert_eq!(device.power_draw, Some(250.0));
740 assert_eq!(device.power_limit, Some(350.0));
741 assert_eq!(device.fan_speed, Some(80));
742 }
743}