unicode-plot 0.1.0

unicode-plot-rs: Unicode terminal plotting library for Rust
Documentation
use std::ops::BitOr;

/// A 3-bit canvas-level color index used for pixel compositing.
///
/// Values range from 0 (`NORMAL`) to 7 (`WHITE`). Colors compose additively
/// via [`BitOr`]: for example, `BLUE | RED` yields `MAGENTA`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct CanvasColor(u8);

impl CanvasColor {
    /// No color (default foreground).
    pub const NORMAL: Self = Self(0);
    /// Blue (bit 0).
    pub const BLUE: Self = Self(1);
    /// Red (bit 1).
    pub const RED: Self = Self(2);
    /// Magenta (blue | red).
    pub const MAGENTA: Self = Self(3);
    /// Green (bit 2).
    pub const GREEN: Self = Self(4);
    /// Cyan (blue | green).
    pub const CYAN: Self = Self(5);
    /// Yellow (red | green).
    pub const YELLOW: Self = Self(6);
    /// White (all bits set).
    pub const WHITE: Self = Self(7);

    /// Creates a canvas color from a raw index. Returns `None` if `value > 7`.
    #[must_use]
    pub const fn new(value: u8) -> Option<Self> {
        if value <= Self::WHITE.0 {
            Some(Self(value))
        } else {
            None
        }
    }

    /// Returns the underlying `u8` index.
    #[must_use]
    pub const fn as_u8(self) -> u8 {
        self.0
    }
}

impl Default for CanvasColor {
    fn default() -> Self {
        Self::NORMAL
    }
}

impl BitOr for CanvasColor {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        Self((self.0 | rhs.0) & Self::WHITE.0)
    }
}

/// Terminal color specification for plot labels, annotations, and series.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TermColor {
    /// One of the standard 16 named ANSI colors.
    Named(NamedColor),
    /// An extended 256-color palette index.
    Ansi256(u8),
    /// A 24-bit true-color RGB value.
    Rgb(u8, u8, u8),
}

/// Standard named terminal colors, including the eight bright/light variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum NamedColor {
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    White,
    /// Bright black (ANSI 90). Alias: `Gray`.
    LightBlack,
    /// Same as `LightBlack` (ANSI 90).
    Gray,
    LightRed,
    LightGreen,
    LightYellow,
    LightBlue,
    LightMagenta,
    LightCyan,
}

/// Controls whether ANSI color escapes are emitted during rendering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ColorMode {
    /// Emit color only when the output writer is a terminal.
    #[default]
    Auto,
    /// Always emit ANSI color escapes.
    Always,
    /// Never emit ANSI color escapes; produce plain text.
    Never,
}

/// The default series color cycle used by [`Plot::next_color`](crate::Plot::next_color).
///
/// Matches the Ruby `UnicodePlot` auto-color sequence: green, blue, red,
/// magenta, yellow, cyan.
/// Maps a [`TermColor`] to the closest [`CanvasColor`] for pixel compositing.
///
/// Only the seven primary named colors have direct canvas equivalents. All
/// other terminal colors (bright variants, 256-color, RGB) map to
/// [`CanvasColor::NORMAL`].
#[must_use]
pub(crate) fn canvas_color_from_term(color: TermColor) -> CanvasColor {
    match color {
        TermColor::Named(NamedColor::Blue) => CanvasColor::BLUE,
        TermColor::Named(NamedColor::Red) => CanvasColor::RED,
        TermColor::Named(NamedColor::Magenta) => CanvasColor::MAGENTA,
        TermColor::Named(NamedColor::Green) => CanvasColor::GREEN,
        TermColor::Named(NamedColor::Cyan) => CanvasColor::CYAN,
        TermColor::Named(NamedColor::Yellow) => CanvasColor::YELLOW,
        TermColor::Named(NamedColor::White) => CanvasColor::WHITE,
        _ => CanvasColor::NORMAL,
    }
}

/// The default series color cycle used by [`Plot::next_color`](crate::Plot::next_color).
///
/// Matches the Ruby `UnicodePlot` auto-color sequence: green, blue, red,
/// magenta, yellow, cyan.
pub const AUTO_SERIES_COLORS: [NamedColor; 6] = [
    NamedColor::Green,
    NamedColor::Blue,
    NamedColor::Red,
    NamedColor::Magenta,
    NamedColor::Yellow,
    NamedColor::Cyan,
];

#[cfg(test)]
mod tests {
    use super::{AUTO_SERIES_COLORS, CanvasColor, NamedColor};

    #[test]
    fn canvas_color_additive_blending_matches_bitwise_or() {
        for lhs in 0_u8..=7 {
            for rhs in 0_u8..=7 {
                let left = CanvasColor::new(lhs);
                let right = CanvasColor::new(rhs);

                assert!(left.is_some() && right.is_some(), "lhs={lhs} rhs={rhs}");

                if let (Some(left), Some(right)) = (left, right) {
                    let blended = left | right;
                    assert_eq!(blended.as_u8(), lhs | rhs, "lhs={lhs} rhs={rhs}");
                }
            }
        }
    }

    #[test]
    fn canvas_color_constants_match_reference_indices() {
        assert_eq!(CanvasColor::NORMAL.as_u8(), 0);
        assert_eq!(CanvasColor::BLUE.as_u8(), 1);
        assert_eq!(CanvasColor::RED.as_u8(), 2);
        assert_eq!(CanvasColor::MAGENTA.as_u8(), 3);
        assert_eq!(CanvasColor::GREEN.as_u8(), 4);
        assert_eq!(CanvasColor::CYAN.as_u8(), 5);
        assert_eq!(CanvasColor::YELLOW.as_u8(), 6);
        assert_eq!(CanvasColor::WHITE.as_u8(), 7);
    }

    #[test]
    fn canvas_color_new_rejects_out_of_range_values() {
        assert_eq!(CanvasColor::new(8), None);
        assert_eq!(CanvasColor::new(u8::MAX), None);
    }

    #[test]
    fn canvas_color_semantic_blending_examples() {
        assert_eq!(CanvasColor::BLUE | CanvasColor::RED, CanvasColor::MAGENTA);
        assert_eq!(CanvasColor::NORMAL | CanvasColor::GREEN, CanvasColor::GREEN);
    }

    #[test]
    fn auto_series_colors_match_reference_cycle() {
        assert_eq!(
            AUTO_SERIES_COLORS,
            [
                NamedColor::Green,
                NamedColor::Blue,
                NamedColor::Red,
                NamedColor::Magenta,
                NamedColor::Yellow,
                NamedColor::Cyan,
            ]
        );
    }
}