aisling 0.2.1

Embeddable terminal text effects and loaders for Rust TUI applications.
Documentation
use crate::color::Color;
use crate::frame::Cell;

#[derive(Clone, Debug)]
pub(crate) struct Glyph {
    pub(crate) index: usize,
    pub(crate) x: usize,
    pub(crate) y: usize,
    pub(crate) cell: Cell,
}

#[derive(Clone, Debug)]
pub(crate) struct TextGrid {
    pub(crate) width: usize,
    pub(crate) height: usize,
    pub(crate) glyphs: Vec<Glyph>,
}

impl TextGrid {
    pub(crate) fn parse(input: &str, tab_width: usize) -> Self {
        let input = if input.is_empty() { "No Input." } else { input };
        let tab_width = tab_width.max(1);
        let mut glyphs = Vec::new();
        let mut x = 0usize;
        let mut y = 0usize;
        let mut width = 0usize;
        let mut style = Cell::default();
        let mut chars = input.chars().peekable();

        while let Some(ch) = chars.next() {
            if ch == '\x1b' && chars.peek() == Some(&'[') {
                chars.next();
                let mut seq = String::new();
                for next in chars.by_ref() {
                    let end = next.is_ascii_alphabetic();
                    seq.push(next);
                    if end {
                        break;
                    }
                }
                if seq.ends_with('m') {
                    apply_sgr(&seq[..seq.len().saturating_sub(1)], &mut style);
                }
                continue;
            }

            match ch {
                '\n' => {
                    width = width.max(x);
                    x = 0;
                    y += 1;
                }
                '\r' => x = 0,
                '\t' => {
                    let spaces = tab_width - (x % tab_width);
                    for _ in 0..spaces {
                        push_glyph(&mut glyphs, &style, ' ', x, y);
                        x += 1;
                    }
                }
                _ => {
                    push_glyph(&mut glyphs, &style, ch, x, y);
                    x += 1;
                }
            }
        }
        width = width.max(x).max(1);
        let height = (y + 1).max(1);
        Self {
            width,
            height,
            glyphs,
        }
    }
}

fn push_glyph(glyphs: &mut Vec<Glyph>, style: &Cell, ch: char, x: usize, y: usize) {
    let mut cell = style.clone();
    cell.ch = ch;
    glyphs.push(Glyph {
        index: glyphs.len(),
        x,
        y,
        cell,
    });
}

fn apply_sgr(params: &str, style: &mut Cell) {
    let values: Vec<u16> = if params.trim().is_empty() {
        vec![0]
    } else {
        params
            .split(';')
            .filter_map(|part| part.parse::<u16>().ok())
            .collect()
    };
    let mut i = 0;
    while i < values.len() {
        match values[i] {
            0 => *style = Cell::default(),
            1 => style.bold = true,
            2 => style.dim = true,
            3 => style.italic = true,
            4 => style.underline = true,
            22 => {
                style.bold = false;
                style.dim = false;
            }
            23 => style.italic = false,
            24 => style.underline = false,
            30..=37 => style.colors.fg = Some(basic_color(values[i] as u8 - 30, false)),
            40..=47 => style.colors.bg = Some(basic_color(values[i] as u8 - 40, false)),
            90..=97 => style.colors.fg = Some(basic_color(values[i] as u8 - 90, true)),
            100..=107 => style.colors.bg = Some(basic_color(values[i] as u8 - 100, true)),
            38 | 48 => {
                let is_fg = values[i] == 38;
                if values.get(i + 1) == Some(&2) && i + 4 < values.len() {
                    let color = Color::rgb(
                        values[i + 2] as u8,
                        values[i + 3] as u8,
                        values[i + 4] as u8,
                    );
                    if is_fg {
                        style.colors.fg = Some(color)
                    } else {
                        style.colors.bg = Some(color)
                    }
                    i += 4;
                } else if values.get(i + 1) == Some(&5) && i + 2 < values.len() {
                    let color = Color::from_xterm(values[i + 2] as u8);
                    if is_fg {
                        style.colors.fg = Some(color)
                    } else {
                        style.colors.bg = Some(color)
                    }
                    i += 2;
                }
            }
            39 => style.colors.fg = None,
            49 => style.colors.bg = None,
            _ => {}
        }
        i += 1;
    }
}

fn basic_color(index: u8, bright: bool) -> Color {
    let code = if bright { index + 8 } else { index };
    Color::from_xterm(code)
}