lios 0.1.89

Lios Linux terminal with native Aen and GTK4/VTE frontends.
Documentation
use super::model::{Grid, MAX_COLS, MAX_ROWS, Terminal};
use ab_glyph::{Font, FontArc, ScaleFont, point};
use std::collections::HashMap;
use std::io::{self, Read};
use std::path::{Path, PathBuf};

const MAX_FONT_BYTES: u64 = 16 * 1024 * 1024;
const MAX_GLYPH_BYTES: usize = 8 * 1024 * 1024;
const BACKGROUND: [u8; 3] = [17, 23, 33];
const FOREGROUND: [u8; 3] = [216, 224, 238];
const PALETTE: [[u8; 3]; 16] = [
    [25, 31, 42],
    [226, 92, 105],
    [133, 198, 122],
    [227, 193, 117],
    [114, 163, 224],
    [189, 143, 223],
    [111, 198, 205],
    [200, 208, 224],
    [98, 110, 130],
    [246, 119, 128],
    [158, 221, 146],
    [245, 213, 145],
    [145, 188, 245],
    [212, 169, 242],
    [147, 222, 226],
    [240, 245, 255],
];

struct GlyphMask {
    x: i32,
    y: i32,
    width: u32,
    height: u32,
    alpha: Vec<u8>,
}
pub struct Renderer {
    fonts: Vec<FontArc>,
    font_size: f32,
    cell_width: u32,
    cell_height: u32,
    ascent: f32,
    glyphs: HashMap<(char, usize), GlyphMask>,
    glyph_bytes: usize,
}
impl Renderer {
    pub fn new(path: Option<&Path>, font_size: f32) -> io::Result<Self> {
        let path = match path {
            Some(path) => path.to_owned(),
            None => default_font()?,
        };
        let regular = load_font(&path)?;
        let scaled = regular.as_scaled(font_size);
        let cell_width = scaled.h_advance(regular.glyph_id('M')).ceil().max(1.0) as u32;
        let cell_height = (scaled.height() + scaled.line_gap())
            .ceil()
            .max(font_size)
            .max(1.0) as u32
            + 3;
        let ascent = scaled.ascent();
        let mut fonts = vec![regular];
        for suffix in ["-Bold", "-Oblique", "-BoldOblique"] {
            let variant = path.with_file_name(format!(
                "{}{suffix}.{}",
                path.file_stem().unwrap_or_default().to_string_lossy(),
                path.extension().unwrap_or_default().to_string_lossy()
            ));
            fonts.push(load_font(&variant).unwrap_or_else(|_| fonts[0].clone()));
        }
        Ok(Self {
            fonts,
            font_size,
            cell_width,
            cell_height,
            ascent,
            glyphs: HashMap::new(),
            glyph_bytes: 0,
        })
    }
    pub fn grid(&self, width: u32, height: u32) -> Grid {
        Grid {
            cols: (width.saturating_sub(16) / self.cell_width).clamp(1, u32::from(MAX_COLS)) as u16,
            rows: (height.saturating_sub(16) / self.cell_height).clamp(1, u32::from(MAX_ROWS))
                as u16,
            cell_width: self.cell_width,
            cell_height: self.cell_height,
            padding: 8,
        }
    }
    pub fn render(&mut self, terminal: &Terminal, width: u32, height: u32) -> io::Result<Vec<u8>> {
        let bytes = aen_window::frame_bytes(width, height)?;
        let mut pixels = Vec::with_capacity(bytes);
        for _ in 0..bytes / 4 {
            pixels.extend_from_slice(&[BACKGROUND[2], BACKGROUND[1], BACKGROUND[0], 255]);
        }
        let grid = self.grid(width, height);
        let screen = terminal.parser.screen();
        for row in 0..grid.rows {
            for col in 0..grid.cols {
                let Some(cell) = screen.cell(row, col) else {
                    continue;
                };
                let (_, bg) = colors(cell, terminal.selected(row, col));
                rectangle(
                    &mut pixels,
                    width,
                    height,
                    grid.padding + u32::from(col) * grid.cell_width,
                    grid.padding + u32::from(row) * grid.cell_height,
                    grid.cell_width,
                    grid.cell_height,
                    bg,
                );
            }
        }
        for row in 0..grid.rows {
            for col in 0..grid.cols {
                let Some(cell) = screen.cell(row, col) else {
                    continue;
                };
                if cell.is_wide_continuation() {
                    continue;
                }
                let (fg, _) = colors(cell, terminal.selected(row, col));
                let x = grid.padding + u32::from(col) * grid.cell_width;
                let y = grid.padding + u32::from(row) * grid.cell_height;
                let span = if cell.is_wide() { 2 } else { 1 };
                self.text(
                    &mut pixels,
                    width,
                    height,
                    x,
                    y,
                    grid.cell_width * span,
                    grid.cell_height,
                    cell.contents(),
                    fg,
                    usize::from(cell.bold()) + 2 * usize::from(cell.italic()),
                );
                if cell.underline() {
                    rectangle(
                        &mut pixels,
                        width,
                        height,
                        x,
                        y + grid.cell_height - 2,
                        grid.cell_width * span,
                        1,
                        fg,
                    );
                }
            }
        }
        if !screen.hide_cursor() && screen.scrollback() == 0 {
            let (row, mut col) = screen.cursor_position();
            if screen
                .cell(row, col)
                .is_some_and(vt100::Cell::is_wide_continuation)
            {
                col = col.saturating_sub(1);
            }
            if row < grid.rows && col < grid.cols {
                let x = grid.padding + u32::from(col) * grid.cell_width;
                let y = grid.padding + u32::from(row) * grid.cell_height;
                if terminal.focused {
                    rectangle(
                        &mut pixels,
                        width,
                        height,
                        x,
                        y,
                        grid.cell_width,
                        grid.cell_height,
                        FOREGROUND,
                    );
                    if let Some(cell) = screen.cell(row, col) {
                        self.text(
                            &mut pixels,
                            width,
                            height,
                            x,
                            y,
                            grid.cell_width,
                            grid.cell_height,
                            cell.contents(),
                            BACKGROUND,
                            0,
                        );
                    }
                } else {
                    for (dx, dy, w, h) in [
                        (0, 0, grid.cell_width, 1),
                        (0, grid.cell_height - 1, grid.cell_width, 1),
                        (0, 0, 1, grid.cell_height),
                        (grid.cell_width - 1, 0, 1, grid.cell_height),
                    ] {
                        rectangle(&mut pixels, width, height, x + dx, y + dy, w, h, FOREGROUND);
                    }
                }
            }
        }
        Ok(pixels)
    }
    fn glyph(&mut self, character: char, style: usize) -> &GlyphMask {
        let key = (character, style);
        if !self.glyphs.contains_key(&key) {
            let font = &self.fonts[style];
            let mut id = font.glyph_id(character);
            if id.0 == 0 {
                id = font.glyph_id('\u{fffd}');
            }
            let glyph = id.with_scale_and_position(self.font_size, point(0.0, self.ascent));
            let mut mask = GlyphMask {
                x: 0,
                y: 0,
                width: 0,
                height: 0,
                alpha: Vec::new(),
            };
            if let Some(outline) = font.outline_glyph(glyph) {
                let bounds = outline.px_bounds();
                let width = bounds.width().ceil() as u32;
                let height = bounds.height().ceil() as u32;
                if width <= 256 && height <= 256 {
                    mask = GlyphMask {
                        x: bounds.min.x as i32,
                        y: bounds.min.y as i32,
                        width,
                        height,
                        alpha: vec![0; (width * height) as usize],
                    };
                    outline.draw(|x, y, coverage| {
                        if x < width && y < height {
                            mask.alpha[(y * width + x) as usize] = (coverage * 255.0).round() as u8;
                        }
                    });
                }
            }
            if self.glyphs.len() >= 4096 || self.glyph_bytes + mask.alpha.len() > MAX_GLYPH_BYTES {
                self.glyphs.clear();
                self.glyph_bytes = 0;
            }
            self.glyph_bytes += mask.alpha.len();
            self.glyphs.insert(key, mask);
        }
        &self.glyphs[&key]
    }
    #[allow(clippy::too_many_arguments)]
    fn text(
        &mut self,
        pixels: &mut [u8],
        width: u32,
        height: u32,
        x: u32,
        y: u32,
        cell_width: u32,
        cell_height: u32,
        text: &str,
        color: [u8; 3],
        style: usize,
    ) {
        for character in text.chars() {
            let mask = self.glyph(character, style);
            for gy in 0..mask.height {
                for gx in 0..mask.width {
                    let dx = mask.x + gx as i32;
                    let dy = mask.y + gy as i32;
                    if dx < 0 || dy < 0 || dx as u32 >= cell_width || dy as u32 >= cell_height {
                        continue;
                    }
                    let px = x + dx as u32;
                    let py = y + dy as u32;
                    if px >= width || py >= height {
                        continue;
                    }
                    let alpha = u32::from(mask.alpha[(gy * mask.width + gx) as usize]);
                    let offset = ((py * width + px) * 4) as usize;
                    for channel in 0..3 {
                        pixels[offset + channel] = ((u32::from(color[2 - channel]) * alpha
                            + u32::from(pixels[offset + channel]) * (255 - alpha)
                            + 127)
                            / 255) as u8;
                    }
                }
            }
        }
    }
}
fn load_font(path: &Path) -> io::Result<FontArc> {
    let file = std::fs::File::open(path)?;
    let mut bytes = Vec::new();
    file.take(MAX_FONT_BYTES + 1).read_to_end(&mut bytes)?;
    if bytes.len() as u64 > MAX_FONT_BYTES {
        return Err(io::Error::other("Lios font exceeds 16 MiB"));
    }
    FontArc::try_from_vec(bytes).map_err(|_| io::Error::other("invalid Lios font"))
}
pub fn default_font() -> io::Result<PathBuf> {
    for path in [
        "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
        "/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
    ] {
        if Path::new(path).is_file() {
            return Ok(path.into());
        }
    }
    Err(io::Error::other(
        "Lios needs a monospace font: install ttf-dejavu or pass --font-file",
    ))
}
fn colors(cell: &vt100::Cell, selected: bool) -> ([u8; 3], [u8; 3]) {
    let mut fg = color(cell.fgcolor(), FOREGROUND);
    let mut bg = color(cell.bgcolor(), BACKGROUND);
    if cell.bold()
        && let vt100::Color::Idx(i @ 0..=7) = cell.fgcolor()
    {
        fg = PALETTE[usize::from(i + 8)];
    }
    if cell.dim() {
        fg = fg.map(|c| (u16::from(c) * 2 / 3) as u8);
    }
    if cell.inverse() {
        std::mem::swap(&mut fg, &mut bg);
    }
    if selected {
        bg = [56, 77, 110];
    }
    (fg, bg)
}
fn color(value: vt100::Color, default: [u8; 3]) -> [u8; 3] {
    match value {
        vt100::Color::Default => default,
        vt100::Color::Rgb(r, g, b) => [r, g, b],
        vt100::Color::Idx(i @ 0..=15) => PALETTE[usize::from(i)],
        vt100::Color::Idx(i @ 16..=231) => {
            let n = i - 16;
            [n / 36, (n / 6) % 6, n % 6].map(|c| if c == 0 { 0 } else { 55 + 40 * c })
        }
        vt100::Color::Idx(i) => [8 + (i - 232) * 10; 3],
    }
}
#[allow(clippy::too_many_arguments)]
fn rectangle(
    pixels: &mut [u8],
    width: u32,
    height: u32,
    x: u32,
    y: u32,
    w: u32,
    h: u32,
    color: [u8; 3],
) {
    for py in y..y.saturating_add(h).min(height) {
        for px in x..x.saturating_add(w).min(width) {
            let offset = ((py * width + px) * 4) as usize;
            pixels[offset..offset + 4].copy_from_slice(&[color[2], color[1], color[0], 255]);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn color_cube_gray_and_clipping() {
        assert_eq!(color(vt100::Color::Idx(196), BACKGROUND), [255, 0, 0]);
        assert_eq!(color(vt100::Color::Idx(255), BACKGROUND), [238; 3]);
        let mut pixels = vec![0; 16];
        rectangle(&mut pixels, 2, 2, 1, 1, u32::MAX, u32::MAX, [1, 2, 3]);
        assert_eq!(&pixels[12..], &[3, 2, 1, 255]);
        assert_eq!(&pixels[..12], &[0; 12]);
    }
}