tastty-driver 0.1.0

Terminal automation driver built on tastty
use tastty::Color;

/// Error returned when parsing the driver color DSL fails.
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error("invalid color spec '{input}'")]
pub struct ParseColorError {
    input: String,
}

impl ParseColorError {
    fn new(input: &str) -> Self {
        Self {
            input: input.to_string(),
        }
    }
}

/// Parse a terminal color.
///
/// Accepted forms are `default`, ANSI names, `bright_<name>`, `bright<name>`,
/// `color(N)`, `rgb(R,G,B)`, and `#rrggbb`.
///
/// # Errors
///
/// Returns [`ParseColorError`] when `input` does not match any
/// accepted form: an unrecognized name, a `color(N)` body that is not
/// a decimal `u8`, an `rgb(R,G,B)` body with the wrong arity or a
/// non-`u8` component, or a hex form that is not exactly six hex
/// digits after `#`.
///
/// # References
///
/// - [ANSI escape codes][ansi-escape]: the 16 standard color names and the
///   `bright_*` palette extension recognized by the named-color branch.
///
/// [ansi-escape]: https://en.wikipedia.org/wiki/ANSI_escape_code
pub fn parse_color(input: &str) -> Result<Color, ParseColorError> {
    let lower = input.to_ascii_lowercase();
    match lower.as_str() {
        "default" => return Ok(Color::Default),
        "black" => return Ok(Color::Index(0)),
        "red" => return Ok(Color::Index(1)),
        "green" => return Ok(Color::Index(2)),
        "yellow" => return Ok(Color::Index(3)),
        "blue" => return Ok(Color::Index(4)),
        "magenta" => return Ok(Color::Index(5)),
        "cyan" => return Ok(Color::Index(6)),
        "white" => return Ok(Color::Index(7)),
        "bright_black" | "brightblack" => return Ok(Color::Index(8)),
        "bright_red" | "brightred" => return Ok(Color::Index(9)),
        "bright_green" | "brightgreen" => return Ok(Color::Index(10)),
        "bright_yellow" | "brightyellow" => return Ok(Color::Index(11)),
        "bright_blue" | "brightblue" => return Ok(Color::Index(12)),
        "bright_magenta" | "brightmagenta" => return Ok(Color::Index(13)),
        "bright_cyan" | "brightcyan" => return Ok(Color::Index(14)),
        "bright_white" | "brightwhite" => return Ok(Color::Index(15)),
        _ => {}
    }

    if let Some(inner) = lower
        .strip_prefix("color(")
        .and_then(|s| s.strip_suffix(')'))
    {
        return inner
            .parse::<u8>()
            .map(Color::Index)
            .map_err(|_err| ParseColorError::new(input));
    }

    if let Some(inner) = lower.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
        let mut parts = inner.split(',').map(str::trim);
        let r = parse_rgb_part(parts.next(), input)?;
        let g = parse_rgb_part(parts.next(), input)?;
        let b = parse_rgb_part(parts.next(), input)?;
        if parts.next().is_some() {
            return Err(ParseColorError::new(input));
        }
        return Ok(Color::Rgb(r, g, b));
    }

    if let Some(hex) = lower.strip_prefix('#')
        && hex.len() == 6
    {
        let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_err| ParseColorError::new(input))?;
        let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_err| ParseColorError::new(input))?;
        let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_err| ParseColorError::new(input))?;
        return Ok(Color::Rgb(r, g, b));
    }

    Err(ParseColorError::new(input))
}

fn parse_rgb_part(part: Option<&str>, input: &str) -> Result<u8, ParseColorError> {
    part.ok_or_else(|| ParseColorError::new(input))?
        .parse()
        .map_err(|_err| ParseColorError::new(input))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_named_index_and_rgb_colors() {
        assert_eq!(parse_color("red").unwrap(), Color::Index(1));
        assert_eq!(parse_color("bright_blue").unwrap(), Color::Index(12));
        assert_eq!(parse_color("color(196)").unwrap(), Color::Index(196));
        assert_eq!(
            parse_color("rgb(255,0,16)").unwrap(),
            Color::Rgb(255, 0, 16)
        );
        assert_eq!(parse_color("#ff0010").unwrap(), Color::Rgb(255, 0, 16));
    }

    #[test]
    fn rejects_invalid_colors() {
        parse_color("rgb(1,2)").unwrap_err();
        parse_color("color(999)").unwrap_err();
        parse_color("#xyzxyz").unwrap_err();
    }
}