use ratatui::style::{Color, Modifier, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeKind {
Vesper,
Default,
Nord,
Gruvbox,
Solarized,
Plain,
}
impl ThemeKind {
#[must_use]
pub fn from_name(name: &str) -> Self {
match name.to_ascii_lowercase().as_str() {
"default" => Self::Default,
"nord" => Self::Nord,
"gruvbox" => Self::Gruvbox,
"solarized" => Self::Solarized,
"plain" | "none" | "no-color" => Self::Plain,
_ => Self::Vesper,
}
}
}
pub struct Theme {
accent: Color,
muted: Color,
border: Color,
dirty: Color,
ahead: Color,
behind: Color,
clean: Color,
selection_bg: Option<Color>,
}
impl Theme {
#[must_use]
pub const fn resolve(kind: ThemeKind, no_color: bool) -> Self {
if no_color {
return Self::plain();
}
match kind {
ThemeKind::Vesper => Self::vesper(),
ThemeKind::Default => Self::default_theme(),
ThemeKind::Nord => Self::nord(),
ThemeKind::Gruvbox => Self::gruvbox(),
ThemeKind::Solarized => Self::solarized(),
ThemeKind::Plain => Self::plain(),
}
}
#[must_use]
pub fn title(&self) -> Style {
Style::default()
.fg(self.accent)
.add_modifier(Modifier::BOLD)
}
#[must_use]
pub fn muted(&self) -> Style {
Style::default().fg(self.muted)
}
#[must_use]
pub fn accent(&self) -> Style {
Style::default()
.fg(self.accent)
.add_modifier(Modifier::BOLD)
}
#[must_use]
pub fn border(&self) -> Style {
Style::default().fg(self.border)
}
#[must_use]
pub fn selected(&self) -> Style {
self.selection_bg.map_or_else(
|| {
Style::default()
.add_modifier(Modifier::REVERSED)
.add_modifier(Modifier::BOLD)
},
|bg| Style::default().bg(bg).add_modifier(Modifier::BOLD),
)
}
#[must_use]
pub fn dirty(&self) -> Style {
Style::default().fg(self.dirty)
}
#[must_use]
pub fn ahead(&self) -> Style {
Style::default().fg(self.ahead)
}
#[must_use]
pub fn behind(&self) -> Style {
Style::default().fg(self.behind)
}
#[must_use]
pub fn clean(&self) -> Style {
Style::default().fg(self.clean)
}
const fn plain() -> Self {
Self {
accent: Color::Reset,
muted: Color::Reset,
border: Color::Reset,
dirty: Color::Reset,
ahead: Color::Reset,
behind: Color::Reset,
clean: Color::Reset,
selection_bg: None,
}
}
const fn vesper() -> Self {
Self {
accent: Color::Rgb(0xff, 0xc7, 0x99),
muted: Color::Rgb(0xa0, 0xa0, 0xa0),
border: Color::Rgb(0x28, 0x28, 0x28),
dirty: Color::Rgb(0xff, 0xc7, 0x99),
ahead: Color::Rgb(0x99, 0xff, 0xe4),
behind: Color::Rgb(0xff, 0x80, 0x80),
clean: Color::Rgb(0x50, 0x50, 0x50),
selection_bg: Some(Color::Rgb(0x23, 0x23, 0x23)),
}
}
const fn default_theme() -> Self {
Self {
accent: Color::Cyan,
muted: Color::DarkGray,
border: Color::DarkGray,
dirty: Color::Yellow,
ahead: Color::Green,
behind: Color::Red,
clean: Color::DarkGray,
selection_bg: None,
}
}
const fn nord() -> Self {
Self {
accent: Color::Rgb(0x88, 0xc0, 0xd0),
muted: Color::Rgb(0x4c, 0x56, 0x6a),
border: Color::Rgb(0x4c, 0x56, 0x6a),
dirty: Color::Rgb(0xeb, 0xcb, 0x8b),
ahead: Color::Rgb(0xa3, 0xbe, 0x8c),
behind: Color::Rgb(0xbf, 0x61, 0x6a),
clean: Color::Rgb(0x4c, 0x56, 0x6a),
selection_bg: Some(Color::Rgb(0x3b, 0x42, 0x52)),
}
}
const fn gruvbox() -> Self {
Self {
accent: Color::Rgb(0xfa, 0xbd, 0x2f),
muted: Color::Rgb(0x92, 0x83, 0x74),
border: Color::Rgb(0x92, 0x83, 0x74),
dirty: Color::Rgb(0xfa, 0xbd, 0x2f),
ahead: Color::Rgb(0xb8, 0xbb, 0x26),
behind: Color::Rgb(0xfb, 0x49, 0x34),
clean: Color::Rgb(0x92, 0x83, 0x74),
selection_bg: Some(Color::Rgb(0x3c, 0x38, 0x36)),
}
}
const fn solarized() -> Self {
Self {
accent: Color::Rgb(0x26, 0x8b, 0xd2),
muted: Color::Rgb(0x58, 0x6e, 0x75),
border: Color::Rgb(0x58, 0x6e, 0x75),
dirty: Color::Rgb(0xb5, 0x89, 0x00),
ahead: Color::Rgb(0x85, 0x99, 0x00),
behind: Color::Rgb(0xdc, 0x32, 0x2f),
clean: Color::Rgb(0x58, 0x6e, 0x75),
selection_bg: Some(Color::Rgb(0x07, 0x36, 0x42)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_resolve() {
assert_eq!(ThemeKind::from_name("vesper"), ThemeKind::Vesper);
assert_eq!(ThemeKind::from_name("VESPER"), ThemeKind::Vesper);
assert_eq!(ThemeKind::from_name("default"), ThemeKind::Default);
assert_eq!(ThemeKind::from_name("nord"), ThemeKind::Nord);
assert_eq!(ThemeKind::from_name("NORD"), ThemeKind::Nord);
assert_eq!(ThemeKind::from_name("gruvbox"), ThemeKind::Gruvbox);
assert_eq!(ThemeKind::from_name("solarized"), ThemeKind::Solarized);
assert_eq!(ThemeKind::from_name("plain"), ThemeKind::Plain);
assert_eq!(ThemeKind::from_name("unknown"), ThemeKind::Vesper);
assert_eq!(ThemeKind::from_name(""), ThemeKind::Vesper);
}
#[test]
fn no_color_matches_plain_theme() {
let nord_no_color = Theme::resolve(ThemeKind::Nord, true);
let plain = Theme::resolve(ThemeKind::Plain, false);
assert_eq!(nord_no_color.dirty(), plain.dirty());
assert_eq!(nord_no_color.accent(), plain.accent());
}
#[test]
fn themed_colors_differ_from_plain() {
let gruvbox = Theme::resolve(ThemeKind::Gruvbox, false);
let plain = Theme::resolve(ThemeKind::Plain, false);
assert_ne!(gruvbox.dirty(), plain.dirty());
}
#[test]
fn vesper_differs_from_plain_and_default() {
let vesper = Theme::resolve(ThemeKind::Vesper, false);
let plain = Theme::resolve(ThemeKind::Plain, false);
let default = Theme::resolve(ThemeKind::Default, false);
assert_ne!(vesper.accent(), plain.accent());
assert_ne!(vesper.accent(), default.accent());
assert_ne!(vesper.selected(), default.selected());
}
}