syswatch 0.1.0

Single-host, read-only system diagnostics TUI. Twelve tabs covering CPU, memory, disks, processes, GPU, power, services, network, plus a Timeline scrubber and an Insights anomaly engine. Sibling to netwatch.
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::Paragraph,
    Frame,
};

use crate::app::{App, Snapshot};
use crate::ui::{
    palette as p,
    widgets::{human_bytes, human_rate, panel, sparkline},
};

pub fn draw(f: &mut Frame, area: Rect, app: &App, snap: &Snapshot) {
    let v = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(8), Constraint::Min(0)])
        .split(area);

    draw_aggregate(f, v[0], app, snap);
    draw_iface_table(f, v[1], snap);
}

fn draw_aggregate(f: &mut Frame, area: Rect, app: &App, snap: &Snapshot) {
    let block = panel("Aggregate throughput (rx + tx)");
    let inner = block.inner(area);
    f.render_widget(block, area);

    let series: Vec<f32> = app
        .history
        .net_rate
        .to_vec()
        .iter()
        .map(|v| *v as f32)
        .collect();
    let peak = series.iter().cloned().fold(1.0f32, f32::max);
    let normalized: Vec<f32> = series.iter().map(|v| (v / peak).min(1.0)).collect();

    let cols = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
        .split(inner);

    let spark_lines: Vec<Line> = (0..cols[0].height)
        .map(|_| sparkline(&normalized, p::CYAN))
        .collect();
    f.render_widget(
        Paragraph::new(spark_lines).style(Style::default().bg(p::BG)),
        cols[0],
    );

    let total: f64 = snap.net.iter().map(|i| i.rx_rate + i.tx_rate).sum();
    let rx: f64 = snap.net.iter().map(|i| i.rx_rate).sum();
    let tx: f64 = snap.net.iter().map(|i| i.tx_rate).sum();
    let counters = vec![
        Line::from(vec![
            Span::styled("now   ", Style::default().fg(p::DIM)),
            Span::styled(
                human_rate(total),
                Style::default().fg(p::CYAN).add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(vec![
            Span::styled("rx    ", Style::default().fg(p::DIM)),
            Span::styled(human_rate(rx), Style::default().fg(p::GREEN)),
        ]),
        Line::from(vec![
            Span::styled("tx    ", Style::default().fg(p::DIM)),
            Span::styled(human_rate(tx), Style::default().fg(p::CYAN)),
        ]),
        Line::from(vec![
            Span::styled("peak  ", Style::default().fg(p::DIM)),
            Span::styled(human_rate(peak as f64), Style::default().fg(p::FG)),
        ]),
        Line::from(vec![
            Span::styled("ifaces ", Style::default().fg(p::DIM)),
            Span::styled(snap.net.len().to_string(), Style::default().fg(p::FG)),
        ]),
    ];
    f.render_widget(
        Paragraph::new(counters).style(Style::default().bg(p::BG)),
        cols[1],
    );
}

fn draw_iface_table(f: &mut Frame, area: Rect, snap: &Snapshot) {
    let block = panel("Interfaces (via netwatch-sdk)");
    let inner = block.inner(area);
    f.render_widget(block, area);

    let mut lines: Vec<Line> = vec![Line::from(vec![
        Span::styled(format!("{:<14} ", "NAME"), header_style()),
        Span::styled(format!("{:<6} ", "STATE"), header_style()),
        Span::styled(format!("{:>12} ", "RX/s"), header_style()),
        Span::styled(format!("{:>12} ", "TX/s"), header_style()),
        Span::styled(format!("{:>14} ", "RX TOTAL"), header_style()),
        Span::styled(format!("{:>14}", "TX TOTAL"), header_style()),
    ])];
    let take = inner.height.saturating_sub(1) as usize;
    for iface in snap.net.iter().take(take) {
        let state_color = if iface.is_up { p::GREEN } else { p::FAINT };
        lines.push(Line::from(vec![
            Span::styled(
                format!("{:<14.14} ", iface.name),
                Style::default().fg(p::FG),
            ),
            Span::styled(
                format!("{:<6} ", if iface.is_up { "UP" } else { "DOWN" }),
                Style::default().fg(state_color),
            ),
            Span::styled(
                format!("{:>12} ", human_rate(iface.rx_rate)),
                Style::default().fg(p::GREEN),
            ),
            Span::styled(
                format!("{:>12} ", human_rate(iface.tx_rate)),
                Style::default().fg(p::CYAN),
            ),
            Span::styled(
                format!("{:>14} ", human_bytes(iface.rx_bytes)),
                Style::default().fg(p::DIM),
            ),
            Span::styled(
                format!("{:>14}", human_bytes(iface.tx_bytes)),
                Style::default().fg(p::DIM),
            ),
        ]));
    }
    f.render_widget(
        Paragraph::new(lines).style(Style::default().bg(p::BG)),
        inner,
    );
}

fn header_style() -> Style {
    Style::default().fg(p::DIM).add_modifier(Modifier::BOLD)
}