use owo_colors::OwoColorize;
use crate::terminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Theme {
#[default]
Dark,
Light,
HighContrast,
Monochrome,
}
impl Theme {
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::is_valid_name(name).then_some(Self::from_name_unchecked(name))
}
#[must_use]
pub fn is_valid_name(name: &str) -> bool {
matches!(
name.to_lowercase().as_str(),
"dark" | "light" | "high-contrast" | "highcontrast" | "monochrome" | "mono"
)
}
fn from_name_unchecked(name: &str) -> Self {
match name.to_lowercase().as_str() {
"dark" => Self::Dark,
"light" => Self::Light,
"high-contrast" | "highcontrast" => Self::HighContrast,
"monochrome" | "mono" => Self::Monochrome,
_ => Self::Dark, }
}
pub fn validate(name: &str) -> Result<(), String> {
if Self::is_valid_name(name) {
Ok(())
} else {
Err(format!(
"Invalid theme '{}'. Valid options: {}",
name,
Self::VALID_NAMES.join(", ")
))
}
}
pub const TYPE_NAME: &'static str = "theme";
pub const VALID_NAMES: &'static [&'static str] =
&["dark", "light", "high-contrast", "monochrome"];
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Dark => "dark",
Self::Light => "light",
Self::HighContrast => "high-contrast",
Self::Monochrome => "monochrome",
}
}
}
pub struct Colors;
impl Colors {
#[must_use]
pub fn good(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark | Theme::HighContrast => s.green().bold().to_string(),
Theme::Light => s.green().to_string(),
Theme::Monochrome => s.bold().to_string(),
}
}
}
#[must_use]
pub fn warn(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark | Theme::HighContrast => s.yellow().bold().to_string(),
Theme::Light => s.yellow().to_string(),
Theme::Monochrome => s.italic().to_string(),
}
}
}
#[must_use]
pub fn bad(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark | Theme::HighContrast => s.red().bold().to_string(),
Theme::Light => s.red().to_string(),
Theme::Monochrome => s.bold().to_string(),
}
}
}
#[must_use]
pub fn info(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark => s.cyan().to_string(),
Theme::Light => s.blue().to_string(),
Theme::HighContrast => s.cyan().bold().to_string(),
Theme::Monochrome => s.italic().to_string(),
}
}
}
#[must_use]
pub fn dimmed(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark | Theme::Light => s.dimmed().to_string(),
Theme::HighContrast | Theme::Monochrome => s.to_string(), }
}
}
#[must_use]
pub fn bold(s: &str, _theme: Theme) -> String {
s.bold().to_string()
}
#[must_use]
pub fn muted(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark | Theme::HighContrast => s.bright_black().to_string(),
Theme::Light => s.dimmed().to_string(),
Theme::Monochrome => s.to_string(),
}
}
}
#[must_use]
pub fn header(s: &str, theme: Theme) -> String {
if terminal::no_color() || theme == Theme::Monochrome {
s.to_string()
} else {
match theme {
Theme::Dark | Theme::HighContrast => s.cyan().bold().underline().to_string(),
Theme::Light => s.blue().bold().underline().to_string(),
Theme::Monochrome => s.bold().to_string(),
}
}
}
}
#[must_use]
pub fn resolve(config_theme: &str, minimal: bool) -> Theme {
if minimal || terminal::no_color() {
return Theme::Monochrome;
}
Theme::from_name(config_theme).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_name() {
assert!(Theme::is_valid_name("dark"));
assert!(Theme::is_valid_name("DARK"));
assert!(Theme::is_valid_name("high-contrast"));
assert!(Theme::is_valid_name("monochrome"));
assert!(Theme::is_valid_name("mono")); assert!(Theme::is_valid_name("highcontrast")); assert!(!Theme::is_valid_name("invalid"));
}
#[test]
fn test_validate_valid() {
assert!(Theme::validate("dark").is_ok());
assert!(Theme::validate("light").is_ok());
assert!(Theme::validate("high-contrast").is_ok());
}
#[test]
fn test_validate_invalid() {
let result = Theme::validate("invalid");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("Invalid theme"));
assert!(err.contains("valid"));
}
#[test]
fn test_theme_from_name() {
assert!(Theme::from_name("dark").is_some());
assert!(Theme::from_name("light").is_some());
assert!(Theme::from_name("high-contrast").is_some());
assert!(Theme::from_name("highcontrast").is_some());
assert!(Theme::from_name("monochrome").is_some());
assert!(Theme::from_name("mono").is_some());
assert!(Theme::from_name("invalid").is_none());
}
#[test]
fn test_theme_name_roundtrip() {
for theme in [
Theme::Dark,
Theme::Light,
Theme::HighContrast,
Theme::Monochrome,
] {
assert_eq!(Theme::from_name(theme.name()), Some(theme));
}
}
#[test]
fn test_resolve_theme_minimal() {
assert_eq!(resolve("dark", true), Theme::Monochrome);
assert_eq!(resolve("light", true), Theme::Monochrome);
}
#[test]
fn test_resolve_theme_default() {
if terminal::no_color() {
return;
}
assert_eq!(resolve("dark", false), Theme::Dark);
assert_eq!(resolve("invalid", false), Theme::Dark);
assert_eq!(resolve("light", false), Theme::Light);
assert_eq!(resolve("high-contrast", false), Theme::HighContrast);
}
}