scrin 0.1.73

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::rect::Rect;
use crossterm::{
    cursor,
    event::{
        self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
        Event,
    },
    execute, terminal,
};
use std::io::{self, Stdout, Write};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalOptions {
    pub mouse_capture: bool,
    pub bracketed_paste: bool,
    pub alternate_screen: bool,
    pub hide_cursor: bool,
}

impl TerminalOptions {
    pub const fn new() -> Self {
        Self {
            mouse_capture: false,
            bracketed_paste: false,
            alternate_screen: true,
            hide_cursor: true,
        }
    }
}

impl Default for TerminalOptions {
    fn default() -> Self {
        Self::new()
    }
}

pub struct Frame<'a> {
    area: Rect,
    buffer: &'a mut Buffer,
    frame_count: u64,
    cursor_position: Option<(u16, u16)>,
}

impl<'a> Frame<'a> {
    pub fn area(&self) -> Rect {
        self.area
    }

    pub fn buffer(&mut self) -> &mut Buffer {
        self.buffer
    }

    pub fn frame_count(&self) -> u64 {
        self.frame_count
    }

    pub fn set_cursor_position(&mut self, x: u16, y: u16) {
        self.cursor_position = Some((x, y));
    }

    pub fn cursor_position(&self) -> Option<(u16, u16)> {
        self.cursor_position
    }
}

#[derive(Debug)]
pub struct Terminal {
    stdout: Stdout,
    width: u16,
    height: u16,
    front_buffer: Buffer,
    back_buffer: Buffer,
    hidden_cursor: bool,
    mouse_capture: bool,
    bracketed_paste: bool,
    alternate_screen: bool,
    restored: bool,
    frame_count: u64,
    cursor_position: Option<(u16, u16)>,
}

impl Terminal {
    pub fn init() -> io::Result<Self> {
        Self::init_with(TerminalOptions::default())
    }

    pub fn init_with(options: TerminalOptions) -> io::Result<Self> {
        let mut stdout = io::stdout();
        terminal::enable_raw_mode()?;
        if options.alternate_screen {
            execute!(stdout, terminal::EnterAlternateScreen)?;
        }
        if options.hide_cursor {
            execute!(stdout, cursor::Hide)?;
        }
        if options.mouse_capture {
            execute!(stdout, EnableMouseCapture)?;
        }
        if options.bracketed_paste {
            execute!(stdout, EnableBracketedPaste)?;
        }
        let (w, h) = terminal::size()?;
        let front = Buffer::new(w as usize, h as usize);
        let back = Buffer::new(w as usize, h as usize);
        Ok(Self {
            stdout,
            width: w,
            height: h,
            front_buffer: front,
            back_buffer: back,
            hidden_cursor: options.hide_cursor,
            mouse_capture: options.mouse_capture,
            bracketed_paste: options.bracketed_paste,
            alternate_screen: options.alternate_screen,
            restored: false,
            frame_count: 0,
            cursor_position: None,
        })
    }

    pub fn restore(&mut self) -> io::Result<()> {
        if self.restored {
            return Ok(());
        }
        if self.bracketed_paste {
            execute!(self.stdout, DisableBracketedPaste)?;
            self.bracketed_paste = false;
        }
        if self.mouse_capture {
            execute!(self.stdout, DisableMouseCapture)?;
            self.mouse_capture = false;
        }
        if self.hidden_cursor {
            execute!(self.stdout, cursor::Show)?;
            self.hidden_cursor = false;
        }
        if self.alternate_screen {
            execute!(self.stdout, terminal::LeaveAlternateScreen)?;
            self.alternate_screen = false;
        }
        terminal::disable_raw_mode()?;
        self.restored = true;
        Ok(())
    }

    pub fn size(&self) -> Rect {
        Rect::new(0, 0, self.width, self.height)
    }

    pub fn width(&self) -> usize {
        self.width as usize
    }

    pub fn height(&self) -> usize {
        self.height as usize
    }

    pub fn back_buffer(&mut self) -> &mut Buffer {
        &mut self.back_buffer
    }

    pub fn front_buffer(&self) -> &Buffer {
        &self.front_buffer
    }

    pub fn draw<F>(&mut self, f: F) -> io::Result<()>
    where
        F: FnOnce(&mut Frame),
    {
        self.size_changed()?;
        self.clear();
        let mut frame = Frame {
            area: self.size(),
            buffer: &mut self.back_buffer,
            frame_count: self.frame_count,
            cursor_position: None,
        };
        f(&mut frame);
        self.cursor_position = frame.cursor_position;
        self.present()?;
        if let Some((x, y)) = self.cursor_position {
            self.set_cursor(x, y)?;
        }
        self.frame_count = self.frame_count.wrapping_add(1);
        Ok(())
    }

