termit 0.7.0

Terminal UI over crossterm
Documentation
use crate::{
    output::{
        ansi::ScreenStyle,
        color::Color,
        style::{Style, Stylist},
    },
    prelude::{point, window, Point, Window},
    Painter, Screen,
};
use log::trace;

/// Emulates a writable terminal so that other libs that write to terminals can be embedded in termit.
#[cfg_attr(docsrs, doc(cfg(feature = "temu")))]
pub struct TerminalEmulator {
    pub title: String,
    screens: [(Screen, Window, Style); 2],
    screen_idx: usize,
    console: ransid::Console,
    raw: bool,
}

impl Default for TerminalEmulator {
    fn default() -> Self {
        TerminalEmulator::new(String::new())
    }
}
impl TerminalEmulator {
    pub fn new(title: impl ToString) -> Self {
        let screen = || (Screen::default(), Window::default(), Style::DEFAULT);
        Self {
            title: title.to_string(),
            screens: [screen(), screen()],
            screen_idx: 0,
            console: ransid::Console::new(0, 0),
            raw: false,
        }
    }
    pub fn size(&self) -> Point {
        point(self.console.state.w as u16, self.console.state.h as u16)
    }
    pub fn resize(&mut self, size: Point) {
        if self.size() != size {
            self.console.resize(size.col as usize, size.row as usize);

            for (screen, view, ..) in &mut self.screens {
                // TODO: adjust screen size properly
                screen.resize(size);
                // TODO: adjust views properly
                *view = size.window()
            }
        }
    }
    /// set new view for the current screen
    pub fn pan(&mut self, new_view: Window) {
        let Self {
            ref mut screens,
            screen_idx: screen,
            ..
        } = *self;
        let (_, view, ..) = &mut screens[screen];
        *view = new_view
    }
    pub fn symbols(&mut self) -> impl Iterator<Item = (Point, &ScreenStyle, &str)> {
        let Self {
            ref mut screens,
            screen_idx: screen,
            ..
        } = *self;
        let (screen, ..) = &mut screens[screen];
        screen.symbols()
    }
    fn move_cursor(
        _from_x: usize,
        _from_y: usize,
        to_x: usize,
        to_y: usize,
        _w: usize,
        _h: usize,
        screen: &mut Screen,
        view: &Window,
    ) {
        let pos = point(to_x as u16, to_y as u16) + view.position();
        screen.set_last_write(pos);
    }
    fn fill(
        x: usize,
        y: usize,
        w: usize,
        h: usize,
        color: ransid::Color,
        screen: &mut Screen,
        view: &Window,
        style: Style,
    ) {
        let pos = point(x as u16, y as u16) + view.position();
        let size = point(w as u16, h as u16);
        let scope = view & &window(pos, size);
        let color = match color {
            ransid::Color::Ansi(c) => Color::ansi(c),
            ransid::Color::TrueColor(r, g, b) => Color::rgb(r, g, b),
        };
        Painter::default()
            .with_scope(scope)
            .with_new_style(style.back(color))
            .fill(screen)
    }
    fn data(data: &[u8], screen: &mut Screen, view: &Window, style: Style, raw: bool) {
        let text = std::str::from_utf8(data).unwrap_or_default();
        let offset = screen
            .last_write()
            .and_then(|o| view.contains_point(o))
            .map(|o| o - view.position())
            .map(|o| 1 + o.row * view.width + o.col)
            .unwrap_or_default();
        Painter::default()
            .with_scope(*view)
            .with_new_style(style)
            .paint(text, screen, offset, raw);
    }
    fn char(
        x: usize,
        y: usize,
        c: char,
        bold: bool,
        italic: bool,
        underlined: bool,
        strikethrough: bool,
        color: ransid::Color,
        screen: &mut Screen,
        view: &Window,
        mut style: Style,
        _raw: bool,
    ) {
        let point = point(x as u16, y as u16) + view.position();

        if bold {
            style = style.bold(true)
        }
        if italic {
            style = style.italic(true)
        }
        if underlined {
            style = style.underlined(underlined);
        }
        if strikethrough {
            style = style.crossed_out(strikethrough)
        }
        style = style.front(match color {
            ransid::Color::Ansi(c) => Color::ansi(c),
            ransid::Color::TrueColor(r, g, b) => Color::rgb(r, g, b),
        });

        if let Some((curr_style, _)) = screen.get(point) {
            if let Some(bg) = curr_style.back_color() {
                // set background previously sent by ransid with fill
                style = style.back(bg);
            }
        }

        screen.set(point, &style, c.to_string().as_str());
    }
}

impl std::io::Write for TerminalEmulator {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        trace!("write {} {:?}", buf.len(), std::str::from_utf8(buf));

        let mut size_next = self.size();
        let size = &mut size_next;
        let mut clear = false;
        let clear_screen = &mut clear;
        let Self {
            ref mut title,
            ref mut screens,
            ref mut screen_idx,
            ref mut console,
            ref mut raw,
        } = *self;
        let (screen, view, style) = &mut screens[*screen_idx];

        console.write(buf, move |e| {
            trace!("event {:?}", e);
            match e {
                ransid::Event::Char {
                    x,
                    y,
                    c,
                    bold,
                    italic,
                    underlined,
                    strikethrough,
                    color,
                } => Self::char(
                    x,
                    y,
                    c,
                    bold,
                    italic,
                    underlined,
                    strikethrough,
                    color,
                    screen,
                    view,
                    *style,
                    *raw,
                ),
                ransid::Event::Input { data } => Self::data(data, screen, view, *style, *raw),
                ransid::Event::Rect { x, y, w, h, color } => {
                    Self::fill(x, y, w, h, color, screen, view, *style)
                }
                ransid::Event::ScreenBuffer { alternate, clear } => {
                    *screen_idx = if alternate { 1 } else { 0 };
                    *clear_screen = clear;
                }
                ransid::Event::Move {
                    from_x,
                    from_y,
                    to_x,
                    to_y,
                    w,
                    h,
                } => Self::move_cursor(from_x, from_y, to_x, to_y, w, h, screen, view),
                ransid::Event::Resize { w, h } => *size = point(w as u16, h as u16),
                ransid::Event::Title { title: t } => *title = t,
            }
        });

        if clear {
            let (screen, view, style) = &mut screens[self.screen_idx];
            Self::fill(
                0,
                0,
                view.width.into(),
                view.height.into(),
                ransid::Color::Ansi(0),
                screen,
                view,
                *style,
            );
        }
        // TODO: self.resize(size_next);

        Ok(buf.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        // nothing to do
        Ok(())
    }
}