malvin 0.2.5

Non-interactive research and coding agent
use std::sync::atomic::{AtomicU8, Ordering};

pub(crate) const ANSI_BOLD: &str = "\x1b[1m";
pub(crate) const ANSI_DIM: &str = "\x1b[90m";
pub(crate) const ANSI_RESET: &str = "\x1b[0m";

#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TerminalTheme {
    #[default]
    Dark,
    Light,
}

/// Semantic terminal colors. Field names describe role, not hue.
#[derive(Clone, Copy)]
pub(crate) struct Palette {
    /// Errors / failure marks (✗).
    pub(crate) error: &'static str,
    /// Warnings.
    pub(crate) warning: &'static str,
    /// Who-tag chrome and heartbeat.
    pub(crate) who_tag: &'static str,
    /// Secondary accents (paths, · separators, ✓).
    pub(crate) accent: &'static str,
    /// Tool names/verbs and agent-start `provider:model` text.
    pub(crate) tool_name: &'static str,
    /// Primary body text (agent messages).
    pub(crate) body: &'static str,
}

const DARK_PALETTE: Palette = Palette {
    error: "\x1b[38;2;224;122;95m",
    warning: "\x1b[38;2;245;158;66m",
    who_tag: "\x1b[38;2;110;113;142m",
    accent: "\x1b[38;2;129;178;154m",
    tool_name: "\x1b[38;2;158;128;78m",
    body: "\x1b[38;2;235;235;235m",
};

const LIGHT_PALETTE: Palette = Palette {
    error: "\x1b[38;2;179;78;61m",
    warning: "\x1b[38;2;196;98;44m",
    who_tag: "\x1b[38;2;55;57;72m",
    accent: "\x1b[38;2;77;118;98m",
    tool_name: "\x1b[38;2;48;48;50m",
    body: "\x1b[38;2;24;24;26m",
};

const THEME_DARK: u8 = 0;
const THEME_LIGHT: u8 = 1;

static ACTIVE_THEME: AtomicU8 = AtomicU8::new(THEME_DARK);

pub fn init_terminal_theme(theme: TerminalTheme) {
    let id = match theme {
        TerminalTheme::Dark => THEME_DARK,
        TerminalTheme::Light => THEME_LIGHT,
    };
    ACTIVE_THEME.store(id, Ordering::Relaxed);
}

pub(crate) fn active_palette() -> Palette {
    match ACTIVE_THEME.load(Ordering::Relaxed) {
        THEME_LIGHT => LIGHT_PALETTE,
        _ => DARK_PALETTE,
    }
}

pub(crate) fn ansi_error() -> &'static str {
    active_palette().error
}

pub(crate) fn ansi_warning() -> &'static str {
    active_palette().warning
}

pub(crate) fn ansi_who_tag() -> &'static str {
    active_palette().who_tag
}

pub(crate) fn ansi_accent() -> &'static str {
    active_palette().accent
}

pub(crate) fn ansi_tool_name() -> &'static str {
    active_palette().tool_name
}

pub(crate) fn ansi_body() -> &'static str {
    active_palette().body
}

#[cfg(test)]
#[path = "terminal_palette_tests.rs"]
mod terminal_palette_tests;