tr300-tui 1.4.3

Real-time interactive system diagnostic TUI
Documentation
use crate::types::{HealthStatus, TempUnit};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders};

// -- Temperature Thresholds --

pub const TEMP_CPU_WARN: f64 = 70.0;
pub const TEMP_CPU_CRIT: f64 = 85.0;
pub const TEMP_GPU_WARN: f64 = 75.0;
pub const TEMP_GPU_CRIT: f64 = 90.0;

// -- Color Palette (Warm Earth) --

pub const COLOR_GOOD: Color = Color::Rgb(130, 170, 120); // Sage green
pub const COLOR_WARN: Color = Color::Rgb(210, 160, 60); // Warm amber
pub const COLOR_CRIT: Color = Color::Rgb(190, 85, 75); // Terracotta red
pub const COLOR_INFO: Color = Color::Rgb(140, 170, 200); // Slate blue
pub const COLOR_ACCENT: Color = Color::Rgb(200, 160, 100); // Warm gold
pub const COLOR_DIM: Color = Color::Rgb(110, 105, 100); // Warm gray
pub const COLOR_HEADER: Color = Color::Rgb(200, 160, 100); // Warm gold (same as accent)
pub const COLOR_TEXT: Color = Color::Rgb(210, 205, 200); // Warm white
pub const COLOR_MUTED: Color = Color::Rgb(150, 145, 140); // Medium warm gray
pub const COLOR_BORDER: Color = Color::Rgb(80, 75, 70); // Dark warm gray for borders
pub const COLOR_HIGHLIGHT_BG: Color = Color::Rgb(50, 48, 45); // Subtle dark bg for active elements

// -- Sparkline Colors --

pub const SPARK_CPU: Color = Color::Rgb(200, 160, 100); // Warm gold (accent)
pub const SPARK_MEMORY: Color = Color::Rgb(160, 120, 170); // Muted purple
pub const SPARK_NET_DOWN: Color = Color::Rgb(140, 170, 200); // Slate blue (info)
pub const SPARK_NET_UP: Color = Color::Rgb(160, 120, 170); // Muted purple
pub const SPARK_GPU: Color = Color::Rgb(130, 170, 120); // Sage green (good)
pub const SPARK_TEMP: Color = Color::Rgb(210, 160, 60); // Warm amber
pub const SPARK_SWAP: Color = Color::Rgb(210, 160, 60); // Warm amber (swap)

/// Get the appropriate sparkline bar set for the current platform.
/// Windows terminals often can't render fractional block chars (U+2581-U+2587),
/// so we use THREE_LEVELS there and NINE_LEVELS everywhere else.
pub fn sparkline_bar_set() -> ratatui::symbols::bar::Set<'static> {
    if cfg!(windows) {
        ratatui::symbols::bar::THREE_LEVELS
    } else {
        ratatui::symbols::bar::NINE_LEVELS
    }
}

/// Get the gauge empty character for the current platform.
/// U+2591 (░ LIGHT SHADE) renders as `?` on some Windows fonts,
/// so we use U+2500 (─ BOX DRAWINGS LIGHT HORIZONTAL) on Windows.
pub fn gauge_empty_char() -> &'static str {
    if cfg!(windows) {
        "\u{2500}"
    } else {
        "\u{2591}"
    }
}

pub fn status_color(status: &HealthStatus) -> Color {
    match status {
        HealthStatus::Good => COLOR_GOOD,
        HealthStatus::Warning => COLOR_WARN,
        HealthStatus::Critical => COLOR_CRIT,
        HealthStatus::Unknown => COLOR_DIM,
    }
}

// -- Block Helpers --

/// Create a content block with warm border and title
pub fn content_block(title: &str) -> Block<'static> {
    Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(COLOR_BORDER))
        .title(format!(" {} ", title))
        .title_style(
            Style::default()
                .fg(COLOR_ACCENT)
                .add_modifier(Modifier::BOLD),
        )
}

/// Create a sub-panel block (for sparklines, tables within a section)
pub fn sub_block(title: &str) -> Block<'static> {
    Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(COLOR_BORDER))
        .title(format!(" {} ", title))
        .title_style(Style::default().fg(COLOR_MUTED))
}

