use colored::{ColoredString, Colorize};
use serde::{Deserialize, Serialize};
use crate::error::WikiError;
pub fn hex_to_rgb(hex: &str) -> Result<(u8, u8, u8), WikiError> {
let hex = hex.trim().trim_start_matches('#');
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| WikiError::InvalidColor(format!("#{hex}")))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| WikiError::InvalidColor(format!("#{hex}")))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| WikiError::InvalidColor(format!("#{hex}")))?;
Ok((r, g, b))
}
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
.map_err(|_| WikiError::InvalidColor(format!("#{hex}")))?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
.map_err(|_| WikiError::InvalidColor(format!("#{hex}")))?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
.map_err(|_| WikiError::InvalidColor(format!("#{hex}")))?;
Ok((r, g, b))
}
_ => Err(WikiError::InvalidColor(format!("#{hex}"))),
}
}
pub fn colorize_hex<S: AsRef<str>>(text: S, hex: &str) -> ColoredString {
match hex_to_rgb(hex) {
Ok((r, g, b)) => text.as_ref().truecolor(r, g, b),
Err(_) => text.as_ref().normal(),
}
}
pub fn bg_hex<S: AsRef<str>>(text: S, hex: &str) -> ColoredString {
match hex_to_rgb(hex) {
Ok((r, g, b)) => text.as_ref().on_truecolor(r, g, b),
Err(_) => text.as_ref().normal(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Theme {
pub title: String,
pub heading: String,
pub subheading: String,
pub body: String,
pub link: String,
pub bold: String,
pub italic: String,
pub code: String,
pub result_title: String,
pub result_snippet: String,
pub result_index: String,
pub separator: String,
pub error: String,
pub success: String,
pub dim: String,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub fn dark() -> Self {
Self {
title: "#61AFEF".into(),
heading: "#E5C07B".into(),
subheading: "#98C379".into(),
body: "#ABB2BF".into(),
link: "#56B6C2".into(),
bold: "#E06C75".into(),
italic: "#C678DD".into(),
code: "#282C34".into(),
result_title: "#61AFEF".into(),
result_snippet: "#ABB2BF".into(),
result_index: "#5C6370".into(),
separator: "#3E4451".into(),
error: "#E06C75".into(),
success: "#98C379".into(),
dim: "#5C6370".into(),
}
}
pub fn light() -> Self {
Self {
title: "#005F87".into(),
heading: "#AF5F00".into(),
subheading: "#008700".into(),
body: "#1C1C1C".into(),
link: "#0087AF".into(),
bold: "#AF0000".into(),
italic: "#875FAF".into(),
code: "#EEEEEE".into(),
result_title: "#005F87".into(),
result_snippet: "#4E4E4E".into(),
result_index: "#9E9E9E".into(),
separator: "#BCBCBC".into(),
error: "#AF0000".into(),
success: "#008700".into(),
dim: "#9E9E9E".into(),
}
}
pub fn solarized() -> Self {
Self {
title: "#268BD2".into(),
heading: "#B58900".into(),
subheading: "#2AA198".into(),
body: "#839496".into(),
link: "#2AA198".into(),
bold: "#DC322F".into(),
italic: "#D33682".into(),
code: "#073642".into(),
result_title: "#268BD2".into(),
result_snippet: "#657B83".into(),
result_index: "#586E75".into(),
separator: "#073642".into(),
error: "#DC322F".into(),
success: "#859900".into(),
dim: "#586E75".into(),
}
}
pub fn nord() -> Self {
Self {
title: "#88C0D0".into(),
heading: "#EBCB8B".into(),
subheading: "#A3BE8C".into(),
body: "#D8DEE9".into(),
link: "#81A1C1".into(),
bold: "#BF616A".into(),
italic: "#B48EAD".into(),
code: "#2E3440".into(),
result_title: "#88C0D0".into(),
result_snippet: "#D8DEE9".into(),
result_index: "#4C566A".into(),
separator: "#3B4252".into(),
error: "#BF616A".into(),
success: "#A3BE8C".into(),
dim: "#4C566A".into(),
}
}
pub fn dracula() -> Self {
Self {
title: "#8BE9FD".into(),
heading: "#F1FA8C".into(),
subheading: "#50FA7B".into(),
body: "#F8F8F2".into(),
link: "#8BE9FD".into(),
bold: "#FF5555".into(),
italic: "#FF79C6".into(),
code: "#282A36".into(),
result_title: "#BD93F9".into(),
result_snippet: "#F8F8F2".into(),
result_index: "#6272A4".into(),
separator: "#44475A".into(),
error: "#FF5555".into(),
success: "#50FA7B".into(),
dim: "#6272A4".into(),
}
}
pub fn by_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"dark" => Some(Self::dark()),
"light" => Some(Self::light()),
"solarized" => Some(Self::solarized()),
"nord" => Some(Self::nord()),
"dracula" => Some(Self::dracula()),
_ => None,
}
}
pub fn title<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.title).bold()
}
pub fn heading<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.heading).bold()
}
pub fn subheading<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.subheading)
}
pub fn body<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.body)
}
pub fn link<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.link).underline()
}
pub fn bold_text<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.bold).bold()
}
pub fn italic_text<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.italic).italic()
}
pub fn code_text<S: AsRef<str>>(&self, s: S) -> ColoredString {
match (hex_to_rgb(&self.body), hex_to_rgb(&self.code)) {
(Ok((fr, fg, fb)), Ok((br, bg, bb))) => {
s.as_ref().truecolor(fr, fg, fb).on_truecolor(br, bg, bb)
}
_ => s.as_ref().normal(),
}
}
pub fn result_title<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.result_title).bold()
}
pub fn result_snippet<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.result_snippet)
}
pub fn result_index<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.result_index)
}
pub fn separator<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.separator)
}
pub fn error<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.error).bold()
}
pub fn success<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.success)
}
pub fn dim<S: AsRef<str>>(&self, s: S) -> ColoredString {
colorize_hex(s, &self.dim)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_to_rgb_6_digit() {
assert_eq!(hex_to_rgb("#ff6600").unwrap(), (255, 102, 0));
assert_eq!(hex_to_rgb("ff6600").unwrap(), (255, 102, 0));
}
#[test]
fn test_hex_to_rgb_3_digit() {
assert_eq!(hex_to_rgb("#f60").unwrap(), (255, 102, 0));
}
#[test]
fn test_hex_invalid() {
assert!(hex_to_rgb("#gg0000").is_err());
assert!(hex_to_rgb("#12345").is_err());
}
#[test]
fn test_theme_by_name() {
assert!(Theme::by_name("dark").is_some());
assert!(Theme::by_name("NORD").is_some());
assert!(Theme::by_name("unknown").is_none());
}
}