new feature: gpu support

This commit is contained in:
2025-08-11 12:04:55 -07:00
parent a4f69a5f7d
commit 20278d67f1
11 changed files with 525 additions and 33 deletions
+46 -18
View File
@@ -13,7 +13,8 @@ use crossterm::{
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Rect}, // add Rect
layout::{Constraint, Direction, Rect},
//style::Color, // + add Color
Terminal,
};
use tokio::time::sleep;
@@ -33,6 +34,7 @@ use crate::ui::{
processes::draw_top_processes,
swap::draw_swap,
};
use crate::ui::gpu::draw_gpu;
use crate::ws::{connect, request_metrics};
pub struct App {
@@ -243,52 +245,77 @@ impl App {
self.last_metrics = Some(m);
}
fn draw(&mut self, f: &mut ratatui::Frame<'_>) {
pub fn draw(&mut self, f: &mut ratatui::Frame<'_>) {
let area = f.area();
// Root rows: header, top (cpu avg + per-core), memory, swap, bottom
let rows = ratatui::layout::Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Ratio(1, 3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(1), // header
Constraint::Ratio(1, 3), // top row
Constraint::Length(3), // memory (left) + GPU (right, part 1)
Constraint::Length(3), // swap (left) + GPU (right, part 2)
Constraint::Min(10), // bottom: disks + net (left), top procs (right)
])
.split(area);
// Header
draw_header(f, rows[0], self.last_metrics.as_ref());
let top = ratatui::layout::Layout::default()
// Top row: left CPU avg, right Per-core (full top-right)
let top_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[1]);
draw_cpu_avg_graph(f, top[0], &self.cpu_hist, self.last_metrics.as_ref());
draw_cpu_avg_graph(f, top_lr[0], &self.cpu_hist, self.last_metrics.as_ref());
draw_per_core_bars(
f,
top[1],
top_lr[1],
self.last_metrics.as_ref(),
&self.per_core_hist,
self.per_core_scroll,
);
draw_mem(f, rows[2], self.last_metrics.as_ref());
draw_swap(f, rows[3], self.last_metrics.as_ref());
// Memory + Swap rows split into left/right columns
let mem_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[2]);
let swap_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[3]);
let bottom = ratatui::layout::Layout::default()
// Left: Memory + Swap
draw_mem(f, mem_lr[0], self.last_metrics.as_ref());
draw_swap(f, swap_lr[0], self.last_metrics.as_ref());
// Right: GPU spans the same vertical space as Memory + Swap
let gpu_area = ratatui::layout::Rect {
x: mem_lr[1].x,
y: mem_lr[1].y,
width: mem_lr[1].width,
height: mem_lr[1].height + swap_lr[1].height,
};
draw_gpu(f, gpu_area, self.last_metrics.as_ref());
// Bottom area: left = Disks + Network, right = Top Processes
let bottom_lr = ratatui::layout::Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
.split(rows[4]);
// Left bottom: Disks + Net stacked (network "back up")
let left_stack = ratatui::layout::Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(6),
Constraint::Length(4),
Constraint::Length(4),
Constraint::Min(7), // Disks grow
Constraint::Length(3), // Download
Constraint::Length(3), // Upload
])
.split(bottom[0]);
.split(bottom_lr[0]);
draw_disks(f, left_stack[0], self.last_metrics.as_ref());
draw_net_spark(
@@ -314,7 +341,8 @@ impl App {
ratatui::style::Color::Blue,
);
draw_top_processes(f, bottom[1], self.last_metrics.as_ref());
// Right bottom: Top Processes fills the column
draw_top_processes(f, bottom_lr[1], self.last_metrics.as_ref());
}
}
+10
View File
@@ -24,6 +24,15 @@ pub struct ProcessInfo {
pub mem_bytes: u64,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct GpuMetrics {
pub name: String,
pub utilization_gpu_pct: u32,
pub mem_used_bytes: u64,
pub mem_total_bytes: u64,
// pub vendor: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Metrics {
pub cpu_total: f32,
@@ -38,4 +47,5 @@ pub struct Metrics {
pub disks: Vec<Disk>,
pub networks: Vec<Network>,
pub top_processes: Vec<ProcessInfo>,
pub gpus: Option<Vec<GpuMetrics>>,
}
+115
View File
@@ -0,0 +1,115 @@
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, Borders, Gauge, Paragraph},
};
use crate::types::Metrics;
fn fmt_bytes(b: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let fb = b as f64;
if fb >= GB { format!("{:.1}G", fb / GB) }
else if fb >= MB { format!("{:.1}M", fb / MB) }
else if fb >= KB { format!("{:.1}K", fb / KB) }
else { format!("{}B", b) }
}
pub fn draw_gpu(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let mut area = area;
let block = Block::default().borders(Borders::ALL).title("GPU");
f.render_widget(block, area);
// Guard: need some space inside the block
if area.height <= 2 || area.width <= 2 {
return;
}
// Inner padding consistent with the rest of the app
area.y += 1;
area.height = area.height.saturating_sub(2);
area.x += 1;
area.width = area.width.saturating_sub(2);
let Some(metrics) = m else { return; };
let Some(gpus) = metrics.gpus.as_ref() else {
f.render_widget(Paragraph::new("No GPUs"), area);
return;
};
if gpus.is_empty() {
f.render_widget(Paragraph::new("No GPUs"), area);
return;
}
// Show 3 rows per GPU: name, util bar, vram bar.
if area.height < 3 {
return;
}
let per_gpu_rows: u16 = 3;
let max_gpus = (area.height / per_gpu_rows) as usize;
let count = gpus.len().min(max_gpus);
let constraints = std::iter::repeat(Constraint::Length(1))
.take(count * per_gpu_rows as usize)
.collect::<Vec<_>>();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(area);
// Per bar horizontal layout: [gauge] [value]
let split_bar = |r: Rect| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(8), // gauge column
Constraint::Length(24), // value column
])
.split(r)
};
for i in 0..count {
let g = &gpus[i];
// Row 1: GPU name (and temp can be appended later)
let name_text = format!("{}", g.name);
f.render_widget(
Paragraph::new(Span::raw(name_text)).style(Style::default().fg(Color::Gray)),
rows[i * 3],
);
// Row 2: Utilization bar + right label
let util_cols = split_bar(rows[i * 3 + 1]);
let util = g.utilization_gpu_pct.min(100) as u16;
let util_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Green))
.label(Span::raw(""))
.ratio(util as f64 / 100.0);
f.render_widget(util_gauge, util_cols[0]);
f.render_widget(
Paragraph::new(Span::raw(format!("util: {}%", util))).style(Style::default().fg(Color::Gray)),
util_cols[1],
);
// Row 3: VRAM bar + right label
let mem_cols = split_bar(rows[i * 3 + 2]);
let used = g.mem_used_bytes;
let total = g.mem_total_bytes.max(1);
let mem_ratio = used as f64 / total as f64;
let mem_pct = (mem_ratio * 100.0).round() as u16;
let mem_gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::LightMagenta))
.label(Span::raw(""))
.ratio(mem_ratio);
f.render_widget(mem_gauge, mem_cols[0]);
f.render_widget(
Paragraph::new(Span::raw(format!("vram: {}/{} ({}%)", fmt_bytes(used), fmt_bytes(total), mem_pct)))
.style(Style::default().fg(Color::Gray)),
mem_cols[1],
);
}
}
+1
View File
@@ -2,6 +2,7 @@
pub mod cpu;
pub mod disks;
pub mod gpu;
pub mod header;
pub mod mem;
pub mod net;