llmtop 0.1.0

Realtime TUI monitor for local LLM servers (ollama, llama.cpp). The only GPU monitor that knows what model is running and how much each token costs you in energy and dollar-equivalent.
use ratatui::{
    Frame,
    layout::{Constraint, Rect},
    widgets::{Block, Borders, Cell, Row, Table},
};

use crate::app::App;

pub fn draw(f: &mut Frame, area: Rect, app: &App) {
    let header = Row::new(vec!["MODEL", "VRAM", "TOK/S", "POWER", "J/TOK", "$/1K"]);

    let total_power = app.hardware.as_ref().map(|h| h.power_w).unwrap_or(0.0);
    let active_models: Vec<_> = app.models.iter().filter(|m| m.tokens_per_sec > 0.1).collect();
    let share = if active_models.is_empty() {
        0.0
    } else {
        total_power / active_models.len() as f64
    };

    let rows: Vec<Row> = if app.models.is_empty() {
        vec![Row::new(vec![Cell::from("(no models loaded)")])]
    } else {
        app.models
            .iter()
            .map(|m| {
                let model_power = if m.tokens_per_sec > 0.1 { share } else { 0.0 };
                let j_per_tok = if m.tokens_per_sec > 0.1 {
                    model_power / m.tokens_per_sec
                } else {
                    0.0
                };
                let usd_per_1k = crate::pricing::provider_output_per_million(&app.cli.compare)
                    .map(|p| p / 1000.0)
                    .unwrap_or(0.0);
                Row::new(vec![
                    Cell::from(m.name.clone()),
                    Cell::from(format!("{:.1}G", m.vram_mb as f64 / 1024.0)),
                    Cell::from(format!("{:>5.1}", m.tokens_per_sec)),
                    Cell::from(format!("{:>4.0}W", model_power)),
                    Cell::from(if j_per_tok > 0.0 {
                        format!("{:>5.1}", j_per_tok)
                    } else {
                        " idle".into()
                    }),
                    Cell::from(format!("{:>5.3}", usd_per_1k)),
                ])
            })
            .collect()
    };

    let title = match app.hardware.as_ref() {
        Some(h) if !h.gpu_name.is_empty() && h.vram_total_mb > 0 => format!(
            " llmtop  {}  {:.0}G ",
            h.gpu_name,
            h.vram_total_mb as f64 / 1024.0
        ),
        _ => " llmtop ".into(),
    };

    let widths = [
        Constraint::Min(20),
        Constraint::Length(10),
        Constraint::Length(7),
        Constraint::Length(8),
        Constraint::Length(7),
        Constraint::Length(7),
    ];

    let table = Table::new(rows, widths)
        .header(header)
        .block(Block::default().borders(Borders::ALL).title(title));
    f.render_widget(table, area);
}