tastty-core 0.1.0

Sans-IO core of the tastty terminal session library: VT parser, screen buffer, and byte encoders.
pub(crate) const PALETTE_SIZE: usize = 256;
const BASE16_COLORS: usize = 16;
const COLOR_CUBE_SIZE: usize = 216;
const COLOR_CUBE_LEVELS: usize = 6;
const COLOR_CUBE_START: usize = BASE16_COLORS;
const COLOR_CUBE_BASE: usize = 55;
const COLOR_CUBE_STEP: usize = 40;
const GRAYSCALE_RAMP_SIZE: usize = 24;
const GRAYSCALE_START: usize = COLOR_CUBE_START + COLOR_CUBE_SIZE;
const GRAYSCALE_BASE: usize = 8;
const GRAYSCALE_STEP: usize = 10;

const BASE16: [(u8, u8, u8); BASE16_COLORS] = [
    (0, 0, 0),       // 0  black
    (205, 0, 0),     // 1  red
    (0, 205, 0),     // 2  green
    (205, 205, 0),   // 3  yellow
    (0, 0, 238),     // 4  blue
    (205, 0, 205),   // 5  magenta
    (0, 205, 205),   // 6  cyan
    (229, 229, 229), // 7  white
    (127, 127, 127), // 8  bright black
    (255, 0, 0),     // 9  bright red
    (0, 255, 0),     // 10 bright green
    (255, 255, 0),   // 11 bright yellow
    (92, 92, 255),   // 12 bright blue
    (255, 0, 255),   // 13 bright magenta
    (0, 255, 255),   // 14 bright cyan
    (255, 255, 255), // 15 bright white
];

pub(crate) fn default_xterm_palette() -> Box<[(u8, u8, u8); PALETTE_SIZE]> {
    let mut p = [(0u8, 0u8, 0u8); PALETTE_SIZE];
    p[..BASE16_COLORS].copy_from_slice(&BASE16);
    for i in 0..COLOR_CUBE_SIZE {
        let r = i / (COLOR_CUBE_LEVELS * COLOR_CUBE_LEVELS);
        let g = (i / COLOR_CUBE_LEVELS) % COLOR_CUBE_LEVELS;
        let b = i % COLOR_CUBE_LEVELS;
        let to_val = |v: usize| {
            if v == 0 {
                0u8
            } else {
                (COLOR_CUBE_BASE + COLOR_CUBE_STEP * v) as u8
            }
        };
        p[COLOR_CUBE_START + i] = (to_val(r), to_val(g), to_val(b));
    }
    for i in 0..GRAYSCALE_RAMP_SIZE {
        let v = (GRAYSCALE_BASE + GRAYSCALE_STEP * i) as u8;
        p[GRAYSCALE_START + i] = (v, v, v);
    }
    Box::new(p)
}

/// Compute the default xterm color for a single palette index.
pub(crate) fn default_xterm_color(idx: usize) -> (u8, u8, u8) {
    if idx < 16 {
        BASE16[idx]
    } else if idx < 232 {
        let i = idx - 16;
        let r = i / 36;
        let g = (i / 6) % 6;
        let b = i % 6;
        let to_val = |v: usize| -> u8 { if v == 0 { 0 } else { (55 + 40 * v) as u8 } };
        (to_val(r), to_val(g), to_val(b))
    } else {
        let v = (8 + 10 * (idx - 232)) as u8;
        (v, v, v)
    }
}

/// Parse an X11 color specification into (r, g, b).
/// Supports `rgb:R/G/B` (1-4 hex digits per channel) and `#RRGGBB`.
pub(crate) fn parse_x11_color(bytes: &[u8]) -> Option<(u8, u8, u8)> {
    let s = std::str::from_utf8(bytes).ok()?;
    if let Some(rest) = s.strip_prefix("rgb:") {
        let parts: Vec<&str> = rest.split('/').collect();
        if parts.len() != 3 {
            return None;
        }
        let scale = |v: u16, len: usize| -> u8 {
            match len {
                1 => (v * 17) as u8,
                2 => v as u8,
                3 => (v >> 4) as u8,
                4 => (v >> 8) as u8,
                _ => v as u8,
            }
        };
        let r = u16::from_str_radix(parts[0], 16).ok()?;
        let g = u16::from_str_radix(parts[1], 16).ok()?;
        let b = u16::from_str_radix(parts[2], 16).ok()?;
        Some((
            scale(r, parts[0].len()),
            scale(g, parts[1].len()),
            scale(b, parts[2].len()),
        ))
    } else if let Some(hex) = s.strip_prefix('#') {
        if hex.len() == 6 {
            let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
            let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
            let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
            Some((r, g, b))
        } else {
            None
        }
    } else {
        None
    }
}

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

    #[test]
    fn parse_x11_color_rgb_2digit() {
        assert_eq!(parse_x11_color(b"rgb:ff/00/80"), Some((255, 0, 128)));
    }

    #[test]
    fn parse_x11_color_rgb_4digit() {
        assert_eq!(parse_x11_color(b"rgb:ffff/0000/8080"), Some((255, 0, 128)));
    }

    #[test]
    fn parse_x11_color_hex() {
        assert_eq!(parse_x11_color(b"#ff0080"), Some((255, 0, 128)));
    }

    #[test]
    fn parse_x11_color_rgb_1digit() {
        assert_eq!(parse_x11_color(b"rgb:f/0/8"), Some((255, 0, 136)));
    }

    #[test]
    fn parse_x11_color_invalid() {
        assert_eq!(parse_x11_color(b"not a color"), None);
        assert_eq!(parse_x11_color(b"#fff"), None);
    }
}