use ratatui::style::{Color, Modifier, Style};
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeName {
Pastel,
Rose,
Mint,
Lavender,
Peach,
Crimson,
}
impl ThemeName {
pub fn all() -> &'static [ThemeName] {
&[
ThemeName::Pastel,
ThemeName::Rose,
ThemeName::Mint,
ThemeName::Lavender,
ThemeName::Peach,
ThemeName::Crimson,
]
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
ThemeName::Pastel => "pastel",
ThemeName::Rose => "rose",
ThemeName::Mint => "mint",
ThemeName::Lavender => "lavender",
ThemeName::Peach => "peach",
ThemeName::Crimson => "crimson",
}
}
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"pastel" => Some(ThemeName::Pastel),
"rose" => Some(ThemeName::Rose),
"mint" => Some(ThemeName::Mint),
"lavender" => Some(ThemeName::Lavender),
"peach" => Some(ThemeName::Peach),
"crimson" => Some(ThemeName::Crimson),
_ => None,
}
}
pub fn next(&self) -> Self {
let all = Self::all();
let pos = all.iter().position(|t| t == self).unwrap_or(0);
let next_idx = (pos + 1) % all.len();
all[next_idx]
}
}
#[derive(Debug, Clone)]
pub struct Theme {
pub name: ThemeName,
pub background: Color,
pub foreground: Color,
pub accent: Color,
pub success: Color,
pub error: Color,
pub warning: Color,
pub muted: Color,
pub border: Color,
pub highlight: Color,
pub cursor: Color,
pub text_untyped: Color,
pub text_correct: Color,
pub text_incorrect: Color,
pub newline: Color,
pub newline_typed: Color,
}
impl Theme {
#[must_use]
pub fn from_name(name: ThemeName) -> Self {
match name {
ThemeName::Pastel => Self::pastel(),
ThemeName::Rose => Self::rose(),
ThemeName::Mint => Self::mint(),
ThemeName::Lavender => Self::lavender(),
ThemeName::Peach => Self::peach(),
ThemeName::Crimson => Self::crimson(),
}
}
#[must_use]
pub fn pastel() -> Self {
Self {
name: ThemeName::Pastel,
background: Color::Rgb(30, 30, 35),
foreground: Color::Rgb(230, 230, 235),
accent: Color::Rgb(150, 200, 255),
success: Color::Rgb(150, 240, 180),
error: Color::Rgb(255, 150, 150),
warning: Color::Rgb(255, 220, 150),
muted: Color::Rgb(180, 180, 190),
border: Color::Rgb(60, 60, 70),
highlight: Color::Rgb(200, 180, 255),
cursor: Color::Rgb(255, 230, 150),
text_untyped: Color::Rgb(140, 140, 150),
text_correct: Color::Rgb(230, 230, 235),
text_incorrect: Color::Rgb(255, 150, 150),
newline: Color::Rgb(100, 100, 110),
newline_typed: Color::Rgb(160, 160, 170),
}
}
#[must_use]
pub fn rose() -> Self {
Self {
name: ThemeName::Rose,
background: Color::Rgb(30, 30, 35),
foreground: Color::Rgb(230, 230, 235),
accent: Color::Rgb(255, 160, 180),
success: Color::Rgb(150, 240, 180),
error: Color::Rgb(255, 150, 150),
warning: Color::Rgb(255, 220, 150),
muted: Color::Rgb(180, 180, 190),
border: Color::Rgb(60, 60, 70),
highlight: Color::Rgb(255, 180, 200),
cursor: Color::Rgb(255, 230, 150),
text_untyped: Color::Rgb(140, 140, 150),
text_correct: Color::Rgb(230, 230, 235),
text_incorrect: Color::Rgb(255, 150, 150),
newline: Color::Rgb(100, 100, 110),
newline_typed: Color::Rgb(160, 160, 170),
}
}
#[must_use]
pub fn mint() -> Self {
Self {
name: ThemeName::Mint,
background: Color::Rgb(30, 30, 35),
foreground: Color::Rgb(230, 230, 235),
accent: Color::Rgb(140, 230, 180),
success: Color::Rgb(150, 240, 180),
error: Color::Rgb(255, 150, 150),
warning: Color::Rgb(255, 220, 150),
muted: Color::Rgb(180, 180, 190),
border: Color::Rgb(60, 60, 70),
highlight: Color::Rgb(160, 240, 200),
cursor: Color::Rgb(255, 230, 150),
text_untyped: Color::Rgb(140, 140, 150),
text_correct: Color::Rgb(230, 230, 235),
text_incorrect: Color::Rgb(255, 150, 150),
newline: Color::Rgb(100, 100, 110),
newline_typed: Color::Rgb(160, 160, 170),
}
}
#[must_use]
pub fn lavender() -> Self {
Self {
name: ThemeName::Lavender,
background: Color::Rgb(30, 30, 35),
foreground: Color::Rgb(230, 230, 235),
accent: Color::Rgb(190, 160, 255),
success: Color::Rgb(150, 240, 180),
error: Color::Rgb(255, 150, 150),
warning: Color::Rgb(255, 220, 150),
muted: Color::Rgb(180, 180, 190),
border: Color::Rgb(60, 60, 70),
highlight: Color::Rgb(210, 180, 255),
cursor: Color::Rgb(255, 230, 150),
text_untyped: Color::Rgb(140, 140, 150),
text_correct: Color::Rgb(230, 230, 235),
text_incorrect: Color::Rgb(255, 150, 150),
newline: Color::Rgb(100, 100, 110),
newline_typed: Color::Rgb(160, 160, 170),
}
}
#[must_use]
pub fn peach() -> Self {
Self {
name: ThemeName::Peach,
background: Color::Rgb(30, 30, 35),
foreground: Color::Rgb(230, 230, 235),
accent: Color::Rgb(255, 190, 140),
success: Color::Rgb(150, 240, 180),
error: Color::Rgb(255, 150, 150),
warning: Color::Rgb(255, 220, 150),
muted: Color::Rgb(180, 180, 190),
border: Color::Rgb(60, 60, 70),
highlight: Color::Rgb(255, 200, 160),
cursor: Color::Rgb(255, 230, 150),
text_untyped: Color::Rgb(140, 140, 150),
text_correct: Color::Rgb(230, 230, 235),
text_incorrect: Color::Rgb(255, 150, 150),
newline: Color::Rgb(100, 100, 110),
newline_typed: Color::Rgb(160, 160, 170),
}
}
#[must_use]
pub fn crimson() -> Self {
Self {
name: ThemeName::Crimson,
background: Color::Rgb(30, 30, 35),
foreground: Color::Rgb(230, 230, 235),
accent: Color::Rgb(240, 100, 120),
success: Color::Rgb(150, 240, 180),
error: Color::Rgb(255, 150, 150),
warning: Color::Rgb(255, 220, 150),
muted: Color::Rgb(180, 180, 190),
border: Color::Rgb(60, 60, 70),
highlight: Color::Rgb(255, 120, 140),
cursor: Color::Rgb(255, 230, 150),
text_untyped: Color::Rgb(140, 140, 150),
text_correct: Color::Rgb(230, 230, 235),
text_incorrect: Color::Rgb(255, 150, 150),
newline: Color::Rgb(100, 100, 110),
newline_typed: Color::Rgb(160, 160, 170),
}
}
#[must_use]
pub fn style(&self, fg: Color) -> Style {
Style::default().fg(fg)
}
#[must_use]
pub fn style_accent(&self) -> Style {
self.style(self.accent).add_modifier(Modifier::BOLD)
}
#[must_use]
pub fn style_success(&self) -> Style {
self.style(self.success)
}
#[must_use]
pub fn style_error(&self) -> Style {
self.style(self.error)
}
#[must_use]
pub fn style_muted(&self) -> Style {
self.style(self.muted)
}
#[must_use]
pub fn style_highlight(&self) -> Style {
self.style(self.highlight).add_modifier(Modifier::BOLD)
}
#[must_use]
pub fn dim_color(&self, color: Color) -> Color {
match color {
Color::Rgb(r, g, b) => {
let dim = |c: u8| {
let faded = (u16::from(c) * 3) / 4; let min_brightness = 80u16; faded.max(min_brightness) as u8
};
Color::Rgb(dim(r), dim(g), dim(b))
}
_ => color, }
}
#[must_use]
pub fn heatmap_color(&self, level: u8) -> Color {
if level == 0 {
self.border
} else {
let intensity = match level {
1 => 0.25,
2 => 0.50,
3 => 0.75,
_ => 1.00,
};
match (self.background, self.accent) {
(Color::Rgb(br, bg, bb), Color::Rgb(ar, ag, ab)) => {
let blend = |base: u8, accent: u8| {
let blended =
f64::from(base) + (f64::from(accent) - f64::from(base)) * intensity;
blended.clamp(0.0, 255.0) as u8
};
Color::Rgb(blend(br, ar), blend(bg, ag), blend(bb, ab))
}
_ => self.accent,
}
}
}
}
impl Default for Theme {
fn default() -> Self {
Self::pastel()
}
}