v1 implemented

it compiles
This commit is contained in:
2026-02-19 01:19:05 +00:00
commit 211cf6b017
10 changed files with 1498 additions and 0 deletions

263
src/ui.rs Normal file
View File

@@ -0,0 +1,263 @@
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
Frame,
};
use crate::state::AppState;
pub fn draw(frame: &mut Frame, state: &AppState) {
let root = frame.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(7),
Constraint::Length(1),
])
.split(root);
draw_header(frame, chunks[0], state);
draw_body(frame, chunks[1], state);
draw_footer(frame, chunks[2], state);
}
fn draw_header(frame: &mut Frame, area: Rect, state: &AppState) {
let uptime = format_uptime(state.snapshot.uptime_seconds);
let refresh_ms = state.refresh_interval.as_millis();
let title = format!(
" {} | uptime {} | proc {} | load(1m) {:.2} | {}ms {}",
state.snapshot.host,
uptime,
state.snapshot.process_count,
state.snapshot.load_avg_1m,
refresh_ms,
if state.paused { "[PAUSED]" } else { "" }
);
frame.render_widget(
Paragraph::new(title)
.block(Block::default().borders(Borders::ALL).title("sloptop"))
.alignment(Alignment::Left),
area,
);
}
fn draw_body(frame: &mut Frame, area: Rect, state: &AppState) {
if area.width < 90 {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(6),
Constraint::Min(4),
])
.split(area);
draw_cpu(frame, chunks[0], state);
draw_memory(frame, chunks[1], state);
draw_network(frame, chunks[2], state);
draw_disks(frame, chunks[3], state);
return;
}
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(5),
])
.split(chunks[0]);
draw_cpu(frame, left_chunks[0], state);
draw_memory(frame, left_chunks[1], state);
draw_disks(frame, left_chunks[2], state);
draw_network(frame, chunks[1], state);
}
fn draw_cpu(frame: &mut Frame, area: Rect, state: &AppState) {
let gauge = Gauge::default()
.block(Block::default().title("CPU").borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.percent(state.snapshot.cpu_total_percent.clamp(0.0, 100.0) as u16)
.label(format!("{:>5.1}%", state.snapshot.cpu_total_percent));
frame.render_widget(gauge, area);
}
fn draw_memory(frame: &mut Frame, area: Rect, state: &AppState) {
let mem_pct = percent(
state.snapshot.mem_used_bytes,
state.snapshot.mem_total_bytes,
);
let swap_pct = percent(
state.snapshot.swap_used_bytes,
state.snapshot.swap_total_bytes,
);
let text = vec![Line::from(vec![
Span::raw(format!("mem {:>5.1}% ", mem_pct)),
Span::styled(
format!(
"{} / {}",
fmt_bytes(state.snapshot.mem_used_bytes),
fmt_bytes(state.snapshot.mem_total_bytes)
),
Style::default().fg(Color::Green),
),
Span::raw(format!(
" swap {:>5.1}% {} / {}",
swap_pct,
fmt_bytes(state.snapshot.swap_used_bytes),
fmt_bytes(state.snapshot.swap_total_bytes)
)),
])];
frame.render_widget(
Paragraph::new(text)
.block(Block::default().title("Memory").borders(Borders::ALL))
.wrap(Wrap { trim: true }),
area,
);
}
fn draw_network(frame: &mut Frame, area: Rect, state: &AppState) {
let mut lines = vec![
Line::from(format!(
"RX: {}/s",
fmt_bytes(state.network_rate.rx_bps as u64)
)),
Line::from(format!(
"TX: {}/s",
fmt_bytes(state.network_rate.tx_bps as u64)
)),
Line::from(format!(
"Total RX: {}",
fmt_bytes(state.snapshot.network_totals.rx_bytes)
)),
Line::from(format!(
"Total TX: {}",
fmt_bytes(state.snapshot.network_totals.tx_bytes)
)),
];
if state.detailed_view {
lines.push(Line::from(""));
lines.push(Line::from("Per-core CPU:"));
for (idx, usage) in state
.snapshot
.cpu_per_core_percent
.iter()
.enumerate()
.take(10)
{
lines.push(Line::from(format!("core {:02}: {:>5.1}%", idx, usage)));
}
if state.snapshot.cpu_per_core_percent.len() > 10 {
lines.push(Line::from(format!(
"... and {} more cores",
state.snapshot.cpu_per_core_percent.len() - 10
)));
}
}
frame.render_widget(
Paragraph::new(lines)
.block(
Block::default()
.title("Network / Details")
.borders(Borders::ALL),
)
.wrap(Wrap { trim: true }),
area,
);
}
fn draw_disks(frame: &mut Frame, area: Rect, state: &AppState) {
let mut lines = Vec::new();
if state.snapshot.disk_samples.is_empty() {
lines.push(Line::from("No disks reported"));
} else {
for disk in &state.snapshot.disk_samples {
let pct = percent(disk.used_bytes, disk.total_bytes);
lines.push(Line::from(format!(
"{} {:>5.1}% {} / {}",
disk.name,
pct,
fmt_bytes(disk.used_bytes),
fmt_bytes(disk.total_bytes)
)));
}
}
frame.render_widget(
Paragraph::new(lines)
.block(Block::default().title("Disk").borders(Borders::ALL))
.wrap(Wrap { trim: true }),
area,
);
}
fn draw_footer(frame: &mut Frame, area: Rect, state: &AppState) {
let mode = if state.detailed_view {
"detail:on"
} else {
"detail:off"
};
let help = format!("q quit | p pause | +/- interval | d toggle detail | {mode}");
frame.render_widget(Paragraph::new(help), area);
}
fn percent(used: u64, total: u64) -> f64 {
if total == 0 {
return 0.0;
}
(used as f64 / total as f64) * 100.0
}
fn fmt_bytes(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
if bytes == 0 {
return "0 B".to_string();
}
let mut value = bytes as f64;
let mut idx = 0;
while value >= 1024.0 && idx < UNITS.len() - 1 {
value /= 1024.0;
idx += 1;
}
if idx == 0 {
format!("{} {}", bytes, UNITS[idx])
} else {
format!("{value:.1} {}", UNITS[idx])
}
}
fn format_uptime(seconds: u64) -> String {
let days = seconds / 86_400;
let hours = (seconds % 86_400) / 3_600;
let mins = (seconds % 3_600) / 60;
let secs = seconds % 60;
if days > 0 {
format!("{days}d {hours:02}:{mins:02}:{secs:02}")
} else {
format!("{hours:02}:{mins:02}:{secs:02}")
}
}