tui-term 0.3.4

A pseudoterminal widget for ratatui
Documentation
use ratatui_core::style::{Modifier, Style};

use crate::widget::{Cell, Screen};

impl Screen for vt100::Screen {
    type C = vt100::Cell;

    #[inline]
    fn cell(&self, row: u16, col: u16) -> Option<&Self::C> {
        self.cell(row, col)
    }

    #[inline]
    fn hide_cursor(&self) -> bool {
        self.hide_cursor()
    }

    #[inline]
    fn cursor_position(&self) -> (u16, u16) {
        let (row, col) = self.cursor_position();
        let scrollback = u16::try_from(self.scrollback()).unwrap_or(u16::MAX);
        (row.saturating_add(scrollback), col)
    }
}

impl Cell for vt100::Cell {
    #[inline]
    fn has_contents(&self) -> bool {
        self.has_contents()
    }

    #[inline]
    fn apply(&self, cell: &mut ratatui_core::buffer::Cell) {
        fill_buf_cell(self, cell)
    }
}

#[inline]
fn fill_buf_cell(screen_cell: &vt100::Cell, buf_cell: &mut ratatui_core::buffer::Cell) {
    if screen_cell.has_contents() {
        buf_cell.set_symbol(screen_cell.contents());
    }

    let mut modifier = Modifier::empty();
    if screen_cell.bold() {
        modifier |= Modifier::BOLD;
    }
    if screen_cell.italic() {
        modifier |= Modifier::ITALIC;
    }
    if screen_cell.underline() {
        modifier |= Modifier::UNDERLINED;
    }
    if screen_cell.inverse() {
        modifier |= Modifier::REVERSED;
    }
    if screen_cell.dim() {
        modifier |= Modifier::DIM;
    }

    let fg = map_color(screen_cell.fgcolor());
    let bg = map_color(screen_cell.bgcolor());

    buf_cell.set_style(Style::reset().fg(fg).bg(bg).add_modifier(modifier));
}

#[inline]
fn map_color(color: vt100::Color) -> ratatui_core::style::Color {
    match color {
        vt100::Color::Default => ratatui_core::style::Color::Reset,
        vt100::Color::Idx(i) => ratatui_core::style::Color::Indexed(i),
        vt100::Color::Rgb(r, g, b) => ratatui_core::style::Color::Rgb(r, g, b),
    }
}

#[cfg(test)]
mod tests {
    use crate::widget::Screen;

    #[test]
    fn cursor_position_offset_by_scrollback() {
        let mut parser = vt100::Parser::new(6, 80, 20);
        for i in 0..9 {
            parser.process(format!("line {i}\r\n").as_bytes());
        }
        let (drawing_row, drawing_col) = parser.screen().cursor_position();

        parser.screen_mut().set_scrollback(2);
        let visible_pos = Screen::cursor_position(parser.screen());
        assert_eq!(visible_pos, (drawing_row + 2, drawing_col));
    }

    #[test]
    fn cursor_position_offset_preserves_col() {
        let mut parser = vt100::Parser::new(6, 80, 20);
        for i in 0..9 {
            parser.process(format!("line {i}\r\n").as_bytes());
        }
        parser.process(b"partial");
        let (drawing_row, drawing_col) = parser.screen().cursor_position();
        assert_eq!(drawing_col, 7);

        parser.screen_mut().set_scrollback(2);
        let (visible_row, visible_col) = Screen::cursor_position(parser.screen());
        assert_eq!(visible_row, drawing_row + 2);
        assert_eq!(visible_col, 7, "scrollback should not affect column");
    }

    #[test]
    fn cursor_position_off_screen_with_full_scrollback() {
        let mut parser = vt100::Parser::new(4, 80, 20);
        for i in 0..10 {
            parser.process(format!("line {i}\r\n").as_bytes());
        }

        parser.screen_mut().set_scrollback(4);
        let (visible_row, _) = Screen::cursor_position(parser.screen());
        assert!(
            visible_row >= 4,
            "cursor at visible row {visible_row} should be off-screen (>= 4 rows)"
        );
    }
}