// -- Formatters --

/// Format bytes to human-readable string (e.g., "12.4 GB")
pub fn format_bytes(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    const TB: u64 = GB * 1024;

    if bytes >= TB {
        format!("{:.1} TB", bytes as f64 / TB as f64)
    } else if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

/// Format bytes using GiB (binary) for technician mode
pub fn format_bytes_gib(bytes: u64) -> String {
    let gib = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
    if gib >= 1024.0 {
        format!("{:.1} TiB", gib / 1024.0)
    } else if gib >= 1.0 {
        format!("{:.2} GiB", gib)
    } else {
        let mib = bytes as f64 / (1024.0 * 1024.0);
        format!("{:.1} MiB", mib)
    }
}

/// Format bytes per second as throughput
pub fn format_throughput(bytes_per_sec: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes_per_sec >= GB {
        format!("{:.1} GB/s", bytes_per_sec as f64 / GB as f64)
    } else if bytes_per_sec >= MB {
        format!("{:.1} MB/s", bytes_per_sec as f64 / MB as f64)
    } else if bytes_per_sec >= KB {
        format!("{:.1} KB/s", bytes_per_sec as f64 / KB as f64)
    } else {
        format!("{} B/s", bytes_per_sec)
    }
}

/// Format uptime seconds as "Xd Xh Xm"
pub fn format_uptime(seconds: u64) -> String {
    let days = seconds / 86400;
    let hours = (seconds % 86400) / 3600;
    let minutes = (seconds % 3600) / 60;

    if days > 0 {
        format!("{}d {}h {}m", days, hours, minutes)
    } else if hours > 0 {
        format!("{}h {}m", hours, minutes)
    } else {
        format!("{}m", minutes)
    }
}

/// Plain language for percentage (User Mode)
pub fn plain_language_percent(pct: f64, resource: &str) -> String {
    if pct < 30.0 {
        format!("Plenty of {} free", resource)
    } else if pct < 60.0 {
        format!("Moderate {} use", resource)
    } else if pct < 80.0 {
        format!("Most {} in use", resource)
    } else if pct < 95.0 {
        format!("{} getting full", resource)
    } else {
        format!("{} nearly full", resource)
    }
}

/// Plain language for temperature (User Mode)
pub fn plain_language_temp(temp_c: f64) -> &'static str {
    if temp_c < 45.0 {
        "Cool"
    } else if temp_c < 65.0 {
        "Normal"
    } else if temp_c < 80.0 {
        "Warm"
    } else if temp_c < 95.0 {
        "Hot"
    } else {
        "Very hot"
    }
}

/// Format temperature value with unit
pub fn format_temp(temp_c: f64, unit: TempUnit) -> String {
    let value = unit.convert(temp_c);
    format!("{:.0}{}", value, unit.suffix())
}

/// Plain language for CPU load (User Mode)
pub fn plain_language_cpu(pct: f32) -> &'static str {
    if pct < 25.0 {
        "Running quietly"
    } else if pct < 50.0 {
        "Running normally"
    } else if pct < 75.0 {
        "Fairly busy right now"
    } else if pct < 90.0 {
        "Very busy"
    } else {
        "Extremely busy"
    }
}

/// Plain language for network speed
pub fn plain_language_speed(bytes_per_sec: u64) -> &'static str {
    const MB: u64 = 1024 * 1024;
    if bytes_per_sec < 100 * 1024 {
        "Slow"
    } else if bytes_per_sec < MB {
        "Moderate"
    } else if bytes_per_sec < 10 * MB {
        "Fast"
    } else {
        "Very fast"
    }
}

/// Create a text gauge bar like ████████──  78%
pub fn gauge_bar(percent: f64, width: usize) -> String {
    let filled = ((percent / 100.0) * width as f64).round() as usize;
    let empty = width.saturating_sub(filled);
    format!(
        "{}{}  {:.0}%",
        "\u{2588}".repeat(filled),
        gauge_empty_char().repeat(empty),
        percent
    )
}

