use {
crate::{app_error::AppError, configs::raw::theme_raw::ThemeEntry, PALETTE_CONFIG},
ratatui::style::{Color, Modifier, Style},
};
#[derive(Clone, Debug)]
pub struct ThemeStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub modifier: Modifier,
}
impl ThemeStyle {
pub fn set_bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
pub fn set_fg(mut self, fg: Color) -> Self {
self.fg = Some(fg);
self
}
pub fn insert(mut self, modifier: Modifier) -> Self {
self.modifier.insert(modifier);
self
}
pub fn as_style(&self) -> Style {
Style::from(self)
}
pub fn str_to_color(s: &str) -> Result<Color, AppError<()>> {
match s {
"black" => Ok(Color::Black),
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"yellow" => Ok(Color::Yellow),
"blue" => Ok(Color::Blue),
"magenta" => Ok(Color::Magenta),
"cyan" => Ok(Color::Cyan),
"gray" => Ok(Color::Gray),
"dark_gray" => Ok(Color::DarkGray),
"light_red" => Ok(Color::LightRed),
"light_green" => Ok(Color::LightGreen),
"light_yellow" => Ok(Color::LightYellow),
"light_blue" => Ok(Color::LightBlue),
"light_magenta" => Ok(Color::LightMagenta),
"light_cyan" => Ok(Color::LightCyan),
"white" => Ok(Color::White),
"reset" | "" => Ok(Color::Reset),
s if s.starts_with('#') => {
let hex = s.trim_start_matches('#');
match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1], 16).unwrap();
let g = u8::from_str_radix(&hex[1..2], 16).unwrap();
let b = u8::from_str_radix(&hex[2..3], 16).unwrap();
Ok(Color::Rgb(r, g, b))
}
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).unwrap();
let g = u8::from_str_radix(&hex[2..4], 16).unwrap();
let b = u8::from_str_radix(&hex[4..6], 16).unwrap();
Ok(Color::Rgb(r, g, b))
}
_ => Err(AppError::InvalidColor(s.to_string())),
}
}
s => {
if let [r, g, b] = s
.split(',')
.map(|s| s.trim())
.collect::<Vec<&str>>()
.as_slice()
{
match (r.parse::<u8>(), g.parse::<u8>(), b.parse::<u8>()) {
(Ok(r), Ok(g), Ok(b)) => return Ok(Color::Rgb(r, g, b)),
_ => return Err(AppError::InvalidColor(s.to_string())),
}
}
match s.parse::<u8>() {
Ok(n) => Ok(Color::Indexed(n)),
_ => Err(AppError::InvalidColor(s.to_string())),
}
}
}
}
pub fn str_to_color_with_palette(s: &str) -> Result<Color, AppError<()>> {
if s.is_empty() {
return Err(AppError::InvalidColor(s.to_string()));
}
if let Some(color) = PALETTE_CONFIG.palette.get(s) {
return Ok(*color);
}
Self::str_to_color(s)
}
}
impl From<ThemeEntry> for ThemeStyle {
fn from(entry: ThemeEntry) -> Self {
let fg = match entry.fg {
Some(fg) => match Self::str_to_color_with_palette(&fg) {
Ok(color) => Some(color),
Err(e) => {
tracing::error!("Failed to parse foreground color: {}", e);
None
}
},
None => {
tracing::info!("Miss foreground color in the theme config");
None
}
};
let bg = match entry.bg {
Some(bg) => match Self::str_to_color_with_palette(&bg) {
Ok(color) => Some(color),
Err(e) => {
tracing::error!("Failed to parse background color: {}", e);
None
}
},
None => {
tracing::info!("Miss background color in the theme config");
None
}
};
let mut modifier = Modifier::empty();
modifier.insert(match entry.italic {
Some(true) => Modifier::ITALIC,
_ => Modifier::empty(),
});
modifier.insert(match entry.bold {
Some(true) => Modifier::BOLD,
_ => Modifier::empty(),
});
modifier.insert(match entry.underline {
Some(true) => Modifier::UNDERLINED,
_ => Modifier::empty(),
});
Self { fg, bg, modifier }
}
}
impl Default for ThemeStyle {
fn default() -> Self {
Self {
fg: None,
bg: None,
modifier: Modifier::empty(),
}
}
}
impl From<&ThemeStyle> for Style {
fn from(style: &ThemeStyle) -> Self {
Style {
fg: style.fg,
bg: style.bg,
add_modifier: style.modifier,
..Style::default()
}
}
}
#[cfg(test)]
mod tests {
use {
crate::configs::{config_theme::ThemeStyle, raw::theme_raw::ThemeEntry},
ratatui::style::{Color, Modifier},
};
#[test]
fn test_theme_style_from() {
let entry = ThemeEntry {
fg: Some("white".to_string()),
bg: Some("black".to_string()),
italic: Some(true),
bold: Some(true),
underline: Some(true),
};
let style = ThemeStyle::from(entry);
assert_eq!(style.fg, Some(Color::Rgb(255, 255, 255)));
assert_eq!(style.bg, Some(Color::Rgb(0, 0, 0)));
assert_eq!(
style.modifier,
Modifier::ITALIC | Modifier::BOLD | Modifier::UNDERLINED
);
}
#[test]
fn test_theme_style_str_to_color() {
let entry = ThemeEntry {
fg: Some("white".to_string()),
bg: Some("black".to_string()),
italic: Some(true),
bold: Some(true),
underline: Some(true),
};
let colored_entry_fg = ThemeStyle::str_to_color(&entry.fg.unwrap()).unwrap();
let colored_entry_bg = ThemeStyle::str_to_color(&entry.bg.unwrap()).unwrap();
let mut modifier = Modifier::empty();
modifier.insert(match entry.italic {
Some(true) => Modifier::ITALIC,
_ => Modifier::empty(),
});
modifier.insert(match entry.bold {
Some(true) => Modifier::BOLD,
_ => Modifier::empty(),
});
modifier.insert(match entry.underline {
Some(true) => Modifier::UNDERLINED,
_ => Modifier::empty(),
});
let style = ThemeStyle {
fg: Some(colored_entry_fg),
bg: Some(colored_entry_bg),
modifier,
};
assert_eq!(style.fg, Some(Color::White));
assert_eq!(style.bg, Some(Color::Black));
assert_eq!(
style.modifier,
Modifier::ITALIC | Modifier::BOLD | Modifier::UNDERLINED
);
}
#[test]
fn test_theme_style_str_to_color_with_palette() {
let entry = ThemeEntry {
fg: Some("black".to_string()),
bg: Some("white".to_string()),
italic: Some(true),
bold: Some(true),
underline: Some(true),
};
let colored_entry_fg = ThemeStyle::str_to_color_with_palette(&entry.fg.unwrap()).unwrap();
let colored_entry_bg = ThemeStyle::str_to_color_with_palette(&entry.bg.unwrap()).unwrap();
let mut modifier = Modifier::empty();
modifier.insert(match entry.italic {
Some(true) => Modifier::ITALIC,
_ => Modifier::empty(),
});
modifier.insert(match entry.bold {
Some(true) => Modifier::BOLD,
_ => Modifier::empty(),
});
modifier.insert(match entry.underline {
Some(true) => Modifier::UNDERLINED,
_ => Modifier::empty(),
});
let style = ThemeStyle {
fg: Some(colored_entry_fg),
bg: Some(colored_entry_bg),
modifier,
};
assert_eq!(style.fg, Some(Color::Rgb(0, 0, 0)));
assert_eq!(style.bg, Some(Color::Rgb(255, 255, 255)));
assert_eq!(
style.modifier,
Modifier::ITALIC | Modifier::BOLD | Modifier::UNDERLINED
);
}
#[test]
fn test_theme_style_as_style() {
let entry = ThemeEntry {
fg: Some("black".to_string()),
bg: Some("white".to_string()),
italic: Some(true),
bold: Some(true),
underline: Some(true),
};
let style = ThemeStyle::from(entry);
let ratatui_style = style.as_style();
assert_eq!(ratatui_style.fg.unwrap(), Color::Rgb(0, 0, 0));
assert_eq!(ratatui_style.bg.unwrap(), Color::Rgb(255, 255, 255));
assert_eq!(
ratatui_style.add_modifier,
Modifier::ITALIC | Modifier::BOLD | Modifier::UNDERLINED
);
}
#[test]
fn test_theme_style_default() {
let style = ThemeStyle::default();
assert_eq!(style.fg, None);
assert_eq!(style.bg, None);
assert_eq!(style.modifier, Modifier::empty());
}
#[test]
fn test_theme_style_from_ref() {
let entry = ThemeEntry {
fg: Some("black".to_string()),
bg: Some("white".to_string()),
italic: Some(true),
bold: Some(true),
underline: Some(true),
};
let style = ThemeStyle::from(entry);
let ratatui_style: ratatui::style::Style = (&style).into();
assert_eq!(ratatui_style.fg.unwrap(), Color::Rgb(0, 0, 0));
assert_eq!(ratatui_style.bg.unwrap(), Color::Rgb(255, 255, 255));
assert_eq!(
ratatui_style.add_modifier,
Modifier::ITALIC | Modifier::BOLD | Modifier::UNDERLINED
);
}
}