netspd 0.1.0

A beautiful, modern, minimalistic network speed testing terminal application
Documentation
//! The results summary shown when a test completes.

use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;

use crate::engine::models::{TestReport, TransferStats};
use crate::tui::theme::Theme;
use crate::utils::format::{format_bytes, format_millis, split_bps};

/// Renders the completion summary: download and upload headline numbers
/// side by side, with latency details and totals underneath.
pub fn render(frame: &mut Frame, area: Rect, theme: &Theme, report: &TestReport) {
    if area.height < 6 {
        return;
    }
    let colors = &theme.colors;

    let [title_area, _, speeds_area, _, latency_area, totals_area] = Layout::vertical([
        Constraint::Length(1),
        Constraint::Length(1),
        Constraint::Length(3),
        Constraint::Length(1),
        Constraint::Length(1),
        Constraint::Length(1),
    ])
    .areas(area);

    frame.render_widget(
        Paragraph::new(
            Line::from(Span::styled(
                "✓ TEST COMPLETE",
                Style::default()
                    .fg(colors.success)
                    .add_modifier(Modifier::BOLD),
            ))
            .centered(),
        ),
        title_area,
    );

    let [download_area, upload_area] =
        Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
            .areas(speeds_area);
    headline(
        frame,
        download_area,
        "↓ DOWNLOAD",
        &report.download,
        colors.download,
        colors.subtext,
    );
    headline(
        frame,
        upload_area,
        "↑ UPLOAD",
        &report.upload,
        colors.upload,
        colors.subtext,
    );

    let latency = &report.latency;
    frame.render_widget(
        Paragraph::new(
            Line::from(vec![
                Span::styled("ping ", Style::default().fg(colors.subtext)),
                Span::styled(
                    format_millis(latency.average_ms),
                    Style::default().fg(colors.latency),
                ),
                Span::styled("   jitter ", Style::default().fg(colors.subtext)),
                Span::styled(
                    format_millis(latency.jitter_ms),
                    Style::default().fg(colors.latency),
                ),
                Span::styled("   loss ", Style::default().fg(colors.subtext)),
                Span::styled(
                    format!("{:.0}%", latency.packet_loss_pct),
                    Style::default().fg(colors.latency),
                ),
            ])
            .centered(),
        ),
        latency_area,
    );

    let transferred = report.download.bytes + report.upload.bytes;
    frame.render_widget(
        Paragraph::new(
            Line::from(vec![
                Span::styled(
                    format!("{}   ", report.server_name),
                    Style::default().fg(colors.muted),
                ),
                Span::styled(
                    format!("{} transferred", format_bytes(transferred)),
                    Style::default().fg(colors.muted),
                ),
            ])
            .centered(),
        ),
        totals_area,
    );
}

/// One large speed figure with its label and peak value.
fn headline(
    frame: &mut Frame,
    area: Rect,
    label: &str,
    stats: &TransferStats,
    color: Color,
    subtext: Color,
) {
    let (value, unit) = split_bps(stats.average_bps);
    let (peak, peak_unit) = split_bps(stats.peak_bps);
    let lines = vec![
        Line::from(Span::styled(label.to_owned(), Style::default().fg(subtext))).centered(),
        Line::from(vec![
            Span::styled(
                value,
                Style::default().fg(color).add_modifier(Modifier::BOLD),
            ),
            Span::styled(format!(" {unit}"), Style::default().fg(subtext)),
        ])
        .centered(),
        Line::from(Span::styled(
            format!("peak {peak} {peak_unit}"),
            Style::default().fg(subtext),
        ))
        .centered(),
    ];
    frame.render_widget(Paragraph::new(lines), area);
}