logger2 0.1.5

A modern, feature-rich replacement for the classic `logger` CLI: syslog (local/remote, RFC3164 & RFC5424), truecolor hex output, emoji, JSON, and a fully configurable TOML config file.
Documentation
use crate::error::{Logger2Error, Result};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;

/// A truecolor RGB value, typically parsed from a hex string like `#00FFFF`
/// or the shorthand `#0FF`.
#[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 }
    }

    /// Returns the ANSI truecolor foreground escape sequence for this color.
    pub fn ansi_fg(self) -> String {
        format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
    }

    pub const RESET: &'static str = "\x1b[0m";

    /// Wraps `text` in this color's escape sequence, followed by a reset.
    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)
    }
}

/// Parses `#RRGGBB` or `#RGB` (with or without the leading `#`) into a
/// [`HexColor`]. Case-insensitive.
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)
    }
}

/// Detects whether the given stream should render ANSI color, honoring
/// `NO_COLOR`, `CLICOLOR_FORCE`, and TTY status.
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");
    }
}