use ratatui::{
style::{Color, Modifier, Style},
widgets::{Block, Borders},
};
#[derive(Debug, Clone, PartialEq)]
pub struct ThemeColors {
pub accent: Color,
pub positive: Color,
pub negative: Color,
pub neutral: Color,
pub dim: Color,
pub border: Color,
pub selected_bg: Color,
pub header_fg: Color,
}
impl ThemeColors {
pub fn accent_style(&self) -> Style {
Style::default().fg(self.accent)
}
pub fn positive_style(&self) -> Style {
Style::default().fg(self.positive)
}
pub fn negative_style(&self) -> Style {
Style::default().fg(self.negative)
}
pub fn dim_style(&self) -> Style {
Style::default().fg(self.dim)
}
pub fn bold_style(&self) -> Style {
Style::default().add_modifier(Modifier::BOLD)
}
pub fn selected_style(&self) -> Style {
Style::default()
.bg(self.selected_bg)
.add_modifier(Modifier::BOLD)
}
pub fn header_style(&self) -> Style {
Style::default()
.fg(self.header_fg)
.add_modifier(Modifier::BOLD)
}
pub fn border_fg_style(&self) -> Style {
Style::default().fg(self.border)
}
pub fn pnl_style(&self, value: &str) -> Style {
if value.trim().starts_with('-') {
self.negative_style()
} else {
self.positive_style()
}
}
pub fn bordered_block<'a>(&self, title: &'a str) -> Block<'a> {
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(self.border_fg_style())
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Theme {
#[default]
Default,
Dark,
HighContrast,
}
impl Theme {
pub fn cycle(&self) -> Self {
match self {
Theme::Default => Theme::Dark,
Theme::Dark => Theme::HighContrast,
Theme::HighContrast => Theme::Default,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Theme::Default => "default",
Theme::Dark => "dark",
Theme::HighContrast => "high-contrast",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Theme::Default => "Default",
Theme::Dark => "Dark",
Theme::HighContrast => "High-contrast",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"dark" => Theme::Dark,
"high-contrast" => Theme::HighContrast,
_ => Theme::Default,
}
}
pub fn colors(&self) -> ThemeColors {
match self {
Theme::Default => ThemeColors {
accent: Color::Cyan,
positive: Color::Green,
negative: Color::Red,
neutral: Color::Yellow,
dim: Color::DarkGray,
border: Color::DarkGray,
selected_bg: Color::Rgb(40, 40, 80),
header_fg: Color::White,
},
Theme::Dark => ThemeColors {
accent: Color::Rgb(0, 160, 200),
positive: Color::Rgb(0, 180, 100),
negative: Color::Rgb(190, 60, 60),
neutral: Color::Rgb(170, 150, 0),
dim: Color::Rgb(90, 90, 90),
border: Color::Rgb(70, 70, 70),
selected_bg: Color::Rgb(30, 30, 60),
header_fg: Color::Rgb(190, 190, 190),
},
Theme::HighContrast => ThemeColors {
accent: Color::Rgb(0, 255, 255),
positive: Color::Rgb(0, 255, 128),
negative: Color::Rgb(255, 80, 80),
neutral: Color::Rgb(255, 255, 0),
dim: Color::White,
border: Color::White,
selected_bg: Color::Rgb(60, 60, 120),
header_fg: Color::Rgb(255, 255, 255),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cycle_default_to_dark() {
assert_eq!(Theme::Default.cycle(), Theme::Dark);
}
#[test]
fn cycle_dark_to_high_contrast() {
assert_eq!(Theme::Dark.cycle(), Theme::HighContrast);
}
#[test]
fn cycle_high_contrast_wraps_to_default() {
assert_eq!(Theme::HighContrast.cycle(), Theme::Default);
}
#[test]
fn from_str_dark() {
assert_eq!(Theme::from_str("dark"), Theme::Dark);
}
#[test]
fn from_str_high_contrast() {
assert_eq!(Theme::from_str("high-contrast"), Theme::HighContrast);
}
#[test]
fn from_str_default_explicit() {
assert_eq!(Theme::from_str("default"), Theme::Default);
}
#[test]
fn from_str_unknown_falls_back_to_default() {
assert_eq!(Theme::from_str("neon"), Theme::Default);
}
#[test]
fn as_str_round_trips() {
for t in [Theme::Default, Theme::Dark, Theme::HighContrast] {
assert_eq!(
Theme::from_str(t.as_str()),
t,
"round-trip failed for {:?}",
t
);
}
}
#[test]
fn display_names_are_non_empty() {
for t in [Theme::Default, Theme::Dark, Theme::HighContrast] {
assert!(!t.display_name().is_empty());
}
}
#[test]
fn pnl_style_negative_for_minus_prefix() {
let c = Theme::Default.colors();
assert_eq!(c.pnl_style("-1.00"), c.negative_style());
}
#[test]
fn pnl_style_positive_for_plus_value() {
let c = Theme::Default.colors();
assert_eq!(c.pnl_style("+2.00"), c.positive_style());
}
#[test]
fn default_colors_accent_is_cyan() {
assert_eq!(Theme::Default.colors().accent, Color::Cyan);
}
#[test]
fn dark_colors_are_distinct_from_default() {
assert_ne!(Theme::Dark.colors(), Theme::Default.colors());
}
#[test]
fn high_contrast_colors_are_distinct_from_dark() {
assert_ne!(Theme::HighContrast.colors(), Theme::Dark.colors());
}
}