use ratatui::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BorderStyle {
#[default]
Unicode,
Ascii,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemePalette {
Default,
ColorblindSafe,
HighContrast,
}
impl std::str::FromStr for ThemePalette {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"default" => Ok(Self::Default),
"colorblind" | "colorblind-safe" | "colorblind_safe" => Ok(Self::ColorblindSafe),
"high-contrast" | "high_contrast" | "highcontrast" => Ok(Self::HighContrast),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SeverityColors {
pub critical: Color,
pub high: Color,
pub medium: Color,
pub low: Color,
}
impl Default for SeverityColors {
fn default() -> Self {
Self {
critical: Color::Red,
high: Color::LightRed,
medium: Color::Yellow,
low: Color::Blue,
}
}
}
impl SeverityColors {
#[must_use]
pub const fn no_color() -> Self {
Self {
critical: Color::Reset,
high: Color::Reset,
medium: Color::Reset,
low: Color::Reset,
}
}
}
#[derive(Debug, Clone)]
pub struct Theme {
pub border_style: BorderStyle,
pub severity_colors: SeverityColors,
pub accent_color: Color,
pub success_color: Color,
pub warning_color: Color,
pub error_color: Color,
pub muted_color: Color,
pub colors_enabled: bool,
}
impl Default for Theme {
fn default() -> Self {
Self {
border_style: BorderStyle::default(),
severity_colors: SeverityColors::default(),
accent_color: Color::Cyan,
success_color: Color::Green,
warning_color: Color::Yellow,
error_color: Color::Red,
muted_color: Color::DarkGray,
colors_enabled: true,
}
}
}
impl Theme {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn colorblind_safe() -> Self {
let error = Color::Rgb(0x00, 0x72, 0xB2);
let warning = Color::Rgb(0xE6, 0x9F, 0x00);
let success = Color::Rgb(0x00, 0x9E, 0x73);
let info = Color::Rgb(0x56, 0xB4, 0xE9);
Self {
border_style: BorderStyle::default(),
severity_colors: SeverityColors {
critical: error,
high: warning,
medium: info,
low: success,
},
accent_color: info,
success_color: success,
warning_color: warning,
error_color: error,
muted_color: Color::DarkGray,
colors_enabled: true,
}
}
#[must_use]
pub const fn high_contrast() -> Self {
let contrast = Color::White;
Self {
border_style: BorderStyle::Ascii,
severity_colors: SeverityColors {
critical: contrast,
high: contrast,
medium: contrast,
low: contrast,
},
accent_color: contrast,
success_color: contrast,
warning_color: contrast,
error_color: contrast,
muted_color: contrast,
colors_enabled: true,
}
}
#[must_use]
pub const fn no_color() -> Self {
Self {
border_style: BorderStyle::Ascii,
severity_colors: SeverityColors::no_color(),
accent_color: Color::Reset,
success_color: Color::Reset,
warning_color: Color::Reset,
error_color: Color::Reset,
muted_color: Color::Reset,
colors_enabled: false,
}
}
#[must_use]
pub fn minimal() -> Self {
Self {
border_style: BorderStyle::None,
..Self::default()
}
}
#[must_use]
pub const fn without_colors(mut self) -> Self {
self.severity_colors = SeverityColors::no_color();
self.accent_color = Color::Reset;
self.success_color = Color::Reset;
self.warning_color = Color::Reset;
self.error_color = Color::Reset;
self.muted_color = Color::Reset;
self.colors_enabled = false;
self
}
#[must_use]
pub fn from_palette(palette: ThemePalette) -> Self {
match palette {
ThemePalette::Default => Self::default(),
ThemePalette::ColorblindSafe => Self::colorblind_safe(),
ThemePalette::HighContrast => Self::high_contrast(),
}
}
#[must_use]
pub const fn color_for_severity(&self, severity: Severity) -> Color {
match severity {
Severity::Critical => self.severity_colors.critical,
Severity::High => self.severity_colors.high,
Severity::Medium => self.severity_colors.medium,
Severity::Low => self.severity_colors.low,
}
}
#[must_use]
pub const fn severity_label(&self, severity: Severity) -> &'static str {
match severity {
Severity::Critical => "CRITICAL",
Severity::High => "HIGH",
Severity::Medium => "MEDIUM",
Severity::Low => "LOW",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
impl Severity {
#[must_use]
pub fn from_str_loose(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"critical" | "crit" => Some(Self::Critical),
"high" | "hi" => Some(Self::High),
"medium" | "med" => Some(Self::Medium),
"low" | "lo" | "info" => Some(Self::Low),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_theme_has_colors() {
let theme = Theme::default();
assert!(theme.colors_enabled);
assert_eq!(theme.border_style, BorderStyle::Unicode);
}
#[test]
fn test_no_color_theme() {
let theme = Theme::no_color();
assert!(!theme.colors_enabled);
assert_eq!(theme.border_style, BorderStyle::Ascii);
assert_eq!(theme.severity_colors.critical, Color::Reset);
}
#[test]
fn test_colorblind_safe_theme() {
let theme = Theme::colorblind_safe();
assert!(theme.colors_enabled);
assert_eq!(theme.border_style, BorderStyle::Unicode);
assert_eq!(theme.error_color, Color::Rgb(0x00, 0x72, 0xB2));
}
#[test]
fn test_high_contrast_theme() {
let theme = Theme::high_contrast();
assert!(theme.colors_enabled);
assert_eq!(theme.border_style, BorderStyle::Ascii);
assert_eq!(theme.error_color, Color::White);
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Low < Severity::Medium);
assert!(Severity::Medium < Severity::High);
assert!(Severity::High < Severity::Critical);
}
#[test]
fn test_severity_from_str() {
assert_eq!(
Severity::from_str_loose("critical"),
Some(Severity::Critical)
);
assert_eq!(Severity::from_str_loose("CRIT"), Some(Severity::Critical));
assert_eq!(Severity::from_str_loose("high"), Some(Severity::High));
assert_eq!(Severity::from_str_loose("medium"), Some(Severity::Medium));
assert_eq!(Severity::from_str_loose("low"), Some(Severity::Low));
assert_eq!(Severity::from_str_loose("info"), Some(Severity::Low));
assert_eq!(Severity::from_str_loose("unknown"), None);
}
#[test]
fn test_color_for_severity() {
let theme = Theme::default();
assert_eq!(theme.color_for_severity(Severity::Critical), Color::Red);
assert_eq!(theme.color_for_severity(Severity::Low), Color::Blue);
}
}