oy-cli 0.11.7

OpenCode launcher and deterministic MCP helpers for repository audit and review workflows
Documentation
//! Minimal terminal output helpers for the launcher.

use std::fmt::Display;
use std::io::IsTerminal as _;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicU8, Ordering};

mod text;

pub use text::truncate_chars;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
    Quiet = 0,
    Normal = 1,
    Verbose = 2,
    Json = 3,
}

static OUTPUT_MODE: AtomicU8 = AtomicU8::new(OutputMode::Normal as u8);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ColorMode {
    Auto,
    Always,
    Never,
}

static COLOR_MODE: LazyLock<ColorMode> = LazyLock::new(color_mode_from_env);

pub fn init_output_mode(mode: Option<OutputMode>) {
    let mode = mode
        .or_else(output_mode_from_env)
        .unwrap_or(OutputMode::Normal);
    set_output_mode(mode);
}

pub fn set_output_mode(mode: OutputMode) {
    OUTPUT_MODE.store(mode as u8, Ordering::Relaxed);
}

pub fn is_json() -> bool {
    matches!(output_mode(), OutputMode::Json)
}

fn output_mode() -> OutputMode {
    match OUTPUT_MODE.load(Ordering::Relaxed) {
        0 => OutputMode::Quiet,
        2 => OutputMode::Verbose,
        3 => OutputMode::Json,
        _ => OutputMode::Normal,
    }
}

fn output_mode_from_env() -> Option<OutputMode> {
    if truthy_env("OY_QUIET") {
        return Some(OutputMode::Quiet);
    }
    if truthy_env("OY_VERBOSE") {
        return Some(OutputMode::Verbose);
    }
    match std::env::var("OY_OUTPUT")
        .ok()?
        .to_ascii_lowercase()
        .as_str()
    {
        "quiet" => Some(OutputMode::Quiet),
        "verbose" => Some(OutputMode::Verbose),
        "json" => Some(OutputMode::Json),
        "normal" => Some(OutputMode::Normal),
        _ => None,
    }
}

fn truthy_env(name: &str) -> bool {
    matches!(
        std::env::var(name).ok().as_deref(),
        Some("1" | "true" | "yes" | "on")
    )
}

fn color_mode_from_env() -> ColorMode {
    if std::env::var_os("NO_COLOR").is_some() {
        return ColorMode::Never;
    }
    match std::env::var("OY_COLOR")
        .ok()
        .map(|value| value.to_ascii_lowercase())
        .as_deref()
    {
        Some("always" | "1" | "true" | "yes" | "on") => ColorMode::Always,
        Some("never" | "0" | "false" | "no" | "off") => ColorMode::Never,
        _ => ColorMode::Auto,
    }
}

fn color_enabled() -> bool {
    match *COLOR_MODE {
        ColorMode::Always => true,
        ColorMode::Never => false,
        ColorMode::Auto => std::io::stdout().is_terminal(),
    }
}

pub fn paint(code: &str, text: impl Display) -> String {
    let text = text.to_string();
    if color_enabled() {
        format!("\x1b[{code}m{text}\x1b[0m")
    } else {
        text
    }
}

pub fn faint(text: impl Display) -> String {
    paint("2", text)
}

pub fn green(text: impl Display) -> String {
    paint("32", text)
}

pub fn red(text: impl Display) -> String {
    paint("31", text)
}

pub fn status_text(ok: bool, text: impl Display) -> String {
    if ok { green(text) } else { red(text) }
}

pub fn out(text: &str) {
    print!("{text}");
}

pub fn err(text: &str) {
    eprint!("{text}");
}

pub fn line(text: impl Display) {
    out(&format!("{text}\n"));
}

pub fn err_line(text: impl Display) {
    err(&format!("{text}\n"));
}

pub fn section(title: &str) {
    line(paint("1", title));
}

pub fn kv(key: &str, value: impl Display) {
    line(format_args!(
        "  {} {value}",
        faint(format_args!("{key:<11}"))
    ));
}

pub fn success(text: impl Display) {
    line(format_args!("{} {text}", green("")));
}