/// Create a colored gauge line with label
pub fn gauge_line<'a>(label: &str, percent: f64, width: usize) -> Line<'a> {
    let status = HealthStatus::from_percent(percent);
    let color = status_color(&status);

    let filled = ((percent / 100.0) * width as f64).round() as usize;
    let empty = width.saturating_sub(filled);

    Line::from(vec![
        Span::styled(format!("  {:<14}", label), Style::default().fg(COLOR_TEXT)),
        Span::styled(
            format!(
                "{}{}",
                "\u{2588}".repeat(filled),
                gauge_empty_char().repeat(empty)
            ),
            Style::default().fg(color),
        ),
        Span::styled(
            format!("  {:.0}%", percent),
            Style::default().fg(color).add_modifier(Modifier::BOLD),
        ),
    ])
}

/// Status icon + description line for User Mode
pub fn status_line<'a>(status: &HealthStatus, label: &str, description: &str) -> Line<'a> {
    let color = status_color(status);
    Line::from(vec![
        Span::styled(format!("  {} ", status.icon()), Style::default().fg(color)),
        Span::styled(format!("{:<16}", label), Style::default().fg(COLOR_TEXT)),
        Span::styled(description.to_string(), Style::default().fg(COLOR_DIM)),
    ])
}

/// Section header line
pub fn section_header<'a>(title: &str) -> Line<'a> {
    Line::from(Span::styled(
        format!("  {}", title),
        Style::default()
            .fg(COLOR_HEADER)
            .add_modifier(Modifier::BOLD),
    ))
}

/// Separator line
pub fn separator(width: usize) -> Line<'static> {
    Line::from(Span::styled(
        format!("  {}", "\u{2500}".repeat(width.saturating_sub(4))),
        Style::default().fg(COLOR_DIM),
    ))
}

/// Truncate a string to `max` characters, appending ".." if truncated
pub fn truncate_str(s: &str, max: usize) -> String {
    if max < 3 {
        return s.chars().take(max).collect();
    }
    if s.chars().count() <= max {
        s.to_string()
    } else {
        let truncated: String = s.chars().take(max - 2).collect();
        format!("{}..", truncated)
    }
}

/// Health gauge line with status icon + label + description + gauge bar (User Mode overview)
pub fn health_gauge_line<'a>(
    label: &str,
    status: &HealthStatus,
    description: &str,
    percent: f64,
    bar_width: usize,
) -> Line<'a> {
    let color = status_color(status);
    let filled = ((percent / 100.0) * bar_width as f64).round() as usize;
    let empty = bar_width.saturating_sub(filled);

    let desc_truncated = truncate_str(description, 26);
    Line::from(vec![
        Span::styled(format!("  {} ", status.icon()), Style::default().fg(color)),
        Span::styled(format!("{:<16}", label), Style::default().fg(COLOR_TEXT)),
        Span::styled(
            format!("{:<28}", desc_truncated),
            Style::default().fg(COLOR_DIM),
        ),
        Span::styled(
            format!(
                "{}{}  {:.0}%",
                "\u{2588}".repeat(filled),
                gauge_empty_char().repeat(empty),
                percent
            ),
            Style::default().fg(color),
        ),
    ])
}

/// Simple gauge line with label + gauge + plain language description (User Mode CPU)
pub fn health_gauge_line_simple<'a>(label: &str, percent: f64, bar_width: usize) -> Line<'a> {
    let status = HealthStatus::from_percent(percent);
    let color = status_color(&status);
    let filled = ((percent / 100.0) * bar_width as f64).round() as usize;
    let empty = bar_width.saturating_sub(filled);

    Line::from(vec![
        Span::styled(format!("  {:<16}", label), Style::default().fg(COLOR_TEXT)),
        Span::styled(
            format!(
                "{}{}  {:.0}% \u{2014} {}",
                "\u{2588}".repeat(filled),
                gauge_empty_char().repeat(empty),
                percent,
                plain_language_cpu(percent as f32)
            ),
            Style::default().fg(color),
        ),
    ])
}