dot-ai 0.6.1

A minimal AI agent that lives in your terminal
Documentation
use ratatui::style::{Color, Modifier, Style};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TerminalBackground {
    Dark,
    Light,
}

pub fn detect_terminal_background() -> TerminalBackground {
    if let Ok(val) = std::env::var("TERM_BACKGROUND") {
        match val.to_lowercase().as_str() {
            "light" => return TerminalBackground::Light,
            "dark" => return TerminalBackground::Dark,
            _ => {}
        }
    }

    if let Ok(val) = std::env::var("COLORFGBG")
        && let Some(bg_str) = val.rsplit(';').next()
        && let Ok(bg) = bg_str.trim().parse::<u8>()
    {
        return if bg < 8 || (232..232 + 12).contains(&bg) {
            TerminalBackground::Dark
        } else if (8..=15).contains(&bg) {
            TerminalBackground::Light
        } else {
            TerminalBackground::Dark
        };
    }

    if let Ok(term_program) = std::env::var("TERM_PROGRAM")
        && term_program.to_lowercase().contains("apple_terminal")
    {
        return TerminalBackground::Light;
    }

    TerminalBackground::Dark
}

#[derive(Debug, Clone, Copy)]
pub struct SyntaxStyles {
    pub keyword: Style,
    pub string: Style,
    pub comment: Style,
    pub function: Style,
    pub type_name: Style,
    pub number: Style,
    pub constant: Style,
    pub attribute: Style,
}

pub struct Theme {
    pub bg: Color,
    pub fg: Color,
    pub dim: Style,
    pub accent: Color,
    pub user_label: Style,
    pub assistant_label: Style,
    pub border: Style,
    pub input_prompt: Style,
    pub status_bar: Style,
    pub code_bg: Color,
    pub inline_code: Style,
    pub error: Style,
    pub tool_name: Style,
    pub tool_output: Style,
    pub heading: Style,
    pub bold: Style,
    pub italic: Style,
    pub blockquote: Style,
    pub link: Style,
    pub list_bullet: Style,
    pub scrollbar_track: Style,
    pub scrollbar_thumb: Style,
    pub tool_success: Style,
    pub highlight: Style,
    pub muted_fg: Color,
    pub tool_file_read: Style,
    pub tool_file_write: Style,
    pub tool_directory: Style,
    pub tool_search: Style,
    pub tool_command: Style,
    pub tool_mcp: Style,
    pub tool_skill: Style,
    pub tool_badge_bg: Color,
    pub tool_path: Style,
    pub thinking: Style,
    pub mode_normal_fg: Color,
    pub mode_normal_bg: Color,
    pub mode_insert_fg: Color,
    pub mode_insert_bg: Color,
    pub cost: Style,
    pub user_text: Style,
    pub tool_action: Style,
    pub separator: Style,
    pub tool_exit_ok: Style,
    pub tool_exit_err: Style,
    pub syntax: Option<SyntaxStyles>,
    pub syntect_theme: Option<&'static str>,
    pub diff_add: Style,
    pub diff_remove: Style,
    pub diff_hunk: Style,
    pub input_bg: Color,
    pub input_fg: Color,
    pub input_dim_fg: Color,
    pub progress_bar_filled: Style,
    pub progress_bar_empty: Style,
    pub streaming_dot: Style,
    pub user_text_bg: Color,
}

impl Theme {
    pub fn from_config(name: &str) -> Self {
        match name {
            "light" => Self::light(),
            "terminal" => Self::terminal(),
            "auto" => match detect_terminal_background() {
                TerminalBackground::Light => Self::light(),
                TerminalBackground::Dark => Self::dark(),
            },
            _ => Self::dark(),
        }
    }

