supercode-cli 0.5.72

Volter Harness — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
//! Tiny, zero-dependency terminal styling for supercode's CLI.
//!
//! supercode ships as a single small static binary, so instead of pulling in a
//! color/table/spinner stack we hand-roll the handful of ANSI helpers the CLI
//! needs. Everything degrades cleanly: when output isn't a terminal, or
//! `NO_COLOR` is set, the paint functions return the plain string and the box
//! helpers still draw with Unicode rules (no escape codes).
//!
//! The palette is the Volter brand's roles in its dark scheme (a terminal is a
//! dark ground): the action accent, two terminal inks (teal, blue), the
//! healthy/attention/danger status tones, and two text grays for metadata.
//! The values are generated from the brand's tokens into `brand_palette.rs`
//! by `crates/cli/scripts/brand-palette.mjs`; nothing is fetched at build or
//! run time.
//!
//! Not every terminal understands 24-bit SGR (`38;2;r;g;b`). We detect the
//! terminal's color depth (UX-17: `COLORTERM=truecolor`/`24bit` → 24-bit,
//! otherwise a 256-color-safe terminal → the nearest xterm-256 cube color,
//! `NO_COLOR`/dumb/non-tty → no escapes at all) and route every paint helper
//! through it, so output degrades gracefully instead of emitting raw 24-bit
//! codes a 256-color terminal can't render correctly.

use std::io::IsTerminal;
use std::sync::OnceLock;

// ---- palette (24-bit truecolor) -------------------------------------------

/// SGR foreground prefix for an RGB color.
const fn fg(r: u8, g: u8, b: u8) -> Rgb {
    Rgb(r, g, b)
}

#[derive(Clone, Copy)]
pub struct Rgb(u8, u8, u8);

include!("brand_palette.rs");

const fn role(c: (u8, u8, u8)) -> Rgb {
    fg(c.0, c.1, c.2)
}

/// The brand accent (`action.default`).
pub const ACCENT: Rgb = role(BRAND_ACCENT);
/// Teal, for a second emphasis tier (`terminal.cyan`).
pub const TEAL: Rgb = role(BRAND_TEAL);
/// Success (`status.healthy.base`).
pub const OK: Rgb = role(BRAND_OK);
/// Warning (`status.attention.base`).
pub const WARN: Rgb = role(BRAND_WARN);
/// Error (`status.danger.base`).
pub const ERR: Rgb = role(BRAND_ERR);
/// Blue, used for the "user" role and links (`terminal.blue`).
pub const SKY: Rgb = role(BRAND_SKY);
/// Muted gray for secondary text (`text.muted`).
pub const MUTED: Rgb = role(BRAND_MUTED);
/// Dim gray for tertiary / chrome (`text.tertiary`).
pub const DIM: Rgb = role(BRAND_DIM);

// ---- color capability detection --------------------------------------------

/// How many colors the target terminal can render.
///
/// Ordered coarse→fine only in the sense that `None` disables all SGR;
/// `Ansi256` and `TrueColor` are two different *encodings* of the same
/// palette, chosen by [`color_level`].
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum ColorLevel {
    /// No SGR at all: `NO_COLOR`, `TERM=dumb`, or output isn't a terminal
    /// (and `CLICOLOR_FORCE` didn't override that).
    None,
    /// 256-color-safe: emit `38;5;N` using the nearest xterm-256 cube color.
    /// This is the safe default for any terminal we can't positively
    /// identify as truecolor-capable.
    Ansi256,
    /// 24-bit-safe: emit `38;2;r;g;b` directly. Only used when the terminal
    /// advertises it via `COLORTERM=truecolor`/`24bit`.
    TrueColor,
}

