agtop 2.3.6

Terminal UI for monitoring AI coding agents (Claude Code, Codex, Aider, Cursor, Gemini, Goose, ...) — like top, but for agents.
// Refined pastel + greyscale palette: still colorful enough to read state at
// a glance, but soft enough to feel professional rather than neon. Every
// color is RGB so it lands the same on any modern terminal.

use crate::model::Status;
use ratatui::style::{Color, Modifier, Style};

// ── Structural colors ──────────────────────────────────────────────────────
pub const BORDER:     Color = Color::Rgb(125, 150, 165);   // soft slate-cyan
pub const BORDER_DIM: Color = Color::Rgb( 70,  85,  95);   // gentle dim
pub const FG:         Color = Color::Rgb(225, 222, 215);   // warm off-white
pub const FG_DIM:     Color = Color::Rgb(165, 170, 178);   // neutral mid-gray (>4.8:1 contrast)
pub const HL_BG:      Color = Color::Rgb( 50,  85, 120);   // bright slate-cyan for selection (high contrast)

/// Alternating bg tints for adjacent project groups (also used for
/// the projects + activity panels' striping).  Same project / row
/// pair → same tint within a single render so the eye reads "this
/// belongs with that" without the rainbow distraction of per-name
/// hashing.
///
/// Tint A is `Color::Reset` — i.e. no override, just the panel /
/// terminal background.  This means the *first* row in any panel
/// matches the surrounding chrome exactly, and a sparsely-populated
/// panel (1–3 items) doesn't get visible bands of color sitting on
/// top of the panel block.  Tint B is a barely-perceptible warmer
/// nudge — visible as alternation when the panel is full, invisible
/// when it isn't.
pub const GROUP_TINT_A: Color = Color::Reset;
pub const GROUP_TINT_B: Color = Color::Rgb(20, 22, 26);

/// Pick the alternating tint for the group at index `i` (0-based).
pub fn group_tint(i: usize) -> Color {
    if i % 2 == 0 { GROUP_TINT_A } else { GROUP_TINT_B }
}

// ── Status accents (soft pastel, still distinguishable) ────────────────────
// BUSY is held to a deeper sage and ACTV to a warmer yellow-sage so they
// don't collapse for deuteranopia / protanopia (audit MED).
pub const C_BUSY:   Color = Color::Rgb(120, 215, 150);     // sage green (deeper)
pub const C_SPAWN:  Color = Color::Rgb(140, 215, 185);     // teal-sage (shifted warmer for tritanopia)
pub const C_ACTIVE: Color = Color::Rgb(195, 210, 130);     // yellow-sage (distinct from BUSY)
// Brighter than the original 155-grey so the `idle` label stays
// legible in the status-distribution bar and the agent rows on
// terminals with low-contrast or backlit-light themes.  Still
// distinct from FG (225) so an idle row doesn't visually compete
// with active rows.
pub const C_IDLE:   Color = Color::Rgb(195, 195, 195);     // soft gray, readable on any bg
pub const C_WAIT:   Color = Color::Rgb(225, 175, 110);     // amber (shifted to break tie with cpu_color tier-2)
pub const C_DONE:   Color = Color::Rgb(200, 175, 215);     // soft lavender
pub const C_STALE:  Color = Color::Rgb(110, 105, 108);     // dim gray

// ── Chart colors ───────────────────────────────────────────────────────────
pub const C_CHART_CPU: Color = Color::Rgb(225, 195, 140);  // warm peach
pub const C_CHART_MEM: Color = Color::Rgb(200, 175, 215);  // soft lavender
pub const C_CHART_TOK: Color = Color::Rgb(180, 200, 215);  // pastel sky for token streams

// ── System memory gauge segments ───────────────────────────────────────────
pub const C_GAUGE_USED:  Color = Color::Rgb(160, 200, 150);  // sage
pub const C_GAUGE_AGENT: Color = Color::Rgb(225, 195, 140);  // peach
pub const C_GAUGE_FREE:  Color = Color::Rgb( 60,  65,  75);  // slate

pub fn status_color(s: Status) -> Color {
    match s {
        Status::Busy      => C_BUSY,
        Status::Spawning  => C_SPAWN,
        Status::Active    => C_ACTIVE,
        Status::Idle      => C_IDLE,
        Status::Waiting   => C_WAIT,
        Status::Completed => C_DONE,
        Status::Stale     => C_STALE,
    }
}

pub fn status_style(s: Status) -> Style {
    let st = Style::default().fg(status_color(s));
    if matches!(s, Status::Busy | Status::Spawning) {
        st.add_modifier(Modifier::BOLD)
    } else if matches!(s, Status::Stale) {
        // Stale agents legitimately recede.  Idle does NOT — the brighter
        // C_IDLE colour already conveys "quiet" without needing the DIM
        // modifier to stack on top, which made the label invisible on
        // some terminals.
        st.add_modifier(Modifier::DIM)
    } else {
        st
    }
}

// Per-agent-type accent so rows for the same agent visually cluster.
// Same pastel discipline as the rest of the palette.
pub fn agent_color(label: &str) -> Color {
    match label {
        "claude" | "claude-code"   => Color::Rgb(160, 180, 215),  // soft blue
        "codex"  | "openai-codex"  => Color::Rgb(160, 200, 150),  // sage
        "aider"                    => Color::Rgb(220, 160, 155),  // dusty rose
        "cursor-agent"             => Color::Rgb(200, 170, 210),  // lavender
        "gemini"                   => Color::Rgb(165, 215, 210),  // teal
        "goose" | "block-goose"    => Color::Rgb(225, 195, 140),  // peach
        "continue"                 => Color::Rgb(225, 222, 215),  // off-white
        "opencode"                 => Color::Rgb(200, 170, 210),  // lavender
        "copilot"                  => Color::Rgb(165, 215, 210),  // teal
        "cody"                     => Color::Rgb(200, 170, 210),  // lavender
        "amp"                      => Color::Rgb(225, 195, 140),  // peach
        "crush"                    => Color::Rgb(220, 160, 155),  // dusty rose
        "mods"                     => Color::Rgb(160, 200, 150),  // sage
        "sgpt"                     => Color::Rgb(160, 180, 215),  // soft blue
        "llm"                      => Color::Rgb(165, 215, 210),  // teal
        "ollama"                   => Color::Rgb(225, 195, 140),  // peach
        "fabric"                   => Color::Rgb(225, 222, 215),  // off-white
        _ => {
            // Hash-based stable color from the soft palette.
            let palette = [
                Color::Rgb(165, 215, 210), Color::Rgb(200, 170, 210), Color::Rgb(225, 195, 140),
                Color::Rgb(220, 160, 155), Color::Rgb(160, 200, 150), Color::Rgb(160, 180, 215),
            ];
            let mut h: u32 = 0;
            for b in label.bytes() { h = h.wrapping_mul(31).wrapping_add(b as u32); }
            palette[(h as usize) % palette.len()]
        }
    }
}

pub fn cpu_color(v: f64) -> Color {
    if !v.is_finite() || v < 0.0 { return FG_DIM; }   // /proc race / NaN guard
    if v >= 50.0      { Color::Rgb(220, 160, 155) }   // dusty rose
    else if v >= 10.0 { Color::Rgb(235, 180, 110) }   // amber (audit fix: separate from C_WAIT)
    else if v >=  1.0 { Color::Rgb(160, 200, 150) }   // sage
    else              { FG_DIM }
}