use crate::style::{Color, Style};
use std::{error::Error, fmt, str::FromStr};
static BUILTIN_THEMES: [BuiltinTheme; 4] = BuiltinTheme::ALL;
static THEME_ROLES: [ThemeRole; 12] = ThemeRole::ALL;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinTheme {
Dark,
Light,
Catppuccin,
TokyoNight,
}
impl BuiltinTheme {
pub const ALL: [Self; 4] = [Self::Dark, Self::Light, Self::Catppuccin, Self::TokyoNight];
pub fn name(self) -> &'static str {
match self {
Self::Dark => "dark",
Self::Light => "light",
Self::Catppuccin => "catppuccin",
Self::TokyoNight => "tokyo-night",
}
}
pub fn label(self) -> &'static str {
match self {
Self::Dark => "Dark",
Self::Light => "Light",
Self::Catppuccin => "Catppuccin",
Self::TokyoNight => "Tokyo Night",
}
}
pub fn theme(self) -> Theme {
match self {
Self::Dark => Theme::dark(),
Self::Light => Theme::light(),
Self::Catppuccin => Theme::catppuccin(),
Self::TokyoNight => Theme::tokyo_night(),
}
}
}
impl FromStr for BuiltinTheme {
type Err = ParseBuiltinThemeError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_token_name(value).as_str() {
"dark" => Ok(Self::Dark),
"light" => Ok(Self::Light),
"catppuccin" | "catppuccin-mocha" => Ok(Self::Catppuccin),
"tokyo-night" | "tokyonight" => Ok(Self::TokyoNight),
_ => Err(ParseBuiltinThemeError),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseBuiltinThemeError;
impl fmt::Display for ParseBuiltinThemeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unknown built-in theme")
}
}
impl Error for ParseBuiltinThemeError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeRole {
Primary,
Secondary,
Background,
Foreground,
Muted,
Border,
Success,
Warning,
Error,
Info,
Surface,
Highlight,
}
impl ThemeRole {
pub const ALL: [Self; 12] = [
Self::Primary,
Self::Secondary,
Self::Background,
Self::Foreground,
Self::Muted,
Self::Border,
Self::Success,
Self::Warning,
Self::Error,
Self::Info,
Self::Surface,
Self::Highlight,
];
pub fn name(self) -> &'static str {
match self {
Self::Primary => "primary",
Self::Secondary => "secondary",
Self::Background => "background",
Self::Foreground => "foreground",
Self::Muted => "muted",
Self::Border => "border",
Self::Success => "success",
Self::Warning => "warning",
Self::Error => "error",
Self::Info => "info",
Self::Surface => "surface",
Self::Highlight => "highlight",
}
}
pub fn label(self) -> &'static str {
match self {
Self::Primary => "Primary",
Self::Secondary => "Secondary",
Self::Background => "Background",
Self::Foreground => "Foreground",
Self::Muted => "Muted",
Self::Border => "Border",
Self::Success => "Success",
Self::Warning => "Warning",
Self::Error => "Error",
Self::Info => "Info",
Self::Surface => "Surface",
Self::Highlight => "Highlight",
}
}
}
impl FromStr for ThemeRole {
type Err = ParseThemeRoleError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match normalized_token_name(value).as_str() {
"primary" => Ok(Self::Primary),
"secondary" => Ok(Self::Secondary),
"background" | "bg" => Ok(Self::Background),
"foreground" | "fg" | "text" => Ok(Self::Foreground),
"muted" | "dim" => Ok(Self::Muted),
"border" => Ok(Self::Border),
"success" | "ok" => Ok(Self::Success),
"warning" | "warn" => Ok(Self::Warning),
"error" | "danger" => Ok(Self::Error),
"info" => Ok(Self::Info),
"surface" => Ok(Self::Surface),
"highlight" | "selection" => Ok(Self::Highlight),
_ => Err(ParseThemeRoleError),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseThemeRoleError;
impl fmt::Display for ParseThemeRoleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("unknown theme role")
}
}
impl Error for ParseThemeRoleError {}
#[derive(Debug, Clone)]
pub struct Theme {
pub primary: Color,
pub secondary: Color,
pub bg: Color,
pub fg: Color,
pub muted: Color,
pub border: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
pub surface: Color,
pub highlight: Color,
}
impl Theme {
pub fn builtins() -> &'static [BuiltinTheme] {
&BUILTIN_THEMES
}
pub fn builtin(theme: BuiltinTheme) -> Self {
theme.theme()
}
pub fn from_builtin_name(name: &str) -> Option<Self> {
name.parse::<BuiltinTheme>().ok().map(Self::builtin)
}
pub fn roles() -> &'static [ThemeRole] {
&THEME_ROLES
}
pub fn color(&self, role: ThemeRole) -> Color {
match role {
ThemeRole::Primary => self.primary,
ThemeRole::Secondary => self.secondary,
ThemeRole::Background => self.bg,
ThemeRole::Foreground => self.fg,
ThemeRole::Muted => self.muted,
ThemeRole::Border => self.border,
ThemeRole::Success => self.success,
ThemeRole::Warning => self.warning,
ThemeRole::Error => self.error,
ThemeRole::Info => self.info,
ThemeRole::Surface => self.surface,
ThemeRole::Highlight => self.highlight,
}
}
pub fn foreground_style(&self, role: ThemeRole) -> Style {
Style::new().fg(self.color(role))
}
pub fn background_style(&self, role: ThemeRole) -> Style {
Style::new().bg(self.color(role))
}
pub fn pair_style(&self, fg: ThemeRole, bg: ThemeRole) -> Style {
Style::new().fg(self.color(fg)).bg(self.color(bg))
}
pub fn selection_style(&self) -> Style {
self.pair_style(ThemeRole::Foreground, ThemeRole::Highlight)
.bold()
}
pub fn surface_style(&self) -> Style {
self.pair_style(ThemeRole::Foreground, ThemeRole::Surface)
}
pub fn dark() -> Self {
Self {
primary: Color::Cyan,
secondary: Color::Blue,
bg: Color::Black,
fg: Color::White,
muted: Color::BrightBlack,
border: Color::BrightBlack,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Blue,
surface: Color::BrightBlack,
highlight: Color::BrightBlue,
}
}
pub fn light() -> Self {
Self {
primary: Color::Blue,
secondary: Color::Cyan,
bg: Color::White,
fg: Color::Black,
muted: Color::BrightBlack,
border: Color::BrightBlack,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Blue,
surface: Color::BrightWhite,
highlight: Color::BrightCyan,
}
}
pub fn catppuccin() -> Self {
Self {
primary: Color::Rgb(137, 180, 250), secondary: Color::Rgb(180, 190, 254), bg: Color::Rgb(30, 30, 46), fg: Color::Rgb(205, 214, 244), muted: Color::Rgb(127, 132, 156), border: Color::Rgb(88, 91, 112), success: Color::Rgb(166, 227, 161), warning: Color::Rgb(249, 226, 175), error: Color::Rgb(243, 139, 168), info: Color::Rgb(116, 199, 236), surface: Color::Rgb(49, 50, 68), highlight: Color::Rgb(69, 71, 90), }
}
pub fn tokyo_night() -> Self {
Self {
primary: 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), border: Color::Rgb(61, 89, 161), success: Color::Rgb(158, 206, 106), warning: Color::Rgb(224, 175, 104), error: Color::Rgb(247, 118, 142), info: Color::Rgb(125, 207, 255), surface: Color::Rgb(36, 40, 59), highlight: Color::Rgb(41, 46, 66), }
}
}
fn normalized_token_name(value: &str) -> String {
value
.trim()
.chars()
.map(|ch| match ch {
'_' | ' ' => '-',
ch => ch.to_ascii_lowercase(),
})
.collect()
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_dark() {
let theme = Theme::default();
assert_eq!(theme.bg, Color::Black);
assert_eq!(theme.fg, Color::White);
}
#[test]
fn light_theme_has_white_bg() {
let theme = Theme::light();
assert_eq!(theme.bg, Color::White);
assert_eq!(theme.fg, Color::Black);
}
#[test]
fn catppuccin_uses_rgb() {
let theme = Theme::catppuccin();
assert!(matches!(theme.primary, Color::Rgb(_, _, _)));
assert!(matches!(theme.bg, Color::Rgb(_, _, _)));
}
#[test]
fn tokyo_night_uses_rgb() {
let theme = Theme::tokyo_night();
assert!(matches!(theme.primary, Color::Rgb(_, _, _)));
assert!(matches!(theme.bg, Color::Rgb(_, _, _)));
}
#[test]
fn built_in_themes_parse_friendly_names() {
assert_eq!("dark".parse::<BuiltinTheme>(), Ok(BuiltinTheme::Dark));
assert_eq!(
"Tokyo Night".parse::<BuiltinTheme>(),
Ok(BuiltinTheme::TokyoNight)
);
assert_eq!(
"catppuccin_mocha".parse::<BuiltinTheme>(),
Ok(BuiltinTheme::Catppuccin)
);
assert!("missing".parse::<BuiltinTheme>().is_err());
assert_eq!(
Theme::from_builtin_name("tokyo-night").unwrap().primary,
Theme::tokyo_night().primary
);
}
#[test]
fn theme_roles_map_to_palette_colors() {
let theme = Theme::dark();
assert_eq!(Theme::roles().len(), ThemeRole::ALL.len());
assert_eq!(theme.color(ThemeRole::Primary), theme.primary);
assert_eq!(theme.color(ThemeRole::Background), theme.bg);
assert_eq!(theme.color(ThemeRole::Highlight), theme.highlight);
assert_eq!("fg".parse::<ThemeRole>(), Ok(ThemeRole::Foreground));
assert_eq!("danger".parse::<ThemeRole>(), Ok(ThemeRole::Error));
assert!("unknown".parse::<ThemeRole>().is_err());
}
#[test]
fn theme_style_helpers_use_roles() {
let theme = Theme::tokyo_night();
let primary = theme.foreground_style(ThemeRole::Primary);
let selected = theme.selection_style();
let surface = theme.surface_style();
assert_eq!(primary.foreground(), Some(theme.primary));
assert_eq!(selected.foreground(), Some(theme.fg));
assert_eq!(selected.background(), Some(theme.highlight));
assert!(selected.is_bold());
assert_eq!(surface.foreground(), Some(theme.fg));
assert_eq!(surface.background(), Some(theme.surface));
}
}