/// Pure decision function (no I/O), so it's unit-testable without needing a
/// real tty or mutating process env vars.
fn detect_color_level(
    no_color: bool,
    clicolor_force: bool,
    term: Option<&str>,
    colorterm: Option<&str>,
    is_tty: bool,
) -> ColorLevel {
    if no_color {
        return ColorLevel::None;
    }
    if term == Some("dumb") {
        return ColorLevel::None;
    }
    if !clicolor_force && !is_tty {
        return ColorLevel::None;
    }
    match colorterm {
        Some("truecolor") | Some("24bit") => ColorLevel::TrueColor,
        // Everything else (unset, "ansi256", or an unrecognized value) gets
        // the 256-color-safe encoding — this is the graceful-degradation
        // default (UX-17): never assume 24-bit support that wasn't declared.
        _ => ColorLevel::Ansi256,
    }
}

fn color_level() -> ColorLevel {
    static LEVEL: OnceLock<ColorLevel> = OnceLock::new();
    *LEVEL.get_or_init(|| {
        detect_color_level(
            std::env::var_os("NO_COLOR").is_some(),
            std::env::var_os("CLICOLOR_FORCE").is_some(),
            std::env::var("TERM").ok().as_deref(),
            std::env::var("COLORTERM").ok().as_deref(),
            // Most styled output goes to stderr; enable when it's a terminal.
            std::io::stderr().is_terminal() || std::io::stdout().is_terminal(),
        )
    })
}

fn color_enabled() -> bool {
    color_level() != ColorLevel::None
}

/// Map a 24-bit RGB triple to the nearest xterm-256 color index (16–255):
/// the 6×6×6 color cube (16–231) or the 24-step grayscale ramp (232–255),
/// whichever is closer in squared Euclidean distance.
fn rgb_to_xterm256(r: u8, g: u8, b: u8) -> u8 {
    const CUBE_STEPS: [u8; 6] = [0, 95, 135, 175, 215, 255];

    fn nearest_cube_level(v: u8) -> (i32, u8) {
        CUBE_STEPS
            .iter()
            .enumerate()
            .map(|(i, &s)| (i as i32, s))
            .min_by_key(|&(_, s)| (s as i32 - v as i32).abs())
            .expect("CUBE_STEPS is non-empty")
    }

    fn dist2(r1: u8, g1: u8, b1: u8, r2: u8, g2: u8, b2: u8) -> i32 {
        let dr = r1 as i32 - r2 as i32;
        let dg = g1 as i32 - g2 as i32;
        let db = b1 as i32 - b2 as i32;
        dr * dr + dg * dg + db * db
    }

    let (rl, rv) = nearest_cube_level(r);
    let (gl, gv) = nearest_cube_level(g);
    let (bl, bv) = nearest_cube_level(b);
    let cube_idx = 16 + 36 * rl + 6 * gl + bl;
    let cube_dist = dist2(r, g, b, rv, gv, bv);

    // 24-step grayscale ramp: index 232..=255, value 8 + 10*step.
    let avg = (r as i32 + g as i32 + b as i32) as f64 / 3.0;
    let gray_step = (((avg - 8.0) / 10.0).round()).clamp(0.0, 23.0) as i32;
    let gray_val = (8 + gray_step * 10) as u8;
    let gray_idx = 232 + gray_step;
    let gray_dist = dist2(r, g, b, gray_val, gray_val, gray_val);

    (if gray_dist < cube_dist {
        gray_idx
    } else {
        cube_idx
    }) as u8
}

/// The SGR foreground fragment for `c` at the detected color level (no
/// leading `\x1b[` / trailing `m` — callers assemble the full sequence so
/// `bold`/`pill` can prefix `1;` or add a paired background).
fn fg_sgr(c: Rgb, level: ColorLevel) -> String {
    match level {
        ColorLevel::TrueColor => format!("38;2;{};{};{}", c.0, c.1, c.2),
        ColorLevel::Ansi256 | ColorLevel::None => {
            format!("38;5;{}", rgb_to_xterm256(c.0, c.1, c.2))
        }
    }
}

fn bg_sgr(c: Rgb, level: ColorLevel) -> String {
    match level {
        ColorLevel::TrueColor => format!("48;2;{};{};{}", c.0, c.1, c.2),
        ColorLevel::Ansi256 | ColorLevel::None => {
            format!("48;5;{}", rgb_to_xterm256(c.0, c.1, c.2))
        }
    }
}

// ---- core paint helpers ----------------------------------------------------

