use core::fmt; use std::{ fmt::Display, ops::{Deref, DerefMut}, }; use nu_ansi_term::Color; use schemars::JsonSchema; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, Serialize, }; use tracing::trace; use validator::Validate; #[derive(Debug, Clone, Serialize)] pub struct LimboColor(pub Color); impl TryFrom<&str> for LimboColor { type Error = String; fn try_from(value: &str) -> Result { // Parse RGB hex values trace!("Parsing color_string: {}", value); let color = match value.chars().collect::>()[..] { // #rrggbb hex color ['#', r1, r2, g1, g2, b1, b2] => { let r = u8::from_str_radix(&format!("{r1}{r2}"), 16).map_err(|e| e.to_string())?; let g = u8::from_str_radix(&format!("{g1}{g2}"), 16).map_err(|e| e.to_string())?; let b = u8::from_str_radix(&format!("{b1}{b2}"), 16).map_err(|e| e.to_string())?; Some(Color::Rgb(r, g, b)) } // #rgb shorthand hex color ['#', r, g, b] => { let r = u8::from_str_radix(&format!("{r}{r}"), 16).map_err(|e| e.to_string())?; let g = u8::from_str_radix(&format!("{g}{g}"), 16).map_err(|e| e.to_string())?; let b = u8::from_str_radix(&format!("{b}{b}"), 16).map_err(|e| e.to_string())?; Some(Color::Rgb(r, g, b)) } // 0-255 color code [c1, c2, c3] => { if let Ok(ansi_color_num) = str::parse::(&format!("{c1}{c2}{c3}")) { Some(Color::Fixed(ansi_color_num)) } else { None } } [c1, c2] => { if let Ok(ansi_color_num) = str::parse::(&format!("{c1}{c2}")) { Some(Color::Fixed(ansi_color_num)) } else { None } } [c1] => { if let Ok(ansi_color_num) = str::parse::(&format!("{c1}")) { Some(Color::Fixed(ansi_color_num)) } else { None } } // unknown format _ => None, }; if let Some(color) = color { return Ok(LimboColor(color)); } // Check for any predefined color strings // There are no predefined enums for bright colors, so we use Color::Fixed let predefined_color = match value.to_lowercase().as_str() { "black" => Color::Black, "red" => Color::Red, "green" => Color::Green, "yellow" => Color::Yellow, "blue" => Color::Blue, "purple" => Color::Purple, "cyan" => Color::Cyan, "magenta" => Color::Magenta, "white" => Color::White, "bright-black" => Color::DarkGray, // "bright-black" is dark grey "bright-red" => Color::LightRed, "bright-green" => Color::LightGreen, "bright-yellow" => Color::LightYellow, "bright-blue" => Color::LightBlue, "bright-cyan" => Color::LightCyan, "birght-magenta" => Color::LightMagenta, "bright-white" => Color::LightGray, "dark-red" => Color::Fixed(1), "dark-green" => Color::Fixed(2), "dark-yellow" => Color::Fixed(3), "dark-blue" => Color::Fixed(4), "dark-magenta" => Color::Fixed(5), "dark-cyan" => Color::Fixed(6), "grey" => Color::Fixed(7), "dark-grey" => Color::Fixed(8), _ => return Err(format!("Could not parse color in string: {value}")), }; trace!("Read predefined color: {}", value); Ok(LimboColor(predefined_color)) } } impl Display for LimboColor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let val = match self.0 { Color::Black => "black".to_string(), Color::Red => "red".to_string(), Color::Green => "green".to_string(), Color::Yellow => "yellow".to_string(), Color::Blue => "blue".to_string(), Color::Purple => "purple".to_string(), Color::Cyan => "cyan".to_string(), Color::Magenta => "magenta".to_string(), Color::White => "white".to_string(), Color::DarkGray => "bright-black".to_string(), // "bright-black" is dark grey Color::LightRed => "bright-red".to_string(), Color::LightGreen => "bright-green".to_string(), Color::LightYellow => "bright-yellow".to_string(), Color::LightBlue => "bright-blue".to_string(), Color::LightCyan => "bright-cyan".to_string(), Color::LightMagenta | Color::LightPurple => "bright-magenta".to_string(), Color::LightGray => "bright-white".to_string(), Color::Fixed(1) => "dark-red".to_string(), Color::Fixed(2) => "dark-green".to_string(), Color::Fixed(3) => "dark-yellow".to_string(), Color::Fixed(4) => "dark-blue".to_string(), Color::Fixed(5) => "dark-magenta".to_string(), Color::Fixed(6) => "dark-cyan".to_string(), Color::Fixed(7) => "grey".to_string(), Color::Fixed(8) => "dark-grey".to_string(), Color::Rgb(r, g, b) => format!("#{r:x}{g:x}{b:X}"), Color::Fixed(ansi_color_num) => format!("{ansi_color_num}"), Color::Default => unreachable!(), }; write!(f, "{val}") } } impl From for LimboColor { fn from(value: comfy_table::Color) -> Self { let color = match value { comfy_table::Color::Rgb { r, g, b } => Color::Rgb(r, g, b), comfy_table::Color::AnsiValue(ansi_color_num) => Color::Fixed(ansi_color_num), comfy_table::Color::Black => Color::Black, comfy_table::Color::Red => Color::Red, comfy_table::Color::Green => Color::Green, comfy_table::Color::Yellow => Color::Yellow, comfy_table::Color::Blue => Color::Blue, comfy_table::Color::Cyan => Color::Cyan, comfy_table::Color::Magenta => Color::Magenta, comfy_table::Color::White => Color::White, comfy_table::Color::DarkRed => Color::Fixed(1), comfy_table::Color::DarkGreen => Color::Fixed(2), comfy_table::Color::DarkYellow => Color::Fixed(3), comfy_table::Color::DarkBlue => Color::Fixed(4), comfy_table::Color::DarkMagenta => Color::Fixed(5), comfy_table::Color::DarkCyan => Color::Fixed(6), comfy_table::Color::Grey => Color::Fixed(7), comfy_table::Color::DarkGrey => Color::Fixed(8), comfy_table::Color::Reset => unreachable!(), // Should never have Reset Color here }; LimboColor(color) } } impl<'de> Deserialize<'de> for LimboColor { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct LimboColorVisitor; impl Visitor<'_> for LimboColorVisitor { type Value = LimboColor; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("struct LimboColor") } fn visit_str(self, v: &str) -> Result where E: de::Error, { LimboColor::try_from(v).map_err(de::Error::custom) } } deserializer.deserialize_str(LimboColorVisitor) } } impl JsonSchema for LimboColor { fn schema_name() -> String { "LimboColor".into() } fn schema_id() -> std::borrow::Cow<'static, str> { // Include the module, in case a type with the same name is in another module/crate std::borrow::Cow::Borrowed(concat!(module_path!(), "::LimboColor")) } fn json_schema(generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { generator.subschema_for::() } } impl Deref for LimboColor { type Target = Color; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for LimboColor { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Validate for LimboColor { fn validate(&self) -> Result<(), validator::ValidationErrors> { Ok(()) } } impl LimboColor { pub fn as_comfy_table_color(&self) -> comfy_table::Color { match self.0 { Color::Black => comfy_table::Color::Black, Color::Red => comfy_table::Color::Red, Color::Green => comfy_table::Color::Green, Color::Yellow => comfy_table::Color::Yellow, Color::Blue => comfy_table::Color::Blue, Color::Magenta | Color::Purple => comfy_table::Color::Magenta, Color::Cyan => comfy_table::Color::Cyan, Color::White | Color::Default => comfy_table::Color::White, Color::Fixed(1) => comfy_table::Color::DarkRed, Color::Fixed(2) => comfy_table::Color::DarkGreen, Color::Fixed(3) => comfy_table::Color::DarkYellow, Color::Fixed(4) => comfy_table::Color::DarkBlue, Color::Fixed(5) => comfy_table::Color::DarkMagenta, Color::Fixed(6) => comfy_table::Color::DarkCyan, Color::Fixed(7) => comfy_table::Color::Grey, Color::Fixed(8) => comfy_table::Color::DarkGrey, Color::DarkGray => comfy_table::Color::AnsiValue(241), Color::LightRed => comfy_table::Color::AnsiValue(9), Color::LightGreen => comfy_table::Color::AnsiValue(10), Color::LightYellow => comfy_table::Color::AnsiValue(11), Color::LightBlue => comfy_table::Color::AnsiValue(12), Color::LightMagenta | Color::LightPurple => comfy_table::Color::AnsiValue(13), Color::LightCyan => comfy_table::Color::AnsiValue(14), Color::LightGray => comfy_table::Color::AnsiValue(15), Color::Rgb(r, g, b) => comfy_table::Color::Rgb { r, g, b }, Color::Fixed(ansi_color_num) => comfy_table::Color::AnsiValue(ansi_color_num), } } }