tui 0.6.1

A library to build rich terminal user interfaces or dashboards
Documentation
use std::io;

use crate::backend::Backend;
use crate::style::{Color, Modifier};
use crate::{buffer::Cell, layout::Rect};
use crossterm::{Crossterm, ErrorKind};
use std::io::{stdout, Write};

pub struct CrosstermBackend {
    alternate_screen: Option<crossterm::AlternateScreen>,
    crossterm: Crossterm,
}

impl Default for CrosstermBackend {
    fn default() -> CrosstermBackend {
        CrosstermBackend {
            crossterm: Crossterm::new(),
            alternate_screen: None,
        }
    }
}

impl CrosstermBackend {
    pub fn new() -> CrosstermBackend {
        CrosstermBackend::default()
    }

    pub fn with_alternate_screen(
        alternate_screen: crossterm::AlternateScreen,
    ) -> Result<CrosstermBackend, io::Error> {
        Ok(CrosstermBackend {
            crossterm: Crossterm::new(),
            alternate_screen: Some(alternate_screen),
        })
    }

    pub fn alternate_screen(&self) -> Option<&crossterm::AlternateScreen> {
        match &self.alternate_screen {
            Some(alt_screen) => Some(&alt_screen),
            None => None,
        }
    }

    pub fn crossterm(&self) -> &crossterm::Crossterm {
        &self.crossterm
    }
}

// TODO: consider associated Error type on Backend to allow custom error types
// per backend
fn convert_error(error: ErrorKind) -> io::Error {
    match error {
        ErrorKind::IoError(err) => err,
        ErrorKind::FmtError(err) => {
            io::Error::new(io::ErrorKind::Other, format!("Invalid formatting: {}", err))
        }
        ErrorKind::ResizingTerminalFailure(err) => io::Error::new(
            io::ErrorKind::Other,
            format!("Failed to resize terminal: {}", err),
        ),
        _ => io::Error::new(io::ErrorKind::Other, "Unknown crossterm error"),
    }
}

impl Backend for CrosstermBackend {
    fn clear(&mut self) -> io::Result<()> {
        let terminal = self.crossterm.terminal();
        terminal.clear(crossterm::ClearType::All)?;
        Ok(())
    }

    fn hide_cursor(&mut self) -> io::Result<()> {
        let cursor = self.crossterm.cursor();
        cursor.hide().map_err(convert_error)?;
        Ok(())
    }

    fn show_cursor(&mut self) -> io::Result<()> {
        let cursor = self.crossterm.cursor();
        cursor.show().map_err(convert_error)?;
        Ok(())
    }

    fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
        let cursor = crossterm::cursor();
        Ok(cursor.pos())
    }

    fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
        let cursor = crossterm::cursor();
        cursor.goto(x, y).map_err(convert_error)
    }

    fn size(&self) -> io::Result<Rect> {
        let terminal = self.crossterm.terminal();
        let (width, height) = terminal.terminal_size();
        // crossterm reports max 0-based col/row index instead of count
        Ok(Rect::new(0, 0, width + 1, height + 1))
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }

    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
    where
        I: Iterator<Item = (u16, u16, &'a Cell)>,
    {
        let cursor = self.crossterm.cursor();
        let mut last_y = 0;
        let mut last_x = 0;
        let mut first = true;

        let stdout = stdout();
        let mut handle = stdout.lock();

        for (x, y, cell) in content {
            if y != last_y || x != last_x + 1 || first {
                cursor.goto(x, y).map_err(convert_error)?;
                first = false;
            }
            last_x = x;
            last_y = y;
            let mut s = self.crossterm.style(&cell.symbol);
            if let Some(color) = cell.style.fg.into() {
                s = s.with(color)
            }
            if let Some(color) = cell.style.bg.into() {
                s = s.on(color)
            }
            s.object_style.attrs = cell.style.modifier.into();

            write!(handle, "{}", s)?;
        }
        Ok(())
    }
}

impl From<Color> for Option<crossterm::Color> {
    fn from(color: Color) -> Option<crossterm::Color> {
        match color {
            Color::Reset => None,
            Color::Black => Some(crossterm::Color::Black),
            Color::Red => Some(crossterm::Color::DarkRed),
            Color::Green => Some(crossterm::Color::DarkGreen),
            Color::Yellow => Some(crossterm::Color::DarkYellow),
            Color::Blue => Some(crossterm::Color::DarkBlue),
            Color::Magenta => Some(crossterm::Color::DarkMagenta),
            Color::Cyan => Some(crossterm::Color::DarkCyan),
            Color::Gray => Some(crossterm::Color::Grey),
            Color::DarkGray => Some(crossterm::Color::DarkGrey),
            Color::LightRed => Some(crossterm::Color::Red),
            Color::LightGreen => Some(crossterm::Color::Green),
            Color::LightBlue => Some(crossterm::Color::Blue),
            Color::LightYellow => Some(crossterm::Color::Yellow),
            Color::LightMagenta => Some(crossterm::Color::Magenta),
            Color::LightCyan => Some(crossterm::Color::Cyan),
            Color::White => Some(crossterm::Color::White),
            Color::Indexed(i) => Some(crossterm::Color::AnsiValue(i)),
            Color::Rgb(r, g, b) => Some(crossterm::Color::Rgb { r, g, b }),
        }
    }
}

impl From<Modifier> for Vec<crossterm::Attribute> {
    #[cfg(unix)]
    fn from(modifier: Modifier) -> Vec<crossterm::Attribute> {
        let mut result = Vec::new();

        if modifier.contains(Modifier::BOLD) {
            result.push(crossterm::Attribute::Bold)
        }
        if modifier.contains(Modifier::DIM) {
            result.push(crossterm::Attribute::Dim)
        }
        if modifier.contains(Modifier::ITALIC) {
            result.push(crossterm::Attribute::Italic)
        }
        if modifier.contains(Modifier::UNDERLINED) {
            result.push(crossterm::Attribute::Underlined)
        }
        if modifier.contains(Modifier::SLOW_BLINK) {
            result.push(crossterm::Attribute::SlowBlink)
        }
        if modifier.contains(Modifier::RAPID_BLINK) {
            result.push(crossterm::Attribute::RapidBlink)
        }
        if modifier.contains(Modifier::REVERSED) {
            result.push(crossterm::Attribute::Reverse)
        }
        if modifier.contains(Modifier::HIDDEN) {
            result.push(crossterm::Attribute::Hidden)
        }
        if modifier.contains(Modifier::CROSSED_OUT) {
            result.push(crossterm::Attribute::CrossedOut)
        }

        result
    }

    #[cfg(windows)]
    fn from(modifier: Modifier) -> Vec<crossterm::Attribute> {
        let mut result = Vec::new();

        if modifier.contains(Modifier::BOLD) {
            result.push(crossterm::Attribute::Bold)
        }
        if modifier.contains(Modifier::UNDERLINED) {
            result.push(crossterm::Attribute::Underlined)
        }

        result
    }
}