use crate::error::{Logger2Error, Result};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HexColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl HexColor {
pub fn new(r: u8, g: u8, b: u8) -> Self {
HexColor { r, g, b }
}
pub fn ansi_fg(self) -> String {
format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
}
pub const RESET: &'static str = "\x1b[0m";
pub fn paint(self, text: &str) -> String {
format!("{}{}{}", self.ansi_fg(), text, Self::RESET)
}
}
impl fmt::Display for HexColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
}
}
impl std::str::FromStr for HexColor {
type Err = Logger2Error;
fn from_str(s: &str) -> Result<Self> {
parse_hex_color(s)
}
}
pub fn parse_hex_color(input: &str) -> Result<HexColor> {
let s = input.trim();
let hex = s.strip_prefix('#').unwrap_or(s);
let expand = |c: char| -> Result<u8> {
u8::from_str_radix(&c.to_string().repeat(2), 16)
.map_err(|_| Logger2Error::InvalidColor(input.to_string()))
};
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| Logger2Error::InvalidColor(input.to_string()))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| Logger2Error::InvalidColor(input.to_string()))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| Logger2Error::InvalidColor(input.to_string()))?;
Ok(HexColor::new(r, g, b))
}
3 => {
let chars: Vec<char> = hex.chars().collect();
let r = expand(chars[0])?;
let g = expand(chars[1])?;
let b = expand(chars[2])?;
Ok(HexColor::new(r, g, b))
}
_ => Err(Logger2Error::InvalidColor(input.to_string())),
}
}
impl Serialize for HexColor {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for HexColor {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_hex_color(&s).map_err(serde::de::Error::custom)
}
}
pub fn should_use_color(force: Option<bool>, is_tty: bool) -> bool {
if let Some(v) = force {
return v;
}
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if std::env::var_os("CLICOLOR_FORCE").is_some() {
return true;
}
is_tty
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_long_form() {
let c = parse_hex_color("#00FFFF").unwrap();
assert_eq!(c, HexColor::new(0, 255, 255));
}
#[test]
fn parses_short_form() {
let c = parse_hex_color("#0FF").unwrap();
assert_eq!(c, HexColor::new(0, 255, 255));
}
#[test]
fn parses_without_hash() {
let c = parse_hex_color("FF0000").unwrap();
assert_eq!(c, HexColor::new(255, 0, 0));
}
#[test]
fn rejects_invalid() {
assert!(parse_hex_color("#GGGGGG").is_err());
assert!(parse_hex_color("#12345").is_err());
assert!(parse_hex_color("").is_err());
}
#[test]
fn display_roundtrip() {
let c = HexColor::new(0, 255, 255);
assert_eq!(c.to_string(), "#00FFFF");
}
}