use crate::core::Color;
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
pub primary: Color,
pub secondary: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
pub text: TextColors,
pub background: BackgroundColors,
pub border: BorderColors,
pub components: ComponentColors,
}
#[derive(Debug, Clone)]
pub struct TextColors {
pub primary: Color,
pub secondary: Color,
pub disabled: Color,
pub inverted: Color,
pub link: Color,
}
#[derive(Debug, Clone)]
pub struct BackgroundColors {
pub default: Color,
pub elevated: Color,
pub selected: Color,
pub hover: Color,
pub disabled: Color,
}
#[derive(Debug, Clone)]
pub struct BorderColors {
pub default: Color,
pub focused: Color,
pub error: Color,
pub disabled: Color,
}
#[derive(Debug, Clone)]
pub struct ComponentColors {
pub input: InputColors,
pub button: ButtonColors,
pub list: ListColors,
pub progress: ProgressColors,
}
#[derive(Debug, Clone)]
pub struct InputColors {
pub background: Color,
pub text: Color,
pub placeholder: Color,
pub cursor: Color,
pub selection: Color,
}
#[derive(Debug, Clone)]
pub struct ButtonColors {
pub primary_bg: Color,
pub primary_text: Color,
pub secondary_bg: Color,
pub secondary_text: Color,
pub danger_bg: Color,
pub danger_text: Color,
}
#[derive(Debug, Clone)]
pub struct ListColors {
pub item_bg: Color,
pub item_text: Color,
pub selected_bg: Color,
pub selected_text: Color,
pub focused_bg: Color,
pub focused_text: Color,
}
#[derive(Debug, Clone)]
pub struct ProgressColors {
pub track: Color,
pub fill: Color,
pub completed: Color,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub fn builder(name: impl Into<String>) -> ThemeBuilder {
ThemeBuilder::new(name)
}
pub fn dark() -> Self {
Self {
name: "dark".to_string(),
primary: Color::Cyan,
secondary: Color::Magenta,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Blue,
text: TextColors {
primary: Color::White,
secondary: Color::BrightBlack,
disabled: Color::BrightBlack,
inverted: Color::Black,
link: Color::Cyan,
},
background: BackgroundColors {
default: Color::Black,
elevated: Color::BrightBlack,
selected: Color::Blue,
hover: Color::BrightBlack,
disabled: Color::BrightBlack,
},
border: BorderColors {
default: Color::BrightBlack,
focused: Color::Cyan,
error: Color::Red,
disabled: Color::BrightBlack,
},
components: ComponentColors {
input: InputColors {
background: Color::Black,
text: Color::White,
placeholder: Color::BrightBlack,
cursor: Color::Cyan,
selection: Color::Blue,
},
button: ButtonColors {
primary_bg: Color::Cyan,
primary_text: Color::Black,
secondary_bg: Color::BrightBlack,
secondary_text: Color::White,
danger_bg: Color::Red,
danger_text: Color::White,
},
list: ListColors {
item_bg: Color::Black,
item_text: Color::White,
selected_bg: Color::Blue,
selected_text: Color::White,
focused_bg: Color::Cyan,
focused_text: Color::Black,
},
progress: ProgressColors {
track: Color::BrightBlack,
fill: Color::Cyan,
completed: Color::Green,
},
},
}
}
pub fn light() -> Self {
Self {
name: "light".to_string(),
primary: Color::Blue,
secondary: Color::Magenta,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
info: Color::Cyan,
text: TextColors {
primary: Color::Black,
secondary: Color::BrightBlack,
disabled: Color::BrightBlack,
inverted: Color::White,
link: Color::Blue,
},
background: BackgroundColors {
default: Color::White,
elevated: Color::BrightWhite,
selected: Color::Cyan,
hover: Color::BrightWhite,
disabled: Color::BrightWhite,
},
border: BorderColors {
default: Color::BrightBlack,
focused: Color::Blue,
error: Color::Red,
disabled: Color::BrightWhite,
},
components: ComponentColors {
input: InputColors {
background: Color::White,
text: Color::Black,
placeholder: Color::BrightBlack,
cursor: Color::Blue,
selection: Color::Cyan,
},
button: ButtonColors {
primary_bg: Color::Blue,
primary_text: Color::White,
secondary_bg: Color::BrightWhite,
secondary_text: Color::Black,
danger_bg: Color::Red,
danger_text: Color::White,
},
list: ListColors {
item_bg: Color::White,
item_text: Color::Black,
selected_bg: Color::Cyan,
selected_text: Color::Black,
focused_bg: Color::Blue,
focused_text: Color::White,
},
progress: ProgressColors {
track: Color::BrightWhite,
fill: Color::Blue,
completed: Color::Green,
},
},
}
}
pub fn monokai() -> Self {
Self {
name: "monokai".to_string(),
primary: Color::Rgb(166, 226, 46), secondary: Color::Rgb(174, 129, 255), success: Color::Rgb(166, 226, 46),
warning: Color::Rgb(230, 219, 116), error: Color::Rgb(249, 38, 114), info: Color::Rgb(102, 217, 239), text: TextColors {
primary: Color::Rgb(248, 248, 242),
secondary: Color::Rgb(117, 113, 94),
disabled: Color::Rgb(117, 113, 94),
inverted: Color::Rgb(39, 40, 34),
link: Color::Rgb(102, 217, 239),
},
background: BackgroundColors {
default: Color::Rgb(39, 40, 34),
elevated: Color::Rgb(49, 50, 44),
selected: Color::Rgb(73, 72, 62),
hover: Color::Rgb(59, 60, 54),
disabled: Color::Rgb(49, 50, 44),
},
border: BorderColors {
default: Color::Rgb(117, 113, 94),
focused: Color::Rgb(166, 226, 46),
error: Color::Rgb(249, 38, 114),
disabled: Color::Rgb(73, 72, 62),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(39, 40, 34),
text: Color::Rgb(248, 248, 242),
placeholder: Color::Rgb(117, 113, 94),
cursor: Color::Rgb(248, 248, 242),
selection: Color::Rgb(73, 72, 62),
},
button: ButtonColors {
primary_bg: Color::Rgb(166, 226, 46),
primary_text: Color::Rgb(39, 40, 34),
secondary_bg: Color::Rgb(73, 72, 62),
secondary_text: Color::Rgb(248, 248, 242),
danger_bg: Color::Rgb(249, 38, 114),
danger_text: Color::Rgb(248, 248, 242),
},
list: ListColors {
item_bg: Color::Rgb(39, 40, 34),
item_text: Color::Rgb(248, 248, 242),
selected_bg: Color::Rgb(73, 72, 62),
selected_text: Color::Rgb(248, 248, 242),
focused_bg: Color::Rgb(166, 226, 46),
focused_text: Color::Rgb(39, 40, 34),
},
progress: ProgressColors {
track: Color::Rgb(73, 72, 62),
fill: Color::Rgb(166, 226, 46),
completed: Color::Rgb(166, 226, 46),
},
},
}
}
pub fn dracula() -> Self {
Self {
name: "dracula".to_string(),
primary: Color::Rgb(189, 147, 249), secondary: Color::Rgb(255, 121, 198), success: Color::Rgb(80, 250, 123), warning: Color::Rgb(255, 184, 108), error: Color::Rgb(255, 85, 85), info: Color::Rgb(139, 233, 253), text: TextColors {
primary: Color::Rgb(248, 248, 242),
secondary: Color::Rgb(98, 114, 164),
disabled: Color::Rgb(68, 71, 90),
inverted: Color::Rgb(40, 42, 54),
link: Color::Rgb(139, 233, 253),
},
background: BackgroundColors {
default: Color::Rgb(40, 42, 54),
elevated: Color::Rgb(68, 71, 90),
selected: Color::Rgb(68, 71, 90),
hover: Color::Rgb(68, 71, 90),
disabled: Color::Rgb(68, 71, 90),
},
border: BorderColors {
default: Color::Rgb(68, 71, 90),
focused: Color::Rgb(189, 147, 249),
error: Color::Rgb(255, 85, 85),
disabled: Color::Rgb(68, 71, 90),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(40, 42, 54),
text: Color::Rgb(248, 248, 242),
placeholder: Color::Rgb(98, 114, 164),
cursor: Color::Rgb(248, 248, 242),
selection: Color::Rgb(68, 71, 90),
},
button: ButtonColors {
primary_bg: Color::Rgb(189, 147, 249),
primary_text: Color::Rgb(40, 42, 54),
secondary_bg: Color::Rgb(68, 71, 90),
secondary_text: Color::Rgb(248, 248, 242),
danger_bg: Color::Rgb(255, 85, 85),
danger_text: Color::Rgb(248, 248, 242),
},
list: ListColors {
item_bg: Color::Rgb(40, 42, 54),
item_text: Color::Rgb(248, 248, 242),
selected_bg: Color::Rgb(68, 71, 90),
selected_text: Color::Rgb(248, 248, 242),
focused_bg: Color::Rgb(189, 147, 249),
focused_text: Color::Rgb(40, 42, 54),
},
progress: ProgressColors {
track: Color::Rgb(68, 71, 90),
fill: Color::Rgb(189, 147, 249),
completed: Color::Rgb(80, 250, 123),
},
},
}
}
pub fn nord() -> Self {
Self {
name: "nord".to_string(),
primary: Color::Rgb(136, 192, 208), secondary: Color::Rgb(180, 142, 173), success: Color::Rgb(163, 190, 140), warning: Color::Rgb(235, 203, 139), error: Color::Rgb(191, 97, 106), info: Color::Rgb(129, 161, 193), text: TextColors {
primary: Color::Rgb(236, 239, 244),
secondary: Color::Rgb(76, 86, 106),
disabled: Color::Rgb(76, 86, 106),
inverted: Color::Rgb(46, 52, 64),
link: Color::Rgb(136, 192, 208),
},
background: BackgroundColors {
default: Color::Rgb(46, 52, 64),
elevated: Color::Rgb(59, 66, 82),
selected: Color::Rgb(67, 76, 94),
hover: Color::Rgb(59, 66, 82),
disabled: Color::Rgb(59, 66, 82),
},
border: BorderColors {
default: Color::Rgb(76, 86, 106),
focused: Color::Rgb(136, 192, 208),
error: Color::Rgb(191, 97, 106),
disabled: Color::Rgb(59, 66, 82),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(46, 52, 64),
text: Color::Rgb(236, 239, 244),
placeholder: Color::Rgb(76, 86, 106),
cursor: Color::Rgb(236, 239, 244),
selection: Color::Rgb(67, 76, 94),
},
button: ButtonColors {
primary_bg: Color::Rgb(136, 192, 208),
primary_text: Color::Rgb(46, 52, 64),
secondary_bg: Color::Rgb(67, 76, 94),
secondary_text: Color::Rgb(236, 239, 244),
danger_bg: Color::Rgb(191, 97, 106),
danger_text: Color::Rgb(236, 239, 244),
},
list: ListColors {
item_bg: Color::Rgb(46, 52, 64),
item_text: Color::Rgb(236, 239, 244),
selected_bg: Color::Rgb(67, 76, 94),
selected_text: Color::Rgb(236, 239, 244),
focused_bg: Color::Rgb(136, 192, 208),
focused_text: Color::Rgb(46, 52, 64),
},
progress: ProgressColors {
track: Color::Rgb(67, 76, 94),
fill: Color::Rgb(136, 192, 208),
completed: Color::Rgb(163, 190, 140),
},
},
}
}
pub fn solarized_dark() -> Self {
Self {
name: "solarized_dark".to_string(),
primary: Color::Rgb(38, 139, 210), secondary: Color::Rgb(108, 113, 196), success: Color::Rgb(133, 153, 0), warning: Color::Rgb(181, 137, 0), error: Color::Rgb(220, 50, 47), info: Color::Rgb(42, 161, 152), text: TextColors {
primary: Color::Rgb(131, 148, 150),
secondary: Color::Rgb(88, 110, 117),
disabled: Color::Rgb(88, 110, 117),
inverted: Color::Rgb(0, 43, 54),
link: Color::Rgb(38, 139, 210),
},
background: BackgroundColors {
default: Color::Rgb(0, 43, 54),
elevated: Color::Rgb(7, 54, 66),
selected: Color::Rgb(7, 54, 66),
hover: Color::Rgb(7, 54, 66),
disabled: Color::Rgb(7, 54, 66),
},
border: BorderColors {
default: Color::Rgb(88, 110, 117),
focused: Color::Rgb(38, 139, 210),
error: Color::Rgb(220, 50, 47),
disabled: Color::Rgb(7, 54, 66),
},
components: ComponentColors {
input: InputColors {
background: Color::Rgb(0, 43, 54),
text: Color::Rgb(131, 148, 150),
placeholder: Color::Rgb(88, 110, 117),
cursor: Color::Rgb(131, 148, 150),
selection: Color::Rgb(7, 54, 66),
},
button: ButtonColors {
primary_bg: Color::Rgb(38, 139, 210),
primary_text: Color::Rgb(253, 246, 227),
secondary_bg: Color::Rgb(7, 54, 66),
secondary_text: Color::Rgb(131, 148, 150),
danger_bg: Color::Rgb(220, 50, 47),
danger_text: Color::Rgb(253, 246, 227),
},
list: ListColors {
item_bg: Color::Rgb(0, 43, 54),
item_text: Color::Rgb(131, 148, 150),
selected_bg: Color::Rgb(7, 54, 66),
selected_text: Color::Rgb(131, 148, 150),
focused_bg: Color::Rgb(38, 139, 210),
focused_text: Color::Rgb(253, 246, 227),
},
progress: ProgressColors {
track: Color::Rgb(7, 54, 66),
fill: Color::Rgb(38, 139, 210),
completed: Color::Rgb(133, 153, 0),
},
},
}
}
pub fn by_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"dark" => Some(Self::dark()),
"light" => Some(Self::light()),
"monokai" => Some(Self::monokai()),
"dracula" => Some(Self::dracula()),
"nord" => Some(Self::nord()),
"solarized" | "solarized_dark" => Some(Self::solarized_dark()),
_ => None,
}
}
pub fn available_themes() -> Vec<&'static str> {
vec![
"dark",
"light",
"monokai",
"dracula",
"nord",
"solarized_dark",
]
}
pub fn semantic_color(&self, semantic: SemanticColor) -> Color {
match semantic {
SemanticColor::Primary => self.primary,
SemanticColor::Secondary => self.secondary,
SemanticColor::Success => self.success,
SemanticColor::Warning => self.warning,
SemanticColor::Error => self.error,
SemanticColor::Info => self.info,
SemanticColor::TextPrimary => self.text.primary,
SemanticColor::TextSecondary => self.text.secondary,
SemanticColor::TextDisabled => self.text.disabled,
SemanticColor::Background => self.background.default,
SemanticColor::BackgroundElevated => self.background.elevated,
SemanticColor::Border => self.border.default,
SemanticColor::BorderFocused => self.border.focused,
}
}
pub fn semantic_fg(&self, semantic: SemanticColor) -> String {
self.semantic_color(semantic).to_ansi_fg()
}
pub fn semantic_bg(&self, semantic: SemanticColor) -> String {
self.semantic_color(semantic).to_ansi_bg()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticColor {
Primary,
Secondary,
Success,
Warning,
Error,
Info,
TextPrimary,
TextSecondary,
TextDisabled,
Background,
BackgroundElevated,
Border,
BorderFocused,
}
#[derive(Debug, Clone)]
pub struct ThemeBuilder {
theme: Theme,
}
impl ThemeBuilder {
pub fn new(name: impl Into<String>) -> Self {
let mut theme = Theme::dark();
theme.name = name.into();
Self { theme }
}
pub fn primary(mut self, color: Color) -> Self {
self.theme.primary = color;
self
}
pub fn secondary(mut self, color: Color) -> Self {
self.theme.secondary = color;
self
}
pub fn success(mut self, color: Color) -> Self {
self.theme.success = color;
self
}
pub fn warning(mut self, color: Color) -> Self {
self.theme.warning = color;
self
}
pub fn error(mut self, color: Color) -> Self {
self.theme.error = color;
self
}
pub fn info(mut self, color: Color) -> Self {
self.theme.info = color;
self
}
pub fn text_colors(mut self, colors: TextColors) -> Self {
self.theme.text = colors;
self
}
pub fn background_colors(mut self, colors: BackgroundColors) -> Self {
self.theme.background = colors;
self
}
pub fn border_colors(mut self, colors: BorderColors) -> Self {
self.theme.border = colors;
self
}
pub fn build(self) -> Theme {
self.theme
}
}
thread_local! {
static CURRENT_THEME: std::cell::RefCell<Theme> = std::cell::RefCell::new(Theme::dark());
}
pub fn set_theme(theme: Theme) {
CURRENT_THEME.with(|t| {
*t.borrow_mut() = theme;
});
}
pub fn get_theme() -> Theme {
CURRENT_THEME.with(|t| t.borrow().clone())
}
pub fn with_theme<F, R>(theme: Theme, f: F) -> R
where
F: FnOnce(&Theme) -> R,
{
let old_theme = get_theme();
set_theme(theme.clone());
let result = f(&theme);
set_theme(old_theme);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_dark() {
let theme = Theme::dark();
assert_eq!(theme.name, "dark");
assert_eq!(theme.primary, Color::Cyan);
}
#[test]
fn test_theme_light() {
let theme = Theme::light();
assert_eq!(theme.name, "light");
assert_eq!(theme.primary, Color::Blue);
}
#[test]
fn test_theme_monokai() {
let theme = Theme::monokai();
assert_eq!(theme.name, "monokai");
}
#[test]
fn test_theme_dracula() {
let theme = Theme::dracula();
assert_eq!(theme.name, "dracula");
}
#[test]
fn test_theme_nord() {
let theme = Theme::nord();
assert_eq!(theme.name, "nord");
}
#[test]
fn test_theme_solarized() {
let theme = Theme::solarized_dark();
assert_eq!(theme.name, "solarized_dark");
}
#[test]
fn test_theme_by_name() {
assert!(Theme::by_name("dark").is_some());
assert!(Theme::by_name("light").is_some());
assert!(Theme::by_name("monokai").is_some());
assert!(Theme::by_name("dracula").is_some());
assert!(Theme::by_name("nord").is_some());
assert!(Theme::by_name("solarized").is_some());
assert!(Theme::by_name("nonexistent").is_none());
}
#[test]
fn test_available_themes() {
let themes = Theme::available_themes();
assert!(themes.contains(&"dark"));
assert!(themes.contains(&"light"));
assert!(themes.contains(&"monokai"));
}
#[test]
fn test_theme_builder() {
let theme = Theme::builder("custom")
.primary(Color::Red)
.secondary(Color::Blue)
.success(Color::Green)
.build();
assert_eq!(theme.name, "custom");
assert_eq!(theme.primary, Color::Red);
assert_eq!(theme.secondary, Color::Blue);
assert_eq!(theme.success, Color::Green);
}
#[test]
fn test_set_get_theme() {
let original = get_theme();
set_theme(Theme::light());
assert_eq!(get_theme().name, "light");
set_theme(Theme::dark());
assert_eq!(get_theme().name, "dark");
set_theme(original);
}
#[test]
fn test_with_theme() {
let result = with_theme(Theme::monokai(), |theme| {
assert_eq!(theme.name, "monokai");
42
});
assert_eq!(result, 42);
}
#[test]
fn test_semantic_color() {
let theme = Theme::dark();
assert_eq!(theme.semantic_color(SemanticColor::Primary), theme.primary);
assert_eq!(
theme.semantic_color(SemanticColor::Secondary),
theme.secondary
);
assert_eq!(theme.semantic_color(SemanticColor::Success), theme.success);
assert_eq!(theme.semantic_color(SemanticColor::Warning), theme.warning);
assert_eq!(theme.semantic_color(SemanticColor::Error), theme.error);
assert_eq!(theme.semantic_color(SemanticColor::Info), theme.info);
assert_eq!(
theme.semantic_color(SemanticColor::TextPrimary),
theme.text.primary
);
assert_eq!(
theme.semantic_color(SemanticColor::Background),
theme.background.default
);
assert_eq!(
theme.semantic_color(SemanticColor::Border),
theme.border.default
);
}
#[test]
fn test_semantic_fg_bg() {
let theme = Theme::dark();
let fg = theme.semantic_fg(SemanticColor::Error);
assert!(fg.starts_with("\x1b["));
let bg = theme.semantic_bg(SemanticColor::Error);
assert!(bg.starts_with("\x1b["));
}
}