use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{BorderType, Borders, Padding};
use crate::palette;
use crate::palette::PaletteMode;
use crate::tui::history::ToolStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Variant {
Dark,
Light,
Grayscale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Theme {
pub variant: Variant,
pub section_borders: Borders,
pub section_border_type: BorderType,
pub section_border_color: Color,
pub section_bg: Color,
pub section_title_color: Color,
pub section_padding: Padding,
pub tool_title_color: Color,
pub tool_value_color: Color,
pub tool_label_color: Color,
pub tool_running_accent: Color,
pub tool_success_accent: Color,
pub tool_failed_accent: Color,
}
impl Theme {
#[must_use]
pub const fn dark() -> Self {
Self {
variant: Variant::Dark,
section_borders: Borders::ALL,
section_border_type: BorderType::Plain,
section_border_color: palette::BORDER_COLOR,
section_bg: palette::WHALE_BG,
section_title_color: palette::WHALE_ACTION,
section_padding: Padding::horizontal(1),
tool_title_color: palette::TEXT_SOFT,
tool_value_color: palette::TEXT_MUTED,
tool_label_color: palette::TEXT_DIM,
tool_running_accent: palette::ACCENT_TOOL_LIVE,
tool_success_accent: palette::STATUS_SUCCESS,
tool_failed_accent: palette::STATUS_ERROR,
}
}
#[must_use]
pub const fn light() -> Self {
Self {
variant: Variant::Light,
section_borders: Borders::ALL,
section_border_type: BorderType::Plain,
section_border_color: palette::LIGHT_BORDER,
section_bg: palette::LIGHT_PANEL,
section_title_color: palette::LIGHT_ACTION,
section_padding: Padding::horizontal(1),
tool_title_color: palette::LIGHT_TEXT_SOFT,
tool_value_color: palette::LIGHT_TEXT_MUTED,
tool_label_color: palette::LIGHT_TEXT_HINT,
tool_running_accent: palette::LIGHT_LIVE,
tool_success_accent: palette::LIGHT_SUCCESS_FG,
tool_failed_accent: palette::LIGHT_DANGER,
}
}
#[must_use]
pub const fn solarized_light() -> Self {
Self {
variant: Variant::Light,
section_borders: Borders::ALL,
section_border_type: BorderType::Plain,
section_border_color: palette::SOLARIZED_BORDER,
section_bg: palette::SOLARIZED_PANEL,
section_title_color: palette::SOLARIZED_BLUE,
section_padding: Padding::horizontal(1),
tool_title_color: palette::SOLARIZED_TEXT_SOFT,
tool_value_color: palette::SOLARIZED_TEXT_MUTED,
tool_label_color: palette::SOLARIZED_TEXT_DIM,
tool_running_accent: palette::SOLARIZED_BLUE,
tool_success_accent: palette::SOLARIZED_CYAN,
tool_failed_accent: palette::SOLARIZED_RED,
}
}
#[must_use]
pub const fn grayscale() -> Self {
Self {
variant: Variant::Grayscale,
section_borders: Borders::ALL,
section_border_type: BorderType::Plain,
section_border_color: palette::GRAYSCALE_BORDER,
section_bg: palette::GRAYSCALE_PANEL,
section_title_color: palette::GRAYSCALE_TEXT_SOFT,
section_padding: Padding::horizontal(1),
tool_title_color: palette::GRAYSCALE_TEXT_SOFT,
tool_value_color: palette::GRAYSCALE_TEXT_MUTED,
tool_label_color: palette::GRAYSCALE_TEXT_HINT,
tool_running_accent: palette::GRAYSCALE_TEXT_SOFT,
tool_success_accent: palette::GRAYSCALE_TEXT_HINT,
tool_failed_accent: palette::GRAYSCALE_TEXT_BODY,
}
}
#[must_use]
pub const fn for_palette_mode(mode: PaletteMode) -> Self {
match mode {
PaletteMode::Dark => Self::dark(),
PaletteMode::Light => Self::light(),
PaletteMode::Grayscale => Self::grayscale(),
PaletteMode::SolarizedLight => Self::solarized_light(),
}
}
#[must_use]
pub const fn tool_status_color(self, status: ToolStatus) -> Color {
match status {
ToolStatus::Running => self.tool_running_accent,
ToolStatus::Success => self.tool_success_accent,
ToolStatus::Hydrated => self.tool_running_accent,
ToolStatus::Failed => self.tool_failed_accent,
}
}
#[must_use]
pub fn tool_title_style(self) -> Style {
Style::default()
.fg(self.tool_title_color)
.add_modifier(Modifier::BOLD)
}
#[must_use]
pub fn tool_status_style(self, status: ToolStatus) -> Style {
Style::default().fg(self.tool_status_color(status))
}
#[must_use]
pub fn tool_label_style(self) -> Style {
Style::default().fg(self.tool_label_color)
}
#[must_use]
pub fn tool_value_style(self) -> Style {
Style::default().fg(self.tool_value_color)
}
}
#[must_use]
pub const fn active_theme() -> Theme {
Theme::dark()
}
#[cfg(test)]
mod tests {
use super::{Theme, Variant, active_theme};
use crate::palette;
use crate::tui::history::ToolStatus;
#[test]
fn active_theme_returns_dark() {
assert_eq!(active_theme(), Theme::dark());
}
#[test]
fn dark_theme_uses_codewhale_semantic_roles() {
let theme = Theme::dark();
assert_eq!(theme.variant, Variant::Dark);
assert_eq!(theme.section_border_color, palette::BORDER_COLOR);
assert_eq!(theme.section_bg, palette::WHALE_BG);
assert_eq!(theme.section_title_color, palette::WHALE_ACTION);
assert_eq!(theme.tool_title_color, palette::TEXT_SOFT);
assert_eq!(theme.tool_value_color, palette::TEXT_MUTED);
assert_eq!(theme.tool_label_color, palette::TEXT_DIM);
assert_eq!(theme.tool_running_accent, palette::ACCENT_TOOL_LIVE);
assert_eq!(theme.tool_success_accent, palette::STATUS_SUCCESS);
assert_eq!(theme.tool_failed_accent, palette::STATUS_ERROR);
}
#[test]
fn light_theme_uses_light_panel_tokens() {
let theme = Theme::for_palette_mode(crate::palette::PaletteMode::Light);
assert_eq!(theme.variant, Variant::Light);
assert_eq!(theme.section_bg, palette::LIGHT_PANEL);
assert_eq!(theme.section_border_color, palette::LIGHT_BORDER);
assert_eq!(theme.tool_title_color, palette::LIGHT_TEXT_SOFT);
assert_eq!(theme.tool_value_color, palette::LIGHT_TEXT_MUTED);
assert_eq!(theme.section_title_color, palette::LIGHT_ACTION);
assert_eq!(theme.tool_running_accent, palette::LIGHT_LIVE);
assert_eq!(theme.tool_success_accent, palette::LIGHT_SUCCESS_FG);
}
#[test]
fn grayscale_theme_uses_neutral_tokens() {
let theme = Theme::for_palette_mode(crate::palette::PaletteMode::Grayscale);
assert_eq!(theme.variant, Variant::Grayscale);
assert_eq!(theme.section_bg, palette::GRAYSCALE_PANEL);
assert_eq!(theme.section_border_color, palette::GRAYSCALE_BORDER);
assert_eq!(theme.tool_running_accent, palette::GRAYSCALE_TEXT_SOFT);
assert_eq!(theme.tool_failed_accent, palette::GRAYSCALE_TEXT_BODY);
}
#[test]
fn tool_status_color_maps_each_status() {
let theme = Theme::dark();
assert_eq!(
theme.tool_status_color(ToolStatus::Running),
theme.tool_running_accent
);
assert_eq!(
theme.tool_status_color(ToolStatus::Success),
theme.tool_success_accent
);
assert_eq!(
theme.tool_status_color(ToolStatus::Hydrated),
theme.tool_running_accent
);
assert_eq!(
theme.tool_status_color(ToolStatus::Failed),
theme.tool_failed_accent
);
}
}