rkik 1.1.1

Rusty Klock Inspection Kit - NTP Query and Compare Tool
Documentation
use crate::domain::ntp::ProbeResult;
use crate::stats::Stats;
use console::style;

/// Render a probe result into human readable text with the legacy style.
pub fn render_probe(r: &ProbeResult, verbose: bool) -> String {
    let ip_val = if r.target.ip.is_ipv6() {
        // [ipv6] in green
        format!("{}", style(format!("[{}]", r.target.ip)).green())
    } else {
        // ipv4/hostname in green
        format!("{}", style(r.target.ip).green())
    };
    let mut out = format!(
        "{srv_lbl} {srv_val}\n\
         {ip_lbl} {ip_val}:{port}\n\
         {utc_lbl} {utc_val}\n\
         {loc_lbl} {loc_val}\n\
         {off_lbl} {off_val:.3} ms\n\
         {rtt_lbl} {rtt_val:.3} ms",
        srv_lbl = style("Server:").cyan().bold(),
        srv_val = style(&r.target.name).green(),
        ip_lbl = style("IP:").cyan().bold(),
        ip_val = ip_val,
        port = style(r.target.port).green(),
        utc_lbl = style("UTC Time:").cyan().bold(),
        utc_val = style(r.utc.to_rfc2822()).green(),
        loc_lbl = style("Local Time:").cyan().bold(),
        loc_val = style(r.local.format("%Y-%m-%d %H:%M:%S")).green(),
        off_lbl = style("Clock Offset:").cyan().bold(),
        off_val = r.offset_ms,
        rtt_lbl = style("Round Trip Delay:").cyan().bold(),
        rtt_val = r.rtt_ms,
    );

    if verbose {
        out.push_str(&format!(
            "\n{str_lbl} {str_val}\n{ref_lbl} {ref_val}\n{str_ts}: {timestamp}",
            str_lbl = style("Stratum:").cyan().bold(),
            str_val = r.stratum,
            ref_lbl = style("Reference ID:").cyan().bold(),
            ref_val = r.ref_id,
            str_ts = style("Timestamp").cyan().bold(),
            timestamp = r.timestamp
        ));
    }

    out
}

/// Render comparison results line by line with the legacy style.
pub fn render_compare(results: &[ProbeResult], verbose: bool) -> String {
    let mut out = String::new();

    // Header
    if results.len() == 2 {
        out.push_str(&format!(
            "{} -  {}:{} and {}:{}\n",
            style("Comparing").bold(),
            style(&results[0].target.name).green(),
            style(&results[0].target.port).green(),
            style(&results[1].target.name).green(),
            style(&results[1].target.port).green()
        ));
    } else {
        out.push_str(&format!(
            "{} {} servers\n",
            style("Comparing (async):").bold(),
            results.len()
        ));
    }

    // Lines
    for r in results {
        let ip_style = if r.target.ip.is_ipv6() {
            style(r.target.ip).cyan()
        } else {
            style(r.target.ip).blue()
        };
        let ip_version = if r.target.ip.is_ipv6() { "v6" } else { "v4" };
        let offset_style = style(format!("{:.3} ms", r.offset_ms)).yellow();

        out.push_str(&format!(
            "{} [{} {}]: {}\n",
            style(&r.target.name).green().bold(),
            ip_style,
            ip_version,
            offset_style
        ));

        if verbose {
            out.push_str(&format!(
                "  {} {}\n  {} {}\n  {} {:.3} ms\n",
                style("Stratum:").cyan().bold(),
                r.stratum,
                style("Reference ID:").cyan().bold(),
                r.ref_id,
                style("Round Trip Delay:").cyan().bold(),
                r.rtt_ms
            ));
        }
    }

    // Stats
    let min = results
        .iter()
        .map(|r| r.offset_ms)
        .fold(f64::INFINITY, f64::min);
    let max = results
        .iter()
        .map(|r| r.offset_ms)
        .fold(f64::NEG_INFINITY, f64::max);
    let avg = results.iter().map(|r| r.offset_ms).sum::<f64>() / results.len() as f64;
    let diff = max - min;

    out.push_str(&format!(
        "{} {:.3} ms (min: {:.3}, max: {:.3}, avg: {:.3})\n",
        style("Max drift:").cyan().bold(),
        diff,
        min,
        max,
        avg
    ));

    out
}

/// Render a minimal line for a probe result.
pub fn render_short_probe(r: &ProbeResult) -> String {
    format!(
        "{name}:{port} {offset}",
        name = style(&r.target.name).green(),
        port = r.target.port,
        offset = style(format!("{:.3} ms", r.offset_ms)).yellow()
    )
}

/// Render a minimal line for comparison results.
pub fn render_short_compare(results: &[ProbeResult]) -> String {
    results
        .iter()
        .map(|r| {
            format!(
                "{name}:{port}:{off}",
                name = style(&r.target.name).green(),
                port = r.target.port,
                off = style(format!("{:.3}", r.offset_ms)).yellow()
            )
        })
        .collect::<Vec<_>>()
        .join(" ")
}

/// Render statistics for a set of probe results
pub fn render_stats(name: &str, stats: &Stats) -> String {
    fn fmt_ms(v: f64) -> String {
        format!("{:.3} ms", v)
    }

    format!(
        "\n{n}: {avg_lbl} {avg} ({min_lbl} {min}, {max_lbl} {max}) {rtt_lbl} {rtt} ({cnt} {rqst})",
        n = style(name).green().bold(),
        avg_lbl = style("avg").cyan().bold(),
        avg = style(fmt_ms(stats.offset_avg)).green(),
        min_lbl = style("min").cyan().bold(),
        min = style(fmt_ms(stats.offset_min)).green(),
        max_lbl = style("max").cyan().bold(),
        max = style(fmt_ms(stats.offset_max)).green(),
        rtt_lbl = style("rtt").cyan().bold(),
        rtt = style(fmt_ms(stats.rtt_avg)).green(),
        cnt = style(stats.count).green(),
        rqst = style("requests").green(),
    )
}

/// Render a probe in simple mode (offset and IP only).
pub fn render_simple_probe(r: &ProbeResult) -> String {
    format!(
        "{name}:{port} {offset}",
        name = style(&r.target.name).green(),
        port = style(&r.target.port).green(),
        offset = style(format!("{:.3} ms", r.offset_ms)).yellow()
    )
}

/// Render multiple probes in simple mode.
pub fn render_simple_compare(results: &[ProbeResult]) -> String {
    results
        .iter()
        .map(render_simple_probe)
        .collect::<Vec<_>>()
        .join("\n")
}