new feature: gpu support
This commit is contained in:
+46
-18
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod cpu;
|
||||
pub mod disks;
|
||||
pub mod gpu;
|
||||
pub mod header;
|
||||
pub mod mem;
|
||||
pub mod net;
|
||||
|
||||
Reference in New Issue
Block a user