    pub fn present(&mut self) -> io::Result<()> {
        let mut output = String::with_capacity(self.width as usize * self.height as usize * 8);
        let mut last_fg = crate::core::color::Color::BLACK;
        let mut last_bg: Option<crate::core::color::Color> = None;
        let mut last_bold = false;
        let mut last_italic = false;
        let mut last_underlined = false;

        for y in 0..self.height as usize {
            let mut line_dirty = false;
            for x in 0..self.width as usize {
                if self.back_buffer.is_skip(x, y) {
                    continue;
                }
                let back_cell = self.back_buffer.get(x, y).unwrap();
                let front_cell = self.front_buffer.get(x, y);
                if let Some(fc) = front_cell {
                    if fc == back_cell {
                        continue;
                    }
                }
                if !line_dirty {
                    output.push_str(&format!("\x1b[{};1H", y + 1));
                    line_dirty = true;
                }
                if back_cell.fg != last_fg {
                    output.push_str(&back_cell.fg.to_ansi_fg());
                    last_fg = back_cell.fg;
                }
                if back_cell.bg != last_bg {
                    match back_cell.bg {
                        Some(c) => output.push_str(&c.to_ansi_bg()),
                        None => output.push_str("\x1b[49m"),
                    }
                    last_bg = back_cell.bg;
                }
                if back_cell.bold != last_bold {
                    output.push_str(if back_cell.bold {
                        "\x1b[1m"
                    } else {
                        "\x1b[22m"
                    });
                    last_bold = back_cell.bold;
                }
                if back_cell.italic != last_italic {
                    output.push_str(if back_cell.italic {
                        "\x1b[3m"
                    } else {
                        "\x1b[23m"
                    });
                    last_italic = back_cell.italic;
                }
                if back_cell.underlined != last_underlined {
                    output.push_str(if back_cell.underlined {
                        "\x1b[4m"
                    } else {
                        "\x1b[24m"
                    });
                    last_underlined = back_cell.underlined;
                }
                output.push(back_cell.ch);
            }
        }
        output.push_str("\x1b[0m");
        write!(self.stdout, "{}", output)?;
        self.stdout.flush()?;
        self.front_buffer = self.back_buffer.clone();
        Ok(())
    }

    pub fn clear(&mut self) {
        self.back_buffer = Buffer::new(self.width as usize, self.height as usize);
    }

    pub fn clear_area(&mut self, area: Rect) {
        self.back_buffer.clear(area);
    }

    pub fn hide_cursor(&mut self) -> io::Result<()> {
        execute!(self.stdout, cursor::Hide)?;
        self.hidden_cursor = true;
        Ok(())
    }

    pub fn show_cursor(&mut self) -> io::Result<()> {
        execute!(self.stdout, cursor::Show)?;
        self.hidden_cursor = false;
        Ok(())
    }

    pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
        execute!(self.stdout, cursor::MoveTo(x, y))?;
        Ok(())
    }

    pub fn set_mouse_capture(&mut self, enabled: bool) -> io::Result<()> {
        if enabled == self.mouse_capture {
            return Ok(());
        }
        if enabled {
            execute!(self.stdout, EnableMouseCapture)?;
        } else {
            execute!(self.stdout, DisableMouseCapture)?;
        }
        self.mouse_capture = enabled;
        Ok(())
    }

    pub fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
        if enabled == self.bracketed_paste {
            return Ok(());
        }
        if enabled {
            execute!(self.stdout, EnableBracketedPaste)?;
        } else {
            execute!(self.stdout, DisableBracketedPaste)?;
        }
        self.bracketed_paste = enabled;
        Ok(())
    }

    pub fn poll_event(&self) -> io::Result<Option<Event>> {
        if event::poll(std::time::Duration::from_millis(0))? {
            Ok(Some(event::read()?))
        } else {
            Ok(None)
        }
    }

    pub fn wait_event(&self) -> io::Result<Event> {
        event::read()
    }

    pub fn flush(&mut self) -> io::Result<()> {
        self.stdout.flush()
    }

    pub fn size_changed(&mut self) -> io::Result<bool> {
        let (w, h) = terminal::size()?;
        if w != self.width || h != self.height {
            self.width = w;
            self.height = h;
            self.front_buffer.resize(w as usize, h as usize);
            self.back_buffer.resize(w as usize, h as usize);
            Ok(true)
        } else {
            Ok(false)
        }
    }

    pub fn raw_output(&self) -> &Stdout {
        &self.stdout
    }
}

impl Drop for Terminal {
    fn drop(&mut self) {
        let _ = self.restore();
    }
}

pub fn terminal_size() -> (u16, u16) {
    terminal::size().unwrap_or((80, 24))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn terminal_options_default_is_safe() {
        let options = TerminalOptions::default();
        assert!(options.alternate_screen);
        assert!(options.hide_cursor);
        assert!(!options.mouse_capture);
    }
}