    pub fn dark() -> Self {
        let muted = Color::Rgb(88, 91, 112);
        let surface = Color::Rgb(42, 44, 60);
        let accent = Color::Rgb(110, 150, 215);
        let green = Color::Rgb(140, 190, 135);
        let peach = Color::Rgb(210, 155, 115);
        let red = Color::Rgb(200, 120, 145);
        let mauve = Color::Rgb(170, 140, 210);
        let yellow = Color::Rgb(210, 190, 150);
        let teal = Color::Rgb(120, 185, 175);
        let sapphire = Color::Rgb(95, 165, 200);
        let base = Color::Rgb(30, 30, 46);

        Self {
            bg: Color::Reset,
            fg: Color::White,
            dim: Style::default().fg(muted),
            accent,
            muted_fg: muted,
            user_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
            assistant_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
            border: Style::default().fg(surface),
            input_prompt: Style::default().fg(accent),
            status_bar: Style::default().fg(muted),
            code_bg: surface,
            inline_code: Style::default().fg(peach),
            error: Style::default().fg(red),
            tool_name: Style::default().fg(yellow).add_modifier(Modifier::BOLD),
            tool_output: Style::default().fg(muted),
            tool_success: Style::default().fg(green),
            heading: Style::default().fg(accent).add_modifier(Modifier::BOLD),
            bold: Style::default().add_modifier(Modifier::BOLD),
            italic: Style::default().add_modifier(Modifier::ITALIC),
            blockquote: Style::default().fg(muted),
            link: Style::default()
                .fg(accent)
                .add_modifier(Modifier::UNDERLINED),
            list_bullet: Style::default().fg(muted),
            scrollbar_track: Style::default().fg(surface),
            scrollbar_thumb: Style::default().fg(muted),
            highlight: Style::default().fg(base).bg(accent),
            tool_file_read: Style::default().fg(sapphire),
            tool_file_write: Style::default().fg(peach),
            tool_directory: Style::default().fg(accent),
            tool_search: Style::default().fg(mauve),
            tool_command: Style::default().fg(green),
            tool_mcp: Style::default().fg(teal),
            tool_skill: Style::default().fg(mauve),
            tool_badge_bg: surface,
            tool_path: Style::default()
                .fg(Color::White)
                .add_modifier(Modifier::UNDERLINED),
            thinking: Style::default().fg(muted),
            mode_normal_fg: base,
            mode_normal_bg: muted,
            mode_insert_fg: base,
            mode_insert_bg: accent,
            cost: Style::default().fg(Color::Rgb(165, 135, 80)),
            user_text: Style::default().fg(Color::Rgb(205, 214, 244)),
            tool_action: Style::default().fg(muted),
            separator: Style::default().fg(Color::Rgb(52, 54, 72)),
            tool_exit_ok: Style::default().fg(green),
            tool_exit_err: Style::default().fg(red),
            syntax: None,
            syntect_theme: Some("base16-ocean.dark"),
            diff_add: Style::default().fg(green),
            diff_remove: Style::default().fg(red),
            diff_hunk: Style::default().fg(accent),
            input_bg: Color::Rgb(36, 38, 55),
            input_fg: Color::White,
            input_dim_fg: muted,
            progress_bar_filled: Style::default().fg(accent).add_modifier(Modifier::BOLD),
            progress_bar_empty: Style::default().fg(surface),
            streaming_dot: Style::default().fg(accent),
            user_text_bg: Color::Rgb(38, 40, 58),
        }
    }

