pub mod burst;
pub mod cpu;
pub mod font;
pub mod gauge;
pub mod header;
pub mod memory;
pub mod modal;
pub mod network;
pub mod ports;
pub mod power;
pub mod processes;
pub mod screen;
pub mod scroll;
pub mod sessions;
pub mod spark;
pub mod temp;
pub mod text;
pub mod theme;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Paragraph};
use crate::app::{App, Focus};
use screen::{Body, Gauge, Screen, Tier};
pub fn draw(f: &mut Frame, app: &App) {
let area = f.area();
f.render_widget(Block::default().style(Style::default().bg(theme::BASE)), area);
let screen = Screen::resolve(area);
let split = Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).split(area);
render_footer(f, split[1], app);
let chunks = Layout::vertical([
Constraint::Length(screen.tier.header_rows()),
Constraint::Length(screen.tier.gauge_rows()),
Constraint::Min(6),
])
.split(split[0]);
header::render(f, chunks[0], app);
render_gauges(f, chunks[1], app, &screen);
render_body(f, chunks[2], app, &screen);
if let Some((pid, name)) = &app.pending_kill {
render_kill_dialog(f, area, *pid, name);
} else if let Some(pick) = &app.pending_port_pick {
render_port_picker(f, area, pick);
}
}
fn render_port_picker(f: &mut Frame, area: Rect, pick: &crate::app::PortPick) {
let mut lines = vec![
Line::styled(pick.label.clone(), Style::default().fg(theme::TEXT).bold()),
Line::from(""),
];
for (i, port) in pick.ports.iter().enumerate() {
lines.push(Line::from(vec![
Span::styled(format!("{}", i + 1), Style::default().fg(theme::ACCENT).bold()),
Span::styled(format!(" :{port}"), Style::default().fg(theme::TEXT)),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("n", Style::default().fg(theme::TEXT).bold()),
Span::styled(" cancel", Style::default().fg(theme::DIM)),
]));
modal::render(f, area, "open which port?", lines, theme::ACCENT);
}
fn render_kill_dialog(f: &mut Frame, area: Rect, pid: i32, name: &str) {
let lines = vec![
Line::styled(name.to_string(), Style::default().fg(theme::TEXT).bold()),
Line::styled(format!("pid {pid}"), Style::default().fg(theme::DIM)),
Line::from(""),
Line::from(vec![
Span::styled("y", Style::default().fg(theme::RED).bold()),
Span::styled(" confirm", Style::default().fg(theme::TEXT)),
Span::styled(" ", Style::default()),
Span::styled("n", Style::default().fg(theme::TEXT).bold()),
Span::styled(" cancel", Style::default().fg(theme::TEXT)),
]),
];
modal::render(f, area, "confirm kill", lines, theme::RED);
}
fn render_gauges(f: &mut Frame, area: Rect, app: &App, screen: &Screen) {
let half = |r: Rect| {
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(r).to_vec()
};
let cells: Vec<Rect> = match screen.tier {
Tier::Grid => {
let bands =
Layout::vertical([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
[half(bands[0]), half(bands[1])].concat()
}
_ => {
let n = screen.gauges.len() as u32;
Layout::horizontal(vec![Constraint::Ratio(1, n); n as usize]).split(area).to_vec()
}
};
for (gauge, cell) in screen.gauges.iter().zip(cells) {
match gauge {
Gauge::Cpu => cpu::render(f, cell, app),
Gauge::Temp => temp::render(f, cell, app),
Gauge::Power => power::render(f, cell, app),
Gauge::Memory => memory::render(f, cell, app),
}
}
}
fn render_body(f: &mut Frame, area: Rect, app: &App, screen: &Screen) {
match screen.body {
Body::ThreeCards => {
let rows = Layout::vertical([Constraint::Min(3), Constraint::Max(10)]).split(area);
let procs = beside_network(f, rows[0], app, screen);
processes::render(f, procs, app);
let cards = Layout::horizontal([Constraint::Ratio(1, 3); 3]).split(rows[1]);
ports::render(f, cards[0], app, ports::Card::Localhost);
sessions::render(f, cards[1], app);
ports::render(f, cards[2], app, ports::Card::Others);
}
Body::ProcessesOverPorts => {
const PROC_PANEL_ROWS: u16 = 10;
let left = beside_network(f, area, app, screen);
let rows = Layout::vertical([
Constraint::Max(PROC_PANEL_ROWS + 3),
Constraint::Min(4),
])
.split(left);
processes::render(f, rows[0], app);
ports::render(f, rows[1], app, ports::Card::Combined);
}
Body::ProcessesOnly => {
let procs = beside_network(f, area, app, screen);
processes::render(f, procs, app);
}
}
}
fn beside_network(f: &mut Frame, area: Rect, app: &App, screen: &Screen) -> Rect {
if !screen.network {
return area;
}
let cols =
Layout::horizontal([Constraint::Ratio(3, 5), Constraint::Ratio(2, 5)]).split(area);
network::render(f, cols[1], app);
cols[0]
}
fn render_footer(f: &mut Frame, area: Rect, app: &App) {
if app.pending_kill.is_some() {
let text = Line::from(vec![
Span::styled("y", Style::default().fg(theme::RED).bold()),
Span::styled(" confirm · ", Style::default().fg(theme::DIM)),
Span::styled("n", Style::default().fg(theme::TEXT).bold()),
Span::styled(" cancel", Style::default().fg(theme::DIM)),
]);
f.render_widget(Paragraph::new(text), area);
return;
}
if let Some(pick) = &app.pending_port_pick {
let text = Line::from(vec![
Span::styled(
format!("1-{}", pick.ports.len()),
Style::default().fg(theme::ACCENT).bold(),
),
Span::styled(" open · ", Style::default().fg(theme::DIM)),
Span::styled("n", Style::default().fg(theme::TEXT).bold()),
Span::styled(" cancel", Style::default().fg(theme::DIM)),
]);
f.render_widget(Paragraph::new(text), area);
return;
}
let show_sort = matches!(app.focus, Focus::Processes);
let show_kill = matches!(app.focus, Focus::Processes | Focus::Localhost);
let show_open = matches!(app.focus, Focus::Localhost);
let items: [Option<&str>; 6] = [
Some("↑↓ select"),
show_sort.then_some("c/m sort"),
show_open.then_some("o open"),
show_kill.then_some("k kill"),
Some("tab focus"),
Some("q quit"),
];
let text = items.into_iter().flatten().collect::<Vec<_>>().join(" · ");
f.render_widget(Paragraph::new(text).style(Style::default().fg(theme::DIM)), area);
}