bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Terminal theme and rendering capability selection.

use std::env;
use std::io::{self, IsTerminal};

use crossterm::terminal;

use crate::ui::terminal_supports_cursor;

pub(crate) const RESET: &str = "\x1b[0m";
pub(crate) const WARM_WHITE_BOLD: &str = "\x1b[1;38;2;255;244;226m";
pub(crate) const MUTED: &str = "\x1b[38;2;178;160;142m";
pub(crate) const ACCENT: &str = "\x1b[38;2;255;165;0m";
pub(crate) const ACCENT_BOLD: &str = "\x1b[1;38;2;255;165;0m";
pub(crate) const INFO_BLUE: &str = "\x1b[1;38;2;96;165;250m";
pub(crate) const SUCCESS_GREEN: &str = "\x1b[1;38;2;74;222;128m";
pub(crate) const WARNING_AMBER: &str = "\x1b[1;38;2;251;191;36m";
pub(crate) const WARNING_AMBER_PLAIN: &str = "\x1b[38;2;251;191;36m";
pub(crate) const ERROR_RED: &str = "\x1b[1;38;2;248;113;113m";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// ANSI styles used by human-readable terminal rendering.
pub(crate) struct Theme {
    pub(crate) reset: &'static str,
    pub(crate) title: &'static str,
    pub(crate) accent_bold: &'static str,
    pub(crate) muted: &'static str,
    pub(crate) accent: &'static str,
    pub(crate) info: &'static str,
    pub(crate) success: &'static str,
    pub(crate) warning: &'static str,
    pub(crate) warning_plain: &'static str,
    pub(crate) error: &'static str,
}

impl Default for Theme {
    fn default() -> Self {
        Self {
            reset: RESET,
            title: WARM_WHITE_BOLD,
            accent_bold: ACCENT_BOLD,
            muted: MUTED,
            accent: ACCENT,
            info: INFO_BLUE,
            success: SUCCESS_GREEN,
            warning: WARNING_AMBER,
            warning_plain: WARNING_AMBER_PLAIN,
            error: ERROR_RED,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Color, width, cursor, and theme settings for one render operation.
pub(crate) struct RenderOptions {
    pub(crate) color: bool,
    pub(crate) width: usize,
    pub(crate) cursor: bool,
    pub(crate) theme: Theme,
}

impl Default for RenderOptions {
    fn default() -> Self {
        Self::stdout()
    }
}

impl RenderOptions {
    /// Return deterministic plain rendering options without color or cursor control.
    #[cfg(test)]
    pub(crate) fn no_color() -> Self {
        Self {
            color: false,
            cursor: false,
            ..Self::default()
        }
    }

    /// Detect rendering options for standard output.
    pub(crate) fn stdout() -> Self {
        let tty = io::stdout().is_terminal();
        Self {
            color: color_enabled(tty),
            width: terminal_width(tty),
            cursor: tty && terminal_supports_cursor(),
            theme: Theme::default(),
        }
    }

    /// Detect rendering options for standard error.
    pub(crate) fn stderr() -> Self {
        let tty = io::stderr().is_terminal();
        Self {
            color: color_enabled(tty),
            width: terminal_width(tty),
            cursor: tty && terminal_supports_cursor(),
            theme: Theme::default(),
        }
    }
}

pub(crate) fn color_enabled(is_terminal: bool) -> bool {
    is_terminal && env::var_os("NO_COLOR").is_none() && terminal_supports_cursor()
}

pub(crate) fn terminal_width(is_terminal: bool) -> usize {
    if is_terminal {
        terminal::size()
            .ok()
            .map_or(100, |(width, _)| usize::from(width))
    } else {
        100
    }
    .max(20)
}