tv 0.1.1

Terminal User Interface library
Documentation
/// Packed 8-byte cell (u64) following rio's approach.
///
/// Bit layout:
///   bits  0..20 (21): codepoint (Unicode scalar value, max 0x10_FFFF)
///   bits 21..28  (8): attr flags (Attr bitflags)
///   bits 29..39 (11): fg color id (compact color encoding)
///   bits 40..50 (11): bg color id (compact color encoding)
///   bits 51..63 (13): reserved
///
/// Color IDs are mapped via a ColorTable:
///   0       = Reset
///   1..16   = Named colors (Black..BrightWhite)
///   17..272 = Ansi256(0..255)
///   273+    = RGB from palette
use crate::attr::Attr;
use crate::color::Color;
use std::collections::HashMap;

const CP_MASK: u64 = (1 << 21) - 1;
const ATTR_SHIFT: u64 = 21;
const ATTR_MASK: u64 = 0xFF << ATTR_SHIFT;
const FG_SHIFT: u64 = 29;
const FG_MASK: u64 = 0x7FF << FG_SHIFT;
const BG_SHIFT: u64 = 40;
const BG_MASK: u64 = 0x7FF << BG_SHIFT;

#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cell(u64);

impl Default for Cell {
    #[inline]
    fn default() -> Self {
        Self::blank()
    }
}

impl Cell {
    pub const BLANK: Cell = Cell(b' ' as u64);

    #[inline]
    pub fn blank() -> Self {
        Self::BLANK
    }

    #[inline]
    pub fn pack(ch: char, attr: Attr, fg_id: u16, bg_id: u16) -> Self {
        let cp = ch as u32 as u64;
        let a = (attr.bits() as u64) << ATTR_SHIFT;
        let f = (fg_id as u64) << FG_SHIFT;
        let b = (bg_id as u64) << BG_SHIFT;
        Cell(cp | a | f | b)
    }

    #[inline]
    pub fn ch(self) -> char {
        let cp = (self.0 & CP_MASK) as u32;
        char::from_u32(cp).unwrap_or('\0')
    }

    #[inline]
    pub fn attr(self) -> Attr {
        let bits = ((self.0 & ATTR_MASK) >> ATTR_SHIFT) as u16;
        Attr(bits)
    }

    #[inline]
    pub fn fg_id(self) -> u16 {
        ((self.0 & FG_MASK) >> FG_SHIFT) as u16
    }

    #[inline]
    pub fn bg_id(self) -> u16 {
        ((self.0 & BG_MASK) >> BG_SHIFT) as u16
    }

    #[inline]
    pub fn is_blank(self) -> bool {
        self.0 == Self::BLANK.0
    }

    #[inline]
    pub fn raw(self) -> u64 {
        self.0
    }
}

/// Maps between `Color` and compact 11-bit color IDs stored in cells.
pub struct ColorTable {
    rgb_to_id: HashMap<(u8, u8, u8), u16>,
    id_to_rgb: Vec<(u8, u8, u8)>,
}

const RGB_BASE: u16 = 273;

impl ColorTable {
    pub fn new() -> Self {
        Self {
            rgb_to_id: HashMap::new(),
            id_to_rgb: Vec::new(),
        }
    }

    #[inline]
    pub fn color_to_id(&mut self, color: Color) -> u16 {
        use crate::color::NamedColor::*;
        match color {
            Color::Named(c) => match c {
                Black => 1,
                Red => 2,
                Green => 3,
                Yellow => 4,
                Blue => 5,
                Magenta => 6,
                Cyan => 7,
                White => 8,
                LightBlack => 9,
                LightRed => 10,
                LightGreen => 11,
                LightYellow => 12,
                LightBlue => 13,
                LightMagenta => 14,
                LightCyan => 15,
                LightWhite => 16,
                // Semantic/dim colors map to their base
                Foreground | Background | Cursor | LightForeground | DimForeground => 0,
                DimBlack => 1,
                DimRed => 2,
                DimGreen => 3,
                DimYellow => 4,
                DimBlue => 5,
                DimMagenta => 6,
                DimCyan => 7,
                DimWhite => 8,
            },
            Color::Indexed(idx) => 17 + idx as u16,
            Color::Spec(rgb) => {
                if let Some(&id) = self.rgb_to_id.get(&(rgb.r, rgb.g, rgb.b)) {
                    id
                } else {
                    let id = RGB_BASE + self.id_to_rgb.len() as u16;
                    self.rgb_to_id.insert((rgb.r, rgb.g, rgb.b), id);
                    self.id_to_rgb.push((rgb.r, rgb.g, rgb.b));
                    id
                }
            }
        }
    }

