aisling 0.2.0

Embeddable terminal text effects and loaders for Rust TUI applications.
Documentation
use crate::color::{Color, ColorPair};

/// A styled terminal cell.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cell {
    pub ch: char,
    pub colors: ColorPair,
    pub bold: bool,
    pub dim: bool,
    pub italic: bool,
    pub underline: bool,
}

impl Cell {
    pub fn new(ch: char) -> Self {
        Self {
            ch,
            ..Self::default()
        }
    }

    pub fn styled(ch: char, fg: impl Into<Option<Color>>, bg: impl Into<Option<Color>>) -> Self {
        Self {
            ch,
            colors: ColorPair::new(fg.into(), bg.into()),
            ..Self::default()
        }
    }

    pub fn with_fg(mut self, color: Color) -> Self {
        self.colors.fg = Some(color);
        self
    }

    pub fn with_bg(mut self, color: Color) -> Self {
        self.colors.bg = Some(color);
        self
    }

    pub fn has_style(&self) -> bool {
        self.colors.fg.is_some()
            || self.colors.bg.is_some()
            || self.bold
            || self.dim
            || self.italic
            || self.underline
    }
}

impl Default for Cell {
    fn default() -> Self {
        Self {
            ch: ' ',
            colors: ColorPair::default(),
            bold: false,
            dim: false,
            italic: false,
            underline: false,
        }
    }
}

/// Fixed-size 2D cell grid returned by effect iterators.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Frame {
    width: usize,
    height: usize,
    cells: Vec<Cell>,
}

impl Frame {
    pub fn new(width: usize, height: usize) -> Self {
        let width = width.max(1);
        let height = height.max(1);
        Self {
            width,
            height,
            cells: vec![Cell::default(); width * height],
        }
    }

    pub fn with_background(width: usize, height: usize, background: Option<Color>) -> Self {
        let mut frame = Self::new(width, height);
        if let Some(bg) = background {
            for cell in &mut frame.cells {
                cell.colors.bg = Some(bg);
            }
        }
        frame
    }

    pub fn width(&self) -> usize {
        self.width
    }

    pub fn height(&self) -> usize {
        self.height
    }

    pub fn cells(&self) -> &[Cell] {
        &self.cells
    }

    pub fn cell(&self, x: usize, y: usize) -> Option<&Cell> {
        if x < self.width && y < self.height {
            Some(&self.cells[y * self.width + x])
        } else {
            None
        }
    }

    pub fn set(&mut self, x: usize, y: usize, cell: Cell) {
        if x < self.width && y < self.height {
            self.cells[y * self.width + x] = cell;
        }
    }

    pub fn set_i32(&mut self, x: i32, y: i32, cell: Cell) {
        if x >= 0 && y >= 0 {
            self.set(x as usize, y as usize, cell);
        }
    }

    pub fn to_plain_string(&self) -> String {
        let mut out = String::new();
        for y in 0..self.height {
            if y > 0 {
                out.push('\n');
            }
            for x in 0..self.width {
                out.push(self.cells[y * self.width + x].ch);
            }
        }
        out
    }

    pub fn to_ansi_string(&self) -> String {
        let mut out = String::new();
        for y in 0..self.height {
            if y > 0 {
                out.push('\n');
            }
            for x in 0..self.width {
                let cell = &self.cells[y * self.width + x];
                if cell.has_style() {
                    out.push_str(&ansi_prefix(cell));
                    out.push(cell.ch);
                    out.push_str("\x1b[0m");
                } else {
                    out.push(cell.ch);
                }
            }
        }
        out
    }
}

fn ansi_prefix(cell: &Cell) -> String {
    let mut parts = Vec::new();
    if cell.bold {
        parts.push("1".to_string());
    }
    if cell.dim {
        parts.push("2".to_string());
    }
    if cell.italic {
        parts.push("3".to_string());
    }
    if cell.underline {
        parts.push("4".to_string());
    }
    if let Some(fg) = cell.colors.fg {
        parts.push(format!("38;2;{};{};{}", fg.r, fg.g, fg.b));
    }
    if let Some(bg) = cell.colors.bg {
        parts.push(format!("48;2;{};{};{}", bg.r, bg.g, bg.b));
    }
    if parts.is_empty() {
        String::new()
    } else {
        format!("\x1b[{}m", parts.join(";"))
    }
}