use crate::palette::ThemePalette;
use ratatui::style::Color;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
#[non_exhaustive]
pub enum ThemeName {
#[default]
Dracula,
OneDarkPro,
Nord,
CatppuccinMocha,
CatppuccinLatte,
GruvboxDark,
GruvboxLight,
TokyoNight,
SolarizedDark,
SolarizedLight,
MonokaiPro,
RosePine,
Kanagawa,
Everforest,
Cyberpunk,
MidnightCommander,
}
impl ThemeName {
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::Dracula,
Self::OneDarkPro,
Self::Nord,
Self::CatppuccinMocha,
Self::CatppuccinLatte,
Self::GruvboxDark,
Self::GruvboxLight,
Self::TokyoNight,
Self::SolarizedDark,
Self::SolarizedLight,
Self::MonokaiPro,
Self::RosePine,
Self::Kanagawa,
Self::Everforest,
Self::Cyberpunk,
Self::MidnightCommander,
]
}
#[must_use]
pub const fn display_name(self) -> &'static str {
match self {
Self::Dracula => "Dracula",
Self::OneDarkPro => "One Dark Pro",
Self::Nord => "Nord",
Self::CatppuccinMocha => "Catppuccin Mocha",
Self::CatppuccinLatte => "Catppuccin Latte",
Self::GruvboxDark => "Gruvbox Dark",
Self::GruvboxLight => "Gruvbox Light",
Self::TokyoNight => "Tokyo Night",
Self::SolarizedDark => "Solarized Dark",
Self::SolarizedLight => "Solarized Light",
Self::MonokaiPro => "Monokai Pro",
Self::RosePine => "Rosé Pine",
Self::Kanagawa => "Kanagawa",
Self::Everforest => "Everforest",
Self::Cyberpunk => "Cyberpunk",
Self::MidnightCommander => "Midnight Commander",
}
}
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Dracula => "dracula",
Self::OneDarkPro => "one-dark-pro",
Self::Nord => "nord",
Self::CatppuccinMocha => "catppuccin-mocha",
Self::CatppuccinLatte => "catppuccin-latte",
Self::GruvboxDark => "gruvbox-dark",
Self::GruvboxLight => "gruvbox-light",
Self::TokyoNight => "tokyo-night",
Self::SolarizedDark => "solarized-dark",
Self::SolarizedLight => "solarized-light",
Self::MonokaiPro => "monokai-pro",
Self::RosePine => "rose-pine",
Self::Kanagawa => "kanagawa",
Self::Everforest => "everforest",
Self::Cyberpunk => "cyberpunk",
Self::MidnightCommander => "midnight-commander",
}
}
#[must_use]
pub fn next(self) -> Self {
let themes = Self::all();
let current = themes.iter().position(|&t| t == self).unwrap_or(0);
themes[(current + 1) % themes.len()]
}
#[must_use]
pub fn prev(self) -> Self {
let themes = Self::all();
let current = themes.iter().position(|&t| t == self).unwrap_or(0);
themes[(current + themes.len() - 1) % themes.len()]
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub const fn palette(self) -> ThemePalette {
match self {
Self::Dracula => ThemePalette {
accent: Color::Rgb(189, 147, 249), secondary: Color::Rgb(255, 121, 198), bg: Color::Rgb(40, 42, 54), fg: Color::Rgb(248, 248, 242), muted: Color::Rgb(98, 114, 164), selection: Color::Rgb(68, 71, 90), error: Color::Rgb(255, 85, 85), warning: Color::Rgb(255, 184, 108), success: Color::Rgb(80, 250, 123), info: Color::Rgb(139, 233, 253), },
Self::OneDarkPro => ThemePalette {
accent: Color::Rgb(97, 175, 239), secondary: Color::Rgb(198, 120, 221), bg: Color::Rgb(40, 44, 52), fg: Color::Rgb(171, 178, 191), muted: Color::Rgb(92, 99, 112), selection: Color::Rgb(62, 68, 81), error: Color::Rgb(224, 108, 117), warning: Color::Rgb(229, 192, 123), success: Color::Rgb(152, 195, 121), info: Color::Rgb(86, 182, 194), },
Self::Nord => ThemePalette {
accent: Color::Rgb(136, 192, 208), secondary: Color::Rgb(129, 161, 193), bg: Color::Rgb(46, 52, 64), fg: Color::Rgb(236, 239, 244), muted: Color::Rgb(76, 86, 106), selection: Color::Rgb(67, 76, 94), error: Color::Rgb(191, 97, 106), warning: Color::Rgb(235, 203, 139), success: Color::Rgb(163, 190, 140), info: Color::Rgb(94, 129, 172), },
Self::CatppuccinMocha => ThemePalette {
accent: Color::Rgb(137, 180, 250), secondary: Color::Rgb(245, 194, 231), bg: Color::Rgb(30, 30, 46), fg: Color::Rgb(205, 214, 244), muted: Color::Rgb(108, 112, 134), selection: Color::Rgb(49, 50, 68), error: Color::Rgb(243, 139, 168), warning: Color::Rgb(249, 226, 175), success: Color::Rgb(166, 227, 161), info: Color::Rgb(148, 226, 213), },
Self::CatppuccinLatte => ThemePalette {
accent: Color::Rgb(30, 102, 245), secondary: Color::Rgb(234, 118, 203), bg: Color::Rgb(239, 241, 245), fg: Color::Rgb(76, 79, 105), muted: Color::Rgb(140, 143, 161), selection: Color::Rgb(204, 208, 218), error: Color::Rgb(210, 15, 57), warning: Color::Rgb(223, 142, 29), success: Color::Rgb(64, 160, 43), info: Color::Rgb(23, 146, 153), },
Self::GruvboxDark => ThemePalette {
accent: Color::Rgb(250, 189, 47), secondary: Color::Rgb(211, 134, 155), bg: Color::Rgb(40, 40, 40), fg: Color::Rgb(235, 219, 178), muted: Color::Rgb(146, 131, 116), selection: Color::Rgb(80, 73, 69), error: Color::Rgb(251, 73, 52), warning: Color::Rgb(254, 128, 25), success: Color::Rgb(184, 187, 38), info: Color::Rgb(131, 165, 152), },
Self::GruvboxLight => ThemePalette {
accent: Color::Rgb(181, 118, 20), secondary: Color::Rgb(143, 63, 113), bg: Color::Rgb(251, 241, 199), fg: Color::Rgb(60, 56, 54), muted: Color::Rgb(146, 131, 116), selection: Color::Rgb(213, 196, 161), error: Color::Rgb(157, 0, 6), warning: Color::Rgb(175, 58, 3), success: Color::Rgb(121, 116, 14), info: Color::Rgb(66, 123, 88), },
Self::TokyoNight => ThemePalette {
accent: Color::Rgb(122, 162, 247), secondary: Color::Rgb(187, 154, 247), bg: Color::Rgb(26, 27, 38), fg: Color::Rgb(192, 202, 245), muted: Color::Rgb(86, 95, 137), selection: Color::Rgb(41, 46, 66), error: Color::Rgb(247, 118, 142), warning: Color::Rgb(224, 175, 104), success: Color::Rgb(158, 206, 106), info: Color::Rgb(125, 207, 255), },
Self::SolarizedDark => ThemePalette {
accent: Color::Rgb(38, 139, 210), secondary: Color::Rgb(108, 113, 196), bg: Color::Rgb(0, 43, 54), fg: Color::Rgb(131, 148, 150), muted: Color::Rgb(88, 110, 117), selection: Color::Rgb(7, 54, 66), error: Color::Rgb(220, 50, 47), warning: Color::Rgb(181, 137, 0), success: Color::Rgb(133, 153, 0), info: Color::Rgb(42, 161, 152), },
Self::SolarizedLight => ThemePalette {
accent: Color::Rgb(38, 139, 210), secondary: Color::Rgb(108, 113, 196), bg: Color::Rgb(253, 246, 227), fg: Color::Rgb(101, 123, 131), muted: Color::Rgb(147, 161, 161), selection: Color::Rgb(238, 232, 213), error: Color::Rgb(220, 50, 47), warning: Color::Rgb(181, 137, 0), success: Color::Rgb(133, 153, 0), info: Color::Rgb(42, 161, 152), },
Self::MonokaiPro => ThemePalette {
accent: Color::Rgb(255, 216, 102), secondary: Color::Rgb(171, 157, 242), bg: Color::Rgb(45, 42, 46), fg: Color::Rgb(252, 252, 250), muted: Color::Rgb(114, 113, 105), selection: Color::Rgb(81, 80, 79), error: Color::Rgb(255, 97, 136), warning: Color::Rgb(252, 152, 103), success: Color::Rgb(169, 220, 118), info: Color::Rgb(120, 220, 232), },
Self::RosePine => ThemePalette {
accent: Color::Rgb(235, 188, 186), secondary: Color::Rgb(196, 167, 231), bg: Color::Rgb(25, 23, 36), fg: Color::Rgb(224, 222, 244), muted: Color::Rgb(110, 106, 134), selection: Color::Rgb(38, 35, 58), error: Color::Rgb(235, 111, 146), warning: Color::Rgb(246, 193, 119), success: Color::Rgb(156, 207, 216), info: Color::Rgb(49, 116, 143), },
Self::Kanagawa => ThemePalette {
accent: Color::Rgb(127, 180, 202), secondary: Color::Rgb(149, 127, 184), bg: Color::Rgb(31, 31, 40), fg: Color::Rgb(220, 215, 186), muted: Color::Rgb(84, 84, 109), selection: Color::Rgb(54, 54, 70), error: Color::Rgb(195, 64, 67), warning: Color::Rgb(255, 169, 107), success: Color::Rgb(118, 148, 106), info: Color::Rgb(126, 156, 216), },
Self::Everforest => ThemePalette {
accent: Color::Rgb(131, 193, 120), secondary: Color::Rgb(214, 153, 182), bg: Color::Rgb(47, 53, 55), fg: Color::Rgb(211, 198, 170), muted: Color::Rgb(133, 146, 137), selection: Color::Rgb(68, 78, 79), error: Color::Rgb(230, 126, 128), warning: Color::Rgb(219, 188, 127), success: Color::Rgb(167, 192, 128), info: Color::Rgb(124, 195, 191), },
Self::Cyberpunk => ThemePalette {
accent: Color::Rgb(0, 255, 255), secondary: Color::Rgb(255, 0, 255), bg: Color::Rgb(13, 2, 33), fg: Color::Rgb(240, 240, 240), muted: Color::Rgb(100, 100, 140), selection: Color::Rgb(40, 20, 80), error: Color::Rgb(255, 0, 60), warning: Color::Rgb(255, 230, 0), success: Color::Rgb(0, 255, 100), info: Color::Rgb(0, 180, 255), },
Self::MidnightCommander => ThemePalette {
accent: Color::Rgb(0, 170, 170), secondary: Color::Rgb(0, 255, 0), bg: Color::Rgb(0, 0, 170), fg: Color::Rgb(255, 255, 255), muted: Color::Rgb(170, 170, 170), selection: Color::Rgb(0, 170, 170), error: Color::Rgb(255, 255, 255), warning: Color::Rgb(255, 170, 0), success: Color::Rgb(0, 255, 0), info: Color::Rgb(0, 170, 170), },
}
}
}
impl std::fmt::Display for ThemeName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.display_name())
}
}
impl std::str::FromStr for ThemeName {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized: String = s
.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric())
.collect();
match normalized.as_str() {
"dracula" => Ok(Self::Dracula),
"onedarkpro" | "onedark" => Ok(Self::OneDarkPro),
"nord" => Ok(Self::Nord),
"catppuccinmocha" | "mocha" => Ok(Self::CatppuccinMocha),
"catppuccinlatte" | "latte" => Ok(Self::CatppuccinLatte),
"gruvboxdark" | "gruvbox" => Ok(Self::GruvboxDark),
"gruvboxlight" => Ok(Self::GruvboxLight),
"tokyonight" | "tokyo" => Ok(Self::TokyoNight),
"solarizeddark" | "solarized" => Ok(Self::SolarizedDark),
"solarizedlight" => Ok(Self::SolarizedLight),
"monokaipro" | "monokai" => Ok(Self::MonokaiPro),
"rosepine" | "rose" => Ok(Self::RosePine),
"kanagawa" => Ok(Self::Kanagawa),
"everforest" => Ok(Self::Everforest),
"cyberpunk" => Ok(Self::Cyberpunk),
"midnightcommander" | "midnight-commander" | "mc" => Ok(Self::MidnightCommander),
_ => Err(format!("Unknown theme: {s}")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Theme {
#[cfg_attr(feature = "serde", serde(default))]
pub name: ThemeName,
}
impl Theme {
#[must_use]
pub const fn new(name: ThemeName) -> Self {
Self { name }
}
#[must_use]
pub const fn palette(&self) -> ThemePalette {
self.name.palette()
}
#[must_use]
pub fn is_light(&self) -> bool {
self.palette().is_light()
}
#[must_use]
pub fn is_dark(&self) -> bool {
self.palette().is_dark()
}
pub fn next(&mut self) {
self.name = self.name.next();
}
pub fn prev(&mut self) {
self.name = self.name.prev();
}
}
impl From<ThemeName> for Theme {
fn from(name: ThemeName) -> Self {
Self::new(name)
}
}
impl std::fmt::Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_themes_have_palettes() {
for theme in ThemeName::all() {
let palette = theme.palette();
assert_ne!(palette.fg, palette.bg);
}
}
#[test]
fn test_theme_cycling() {
let mut theme = ThemeName::Dracula;
let original = theme;
for _ in 0..ThemeName::all().len() {
theme = theme.next();
}
assert_eq!(theme, original);
}
#[test]
fn test_theme_cycling_backward() {
let mut theme = ThemeName::Dracula;
let original = theme;
for _ in 0..ThemeName::all().len() {
theme = theme.prev();
}
assert_eq!(theme, original);
}
#[test]
fn test_light_dark_detection() {
assert!(ThemeName::CatppuccinLatte.palette().is_light());
assert!(ThemeName::GruvboxLight.palette().is_light());
assert!(ThemeName::SolarizedLight.palette().is_light());
assert!(ThemeName::Dracula.palette().is_dark());
assert!(ThemeName::Nord.palette().is_dark());
assert!(ThemeName::TokyoNight.palette().is_dark());
}
#[test]
fn test_display_name() {
assert_eq!(ThemeName::Dracula.display_name(), "Dracula");
assert_eq!(ThemeName::TokyoNight.display_name(), "Tokyo Night");
assert_eq!(
ThemeName::CatppuccinMocha.display_name(),
"Catppuccin Mocha"
);
}
#[test]
fn test_theme_display_trait() {
assert_eq!(format!("{}", ThemeName::Dracula), "Dracula");
assert_eq!(format!("{}", ThemeName::TokyoNight), "Tokyo Night");
}
#[test]
fn test_theme_wrapper() {
let mut theme = Theme::new(ThemeName::Dracula);
assert_eq!(theme.name, ThemeName::Dracula);
assert!(theme.is_dark());
theme.next();
assert_eq!(theme.name, ThemeName::OneDarkPro);
theme.prev();
assert_eq!(theme.name, ThemeName::Dracula);
}
#[test]
fn test_theme_from_name() {
let theme: Theme = ThemeName::Nord.into();
assert_eq!(theme.name, ThemeName::Nord);
}
#[test]
fn test_default_theme() {
assert_eq!(ThemeName::default(), ThemeName::Dracula);
assert_eq!(Theme::default().name, ThemeName::Dracula);
}
#[test]
fn test_theme_count() {
assert_eq!(ThemeName::all().len(), 16);
}
}