    #[inline]
    pub fn id_to_color(&self, id: u16) -> Color {
        use crate::color::NamedColor::*;
        match id {
            0 => Color::Named(Foreground),
            1 => Color::Named(Black),
            2 => Color::Named(Red),
            3 => Color::Named(Green),
            4 => Color::Named(Yellow),
            5 => Color::Named(Blue),
            6 => Color::Named(Magenta),
            7 => Color::Named(Cyan),
            8 => Color::Named(White),
            9 => Color::Named(LightBlack),
            10 => Color::Named(LightRed),
            11 => Color::Named(LightGreen),
            12 => Color::Named(LightYellow),
            13 => Color::Named(LightBlue),
            14 => Color::Named(LightMagenta),
            15 => Color::Named(LightCyan),
            16 => Color::Named(LightWhite),
            17..=272 => Color::Indexed((id - 17) as u8),
            _ => {
                let idx = (id - RGB_BASE) as usize;
                let (r, g, b) = self.id_to_rgb[idx];
                Color::rgb(r, g, b)
            }
        }
    }
}

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

    #[test]
    fn cell_is_eight_bytes() {
        assert_eq!(std::mem::size_of::<Cell>(), 8);
    }

    #[test]
    fn codepoint_round_trip() {
        let c = Cell::pack('🦀', Attr::NORMAL, 0, 0);
        assert_eq!(c.ch(), '🦀');

        let c = Cell::pack('a', Attr::NORMAL, 0, 0);
        assert_eq!(c.ch(), 'a');
    }

    #[test]
    fn attr_round_trip() {
        let c = Cell::pack('x', Attr::BOLD | Attr::ITALIC, 0, 0);
        assert!(c.attr().contains(Attr::BOLD));
        assert!(c.attr().contains(Attr::ITALIC));
        assert!(!c.attr().contains(Attr::UNDERLINE));
    }

    #[test]
    fn color_id_round_trip() {
        let c = Cell::pack('x', Attr::NORMAL, 42, 99);
        assert_eq!(c.fg_id(), 42);
        assert_eq!(c.bg_id(), 99);
    }

    #[test]
    fn fields_are_independent() {
        let c = Cell::pack('Z', Attr::BOLD, 100, 200);
        assert_eq!(c.ch(), 'Z');
        assert!(c.attr().contains(Attr::BOLD));
        assert_eq!(c.fg_id(), 100);
        assert_eq!(c.bg_id(), 200);
    }

    #[test]
    fn blank_is_space() {
        let b = Cell::blank();
        assert_eq!(b.ch(), ' ');
        assert!(b.is_blank());
    }

    #[test]
    fn color_table_named() {
        let mut ct = ColorTable::new();
        assert_eq!(ct.color_to_id(Color::RESET), 0);
        assert_eq!(ct.color_to_id(Color::RED), 2);
        assert_eq!(ct.id_to_color(2), Color::RED);
    }

    #[test]
    fn color_table_ansi256() {
        let mut ct = ColorTable::new();
        assert_eq!(ct.color_to_id(Color::indexed(42)), 59);
        assert_eq!(ct.id_to_color(59), Color::indexed(42));
    }

    #[test]
    fn color_table_rgb() {
        let mut ct = ColorTable::new();
        let id = ct.color_to_id(Color::rgb(40, 42, 54));
        assert_eq!(id, RGB_BASE);
        assert_eq!(ct.id_to_color(id), Color::rgb(40, 42, 54));

        // Same color returns same id
        assert_eq!(ct.color_to_id(Color::rgb(40, 42, 54)), RGB_BASE);

        // Different color gets next id
        let id2 = ct.color_to_id(Color::rgb(255, 0, 0));
        assert_eq!(id2, RGB_BASE + 1);
    }

    #[test]
    fn memory_efficiency() {
        let line: Vec<Cell> = (0..80).map(|_| Cell::blank()).collect();
        assert_eq!(std::mem::size_of_val(&line[..]), 640); // 80 * 8
    }
}