use ratatui::style::{Color, Modifier, Style};
use crate::terminal::{Rgb, TerminalPalette};
#[allow(dead_code)]
pub mod borders {
pub const LEFT_BORDER: &str = "▎";
}
pub mod indicators {
pub const PROMPT: &str = "❯";
pub const BULLET: &str = "●";
pub const CHECK: &str = "✓";
pub const CROSS: &str = "✗";
pub const EXPAND: &str = "▸";
pub const COLLAPSE: &str = "▾";
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Theme {
pub user_border: Style,
pub user_bg: Style,
pub assistant_border: Style,
pub assistant_bg: Style,
pub tool_border: Style,
pub tool_bg: Style,
pub user: Style,
pub assistant: Style,
pub tool: Style,
pub tool_dim: Style,
pub error: Style,
pub error_border: Style,
pub diff_add: Style,
pub diff_remove: Style,
pub diff_header: Style,
pub diff_lineno: Style,
pub status: Style,
pub status_ok: Style,
pub status_warn: Style,
pub status_error: Style,
pub status_indicator: Style,
pub accent: Style,
pub muted: Style,
pub border: Style,
pub border_focused: Style,
pub code_bg: Style,
pub code_border: Style,
pub code_header: Style,
pub prompt: Style,
pub mode_indicator: Style,
}
impl Theme {
pub fn from_name(name: &str, palette: &TerminalPalette) -> Self {
match name.to_lowercase().as_str() {
"mono" => Self::mono_with_palette(palette),
"cool" => Self::cool_with_palette(palette),
_ => Self::warm_with_palette(palette),
}
}
pub fn warm_with_palette(palette: &TerminalPalette) -> Self {
let orange = Rgb::new(217, 119, 87); let terracotta = Rgb::new(198, 120, 95); let coral = Rgb::new(230, 145, 120); let _sand = Rgb::new(245, 235, 220);
let user_bg_color = Rgb::new(45, 35, 30); let assistant_bg_color = Rgb::new(30, 32, 35); let tool_bg_color = Rgb::new(35, 40, 38); let code_bg_color = Rgb::new(35, 35, 40);
let teal = Rgb::new(94, 180, 160); let green = Rgb::new(120, 200, 140); let yellow = Rgb::new(233, 182, 89); let muted_gray = Rgb::new(140, 135, 130); let dim_gray = Rgb::new(100, 95, 90);
Self {
user_border: Style::default().fg(palette.map(orange)),
user_bg: Style::default().bg(palette.map(user_bg_color)),
assistant_border: Style::default().fg(palette.map(coral)),
assistant_bg: Style::default().bg(palette.map(assistant_bg_color)),
tool_border: Style::default().fg(palette.map(teal)),
tool_bg: Style::default().bg(palette.map(tool_bg_color)),
user: Style::default().bg(palette.map(user_bg_color)),
assistant: Style::default(),
tool: Style::default().fg(palette.map(teal)),
tool_dim: Style::default()
.fg(palette.map(teal))
.add_modifier(Modifier::DIM),
error: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
error_border: Style::default().fg(Color::Red),
diff_add: Style::default().fg(palette.map(green)),
diff_remove: Style::default().fg(Color::Red),
diff_header: Style::default()
.fg(palette.map(muted_gray))
.add_modifier(Modifier::DIM),
diff_lineno: Style::default()
.fg(palette.map(dim_gray))
.add_modifier(Modifier::DIM),
status: Style::default()
.fg(palette.map(dim_gray))
.add_modifier(Modifier::DIM),
status_ok: Style::default().fg(palette.map(green)),
status_warn: Style::default().fg(palette.map(yellow)),
status_error: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
status_indicator: Style::default().fg(palette.map(orange)),
accent: Style::default()
.fg(palette.map(orange))
.add_modifier(Modifier::BOLD),
muted: Style::default().fg(palette.map(muted_gray)),
border: Style::default().fg(palette.map(dim_gray)),
border_focused: Style::default().fg(palette.map(orange)),
code_bg: Style::default().bg(palette.map(code_bg_color)),
code_border: Style::default().fg(palette.map(dim_gray)),
code_header: Style::default()
.fg(palette.map(muted_gray))
.add_modifier(Modifier::DIM),
prompt: Style::default()
.fg(palette.map(orange))
.add_modifier(Modifier::BOLD),
mode_indicator: Style::default().fg(palette.map(terracotta)),
}
}
pub fn cool_with_palette(palette: &TerminalPalette) -> Self {
let blue = Rgb::new(100, 150, 220); let cyan = Rgb::new(80, 180, 200); let ice = Rgb::new(140, 190, 230);
let user_bg_color = Rgb::new(25, 35, 50); let assistant_bg_color = Rgb::new(30, 32, 38); let tool_bg_color = Rgb::new(30, 40, 45); let code_bg_color = Rgb::new(30, 35, 45);
let teal = Rgb::new(94, 209, 170); let green = Rgb::new(96, 200, 136); let yellow = Rgb::new(233, 182, 89); let muted_gray = Rgb::new(130, 140, 150); let dim_gray = Rgb::new(90, 100, 110);
Self {
user_border: Style::default().fg(palette.map(blue)),
user_bg: Style::default().bg(palette.map(user_bg_color)),
assistant_border: Style::default().fg(palette.map(ice)),
assistant_bg: Style::default().bg(palette.map(assistant_bg_color)),
tool_border: Style::default().fg(palette.map(teal)),
tool_bg: Style::default().bg(palette.map(tool_bg_color)),
user: Style::default().bg(palette.map(user_bg_color)),
assistant: Style::default(),
tool: Style::default().fg(palette.map(teal)),
tool_dim: Style::default()
.fg(palette.map(teal))
.add_modifier(Modifier::DIM),
error: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
error_border: Style::default().fg(Color::Red),
diff_add: Style::default().fg(palette.map(green)),
diff_remove: Style::default().fg(Color::Red),
diff_header: Style::default()
.fg(palette.map(muted_gray))
.add_modifier(Modifier::DIM),
diff_lineno: Style::default()
.fg(palette.map(dim_gray))
.add_modifier(Modifier::DIM),
status: Style::default()
.fg(palette.map(dim_gray))
.add_modifier(Modifier::DIM),
status_ok: Style::default().fg(palette.map(green)),
status_warn: Style::default().fg(palette.map(yellow)),
status_error: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
status_indicator: Style::default().fg(palette.map(cyan)),
accent: Style::default()
.fg(palette.map(blue))
.add_modifier(Modifier::BOLD),
muted: Style::default().fg(palette.map(muted_gray)),
border: Style::default().fg(palette.map(dim_gray)),
border_focused: Style::default().fg(palette.map(blue)),
code_bg: Style::default().bg(palette.map(code_bg_color)),
code_border: Style::default().fg(palette.map(dim_gray)),
code_header: Style::default()
.fg(palette.map(muted_gray))
.add_modifier(Modifier::DIM),
prompt: Style::default()
.fg(palette.map(cyan))
.add_modifier(Modifier::BOLD),
mode_indicator: Style::default().fg(palette.map(ice)),
}
}
fn mono_with_palette(palette: &TerminalPalette) -> Self {
let white = Rgb::new(220, 220, 220);
let light_gray = Rgb::new(180, 180, 180);
let mid_gray = Rgb::new(140, 140, 140);
let dark_gray = Rgb::new(80, 80, 80);
let darker_gray = Rgb::new(50, 50, 50);
let base_bg = Rgb::new(35, 35, 35);
Self {
user_border: Style::default().fg(palette.map(white)),
user_bg: Style::default().bg(palette.map(base_bg)),
assistant_border: Style::default().fg(palette.map(light_gray)),
assistant_bg: Style::default(),
tool_border: Style::default().fg(palette.map(mid_gray)),
tool_bg: Style::default().bg(palette.map(darker_gray)),
user: Style::default().bg(palette.map(base_bg)),
assistant: Style::default(),
tool: Style::default().fg(palette.map(light_gray)),
tool_dim: Style::default()
.fg(palette.map(light_gray))
.add_modifier(Modifier::DIM),
error: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
error_border: Style::default().fg(Color::Red),
diff_add: Style::default().fg(palette.map(light_gray)),
diff_remove: Style::default().fg(Color::Red),
diff_header: Style::default()
.fg(palette.map(mid_gray))
.add_modifier(Modifier::DIM),
diff_lineno: Style::default()
.fg(palette.map(dark_gray))
.add_modifier(Modifier::DIM),
status: Style::default()
.fg(palette.map(dark_gray))
.add_modifier(Modifier::DIM),
status_ok: Style::default().fg(palette.map(mid_gray)),
status_warn: Style::default().fg(Color::Yellow),
status_error: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
status_indicator: Style::default().fg(palette.map(white)),
accent: Style::default()
.fg(palette.map(white))
.add_modifier(Modifier::BOLD),
muted: Style::default().fg(palette.map(mid_gray)),
border: Style::default().fg(palette.map(dark_gray)),
border_focused: Style::default().fg(palette.map(white)),
code_bg: Style::default().bg(palette.map(darker_gray)),
code_border: Style::default().fg(palette.map(dark_gray)),
code_header: Style::default()
.fg(palette.map(mid_gray))
.add_modifier(Modifier::DIM),
prompt: Style::default()
.fg(palette.map(white))
.add_modifier(Modifier::BOLD),
mode_indicator: Style::default().fg(palette.map(light_gray)),
}
}
#[allow(dead_code)]
pub fn codex_with_palette(palette: &TerminalPalette) -> Self {
Self::warm_with_palette(palette)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal::ColorLevel;
#[test]
fn warm_theme_creates_valid_styles() {
let palette = TerminalPalette::new(ColorLevel::TrueColor);
let theme = Theme::warm_with_palette(&palette);
assert_ne!(theme.user_border, Style::default());
assert_ne!(theme.accent, Style::default());
}
#[test]
fn cool_theme_creates_valid_styles() {
let palette = TerminalPalette::new(ColorLevel::TrueColor);
let theme = Theme::cool_with_palette(&palette);
assert_ne!(theme.user_border, Style::default());
assert_ne!(theme.accent, Style::default());
}
#[test]
fn mono_theme_creates_valid_styles() {
let palette = TerminalPalette::new(ColorLevel::TrueColor);
let theme = Theme::mono_with_palette(&palette);
assert_ne!(theme.user_border, Style::default());
}
#[test]
fn from_name_selects_correct_theme() {
let palette = TerminalPalette::new(ColorLevel::TrueColor);
let warm = Theme::from_name("warm", &palette);
let cool = Theme::from_name("cool", &palette);
let mono = Theme::from_name("mono", &palette);
let default = Theme::from_name("unknown", &palette);
assert_eq!(
format!("{:?}", warm.accent),
format!("{:?}", default.accent)
);
assert_ne!(format!("{:?}", cool.accent), format!("{:?}", warm.accent));
assert_ne!(format!("{:?}", mono.accent), format!("{:?}", warm.accent));
}
#[test]
fn codex_maps_to_warm() {
let palette = TerminalPalette::new(ColorLevel::TrueColor);
let codex = Theme::codex_with_palette(&palette);
let warm = Theme::warm_with_palette(&palette);
assert_eq!(format!("{:?}", codex.accent), format!("{:?}", warm.accent));
}
}