/// Paint `s` with a foreground color (no-op when color is disabled).
pub fn paint(c: Rgb, s: impl AsRef<str>) -> String {
    let s = s.as_ref();
    let level = color_level();
    if level != ColorLevel::None {
        format!("\x1b[{}m{s}\x1b[0m", fg_sgr(c, level))
    } else {
        s.to_string()
    }
}

/// Paint `s` bold + colored.
pub fn bold(c: Rgb, s: impl AsRef<str>) -> String {
    let s = s.as_ref();
    let level = color_level();
    if level != ColorLevel::None {
        format!("\x1b[1;{}m{s}\x1b[0m", fg_sgr(c, level))
    } else {
        s.to_string()
    }
}

/// Plain bold (no color), for headings on minimal palettes.
pub fn bold_plain(s: impl AsRef<str>) -> String {
    let s = s.as_ref();
    if color_enabled() {
        format!("\x1b[1m{s}\x1b[0m")
    } else {
        s.to_string()
    }
}

// ---- semantic glyphs -------------------------------------------------------

/// A high-contrast pill: colored background, the brand's ink for text on a
/// strong fill (`text.onStrong`). Use sparingly for the one thing that must
/// pop (a missing key, a failure).
pub fn pill(bg: Rgb, s: impl AsRef<str>) -> String {
    let s = s.as_ref();
    let level = color_level();
    if level != ColorLevel::None {
        format!(
            "\x1b[1;{};{}m {s} \x1b[0m",
            bg_sgr(bg, level),
            fg_sgr(role(BRAND_ON_STRONG), level)
        )
    } else {
        format!("[{s}]")
    }
}

/// A green ✓ / red ✗ status mark.
pub fn mark(ok: bool) -> String {
    if ok {
        paint(OK, "✓")
    } else {
        paint(ERR, "✗")
    }
}

/// The supercode logo mark.
pub fn logo_mark() -> String {
    bold(ACCENT, "◆")
}

// ---- panels & rules --------------------------------------------------------

const RULE_WIDTH: usize = 60;

/// A titled top rule: `╭─ title ─────────────╮`, accent title, dim chrome.
pub fn panel_top(title: &str) -> String {
    // A clean "header + underline rule" — an accent bar, a bold title, and a
    // full-width dim rule beneath. (No corner hooks: half-drawn boxes read as
    // rendering glitches; a straight rule reads as an intentional divider.)
    format!(
        "{} {}\n{}",
        paint(ACCENT, "▌"),
        bold_plain(title),
        paint(DIM, "─".repeat(RULE_WIDTH)),
    )
}

/// The closing rule that brackets a panel's content.
pub fn panel_bottom() -> String {
    paint(DIM, "─".repeat(RULE_WIDTH))
}

/// A section heading: a small accent bar + bold label.
pub fn section(title: &str) -> String {
    format!("{} {}", paint(ACCENT, "▌"), bold_plain(title))
}

/// An aligned key/value row used inside panels.
pub fn kv(key: &str, val: impl AsRef<str>) -> String {
    format!("  {}  {}", paint(MUTED, format!("{key:<12}")), val.as_ref())
}

/// A key/value row prefixed by a dim info glyph (neutral, not a status).
pub fn kv_info(key: &str, val: impl AsRef<str>) -> String {
    format!(
        "  {}  {}  {}",
        paint(DIM, "◦"),
        paint(MUTED, format!("{key:<12}")),
        val.as_ref()
    )
}

/// A key/value row with a blank icon slot, so its label aligns with rows that
/// do have a status/info glyph (keeps the text column's vertical rhythm).
pub fn kv_blank(key: &str, val: impl AsRef<str>) -> String {
    format!(
        "     {}  {}",
        paint(MUTED, format!("{key:<12}")),
        val.as_ref()
    )
}

/// A key/value row prefixed by a status mark.
pub fn kv_status(ok: bool, key: &str, val: impl AsRef<str>) -> String {
    format!(
        "  {}  {}  {}",
        mark(ok),
        paint(MUTED, format!("{key:<12}")),
        val.as_ref()
    )
}