    pub fn light() -> Self {
        let muted = Color::Rgb(140, 143, 161);
        let surface = Color::Rgb(204, 208, 218);
        let accent = Color::Rgb(35, 90, 210);
        let green = Color::Rgb(55, 135, 40);
        let peach = Color::Rgb(210, 90, 20);
        let red = Color::Rgb(175, 30, 60);
        let mauve = Color::Rgb(110, 55, 190);
        let yellow = Color::Rgb(185, 120, 30);
        let teal = Color::Rgb(25, 125, 130);
        let sapphire = Color::Rgb(30, 130, 155);
        let text = Color::Rgb(76, 79, 105);

        Self {
            bg: Color::Reset,
            fg: text,
            dim: Style::default().fg(muted),
            accent,
            muted_fg: muted,
            user_label: Style::default().fg(mauve).add_modifier(Modifier::BOLD),
            assistant_label: Style::default().fg(accent).add_modifier(Modifier::BOLD),
            border: Style::default().fg(surface),
            input_prompt: Style::default().fg(accent),
            status_bar: Style::default().fg(muted),
            code_bg: surface,
            inline_code: Style::default().fg(peach),
            error: Style::default().fg(red),
            tool_name: Style::default().fg(yellow).add_modifier(Modifier::BOLD),
            tool_output: Style::default().fg(muted),
            tool_success: Style::default().fg(green),
            heading: Style::default().fg(accent).add_modifier(Modifier::BOLD),
            bold: Style::default().add_modifier(Modifier::BOLD),
            italic: Style::default().add_modifier(Modifier::ITALIC),
            blockquote: Style::default().fg(muted),
            link: Style::default()
                .fg(accent)
                .add_modifier(Modifier::UNDERLINED),
            list_bullet: Style::default().fg(muted),
            scrollbar_track: Style::default().fg(surface),
            scrollbar_thumb: Style::default().fg(muted),
            highlight: Style::default().fg(Color::White).bg(accent),
            tool_file_read: Style::default().fg(sapphire),
            tool_file_write: Style::default().fg(peach),
            tool_directory: Style::default().fg(accent),
            tool_search: Style::default().fg(mauve),
            tool_command: Style::default().fg(green),
            tool_mcp: Style::default().fg(teal),
            tool_skill: Style::default().fg(mauve),
            tool_badge_bg: surface,
            tool_path: Style::default().fg(text).add_modifier(Modifier::UNDERLINED),
            thinking: Style::default().fg(muted),
            mode_normal_fg: Color::White,
            mode_normal_bg: muted,
            mode_insert_fg: Color::White,
            mode_insert_bg: accent,
            cost: Style::default().fg(Color::Rgb(150, 110, 35)),
            user_text: Style::default().fg(text),
            tool_action: Style::default().fg(muted),
            separator: Style::default().fg(surface),
            tool_exit_ok: Style::default().fg(green),
            tool_exit_err: Style::default().fg(red),
            syntax: None,
            syntect_theme: Some("base16-ocean.light"),
            diff_add: Style::default().fg(green),
            diff_remove: Style::default().fg(red),
            diff_hunk: Style::default().fg(accent),
            input_bg: Color::Rgb(210, 214, 225),
            input_fg: text,
            input_dim_fg: muted,
            progress_bar_filled: Style::default().fg(accent).add_modifier(Modifier::BOLD),
            progress_bar_empty: Style::default().fg(surface),
            streaming_dot: Style::default().fg(accent),
            user_text_bg: Color::Rgb(218, 222, 232),
        }
    }

    pub fn terminal() -> Self {
        let dim = Style::default().add_modifier(Modifier::DIM);
        let bold = Style::default().add_modifier(Modifier::BOLD);
        let muted = Color::Indexed(8);

        Self {
            bg: Color::Reset,
            fg: Color::Reset,
            dim,
            accent: Color::Reset,
            muted_fg: muted,
            user_label: bold,
            assistant_label: Style::default(),
            border: dim,
            input_prompt: bold,
            status_bar: dim,
            code_bg: Color::Indexed(0),
            inline_code: Style::default().fg(muted),
            error: Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED),
            tool_name: bold,
            tool_output: dim,
            tool_success: bold,
            heading: Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
            bold,
            italic: Style::default().add_modifier(Modifier::ITALIC),
            blockquote: dim,
            link: Style::default().add_modifier(Modifier::UNDERLINED),
            list_bullet: dim,
            scrollbar_track: dim,
            scrollbar_thumb: Style::default(),
            highlight: Style::default().add_modifier(Modifier::REVERSED),
            tool_file_read: Style::default().fg(muted),
            tool_file_write: Style::default().fg(muted),
            tool_directory: bold,
            tool_search: Style::default().fg(muted),
            tool_command: Style::default().fg(muted),
            tool_mcp: Style::default().fg(muted),
            tool_skill: Style::default().fg(muted),
            tool_badge_bg: muted,
            tool_path: Style::default().add_modifier(Modifier::UNDERLINED),
            thinking: dim,
            mode_normal_fg: Color::Reset,
            mode_normal_bg: muted,
            mode_insert_fg: Color::Indexed(0),
            mode_insert_bg: Color::Reset,
            cost: dim,
            user_text: bold,
            tool_action: dim,
            separator: dim,
            tool_exit_ok: bold,
            tool_exit_err: Style::default().add_modifier(Modifier::BOLD),
            syntax: Some(SyntaxStyles {
                keyword: bold,
                string: Style::default().fg(muted),
                comment: dim.add_modifier(Modifier::ITALIC),
                function: bold,
                type_name: Style::default().fg(muted),
                number: Style::default().fg(muted),
                constant: bold,
                attribute: Style::default().fg(muted),
            }),
            syntect_theme: None,
            diff_add: bold,
            diff_remove: dim,
            diff_hunk: Style::default().fg(muted),
            input_bg: muted,
            input_fg: Color::Reset,
            input_dim_fg: muted,
            progress_bar_filled: bold,
            progress_bar_empty: dim,
            streaming_dot: dim,
            user_text_bg: Color::Indexed(0),
        }
    }
}

impl Default for Theme {
    fn default() -> Self {
        Self::terminal()
    }
}