sl-rust 1.0.17

SL - An ASCII art train animation
Documentation
use std::io::{self, Write};

#[cfg(not(target_os = "wasi"))]
use std::time::Duration;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputAction {
    None,
    Pause,
    Quit,
}

pub struct Terminal {
    width: u16,
    height: u16,
}

#[cfg(not(target_os = "wasi"))]
mod sys {
    use super::*;
    use crossterm::event::KeyCode;
    use crossterm::{
        cursor::{Hide, Show},
        event::{Event, poll, read},
        execute,
        terminal::{
            Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
            enable_raw_mode,
        },
    };

    pub fn init() -> io::Result<(u16, u16)> {
        enable_raw_mode()?;
        let (w, h) = crossterm::terminal::size()?;
        let mut stdout = io::stdout();
        execute!(stdout, EnterAlternateScreen, Hide, Clear(ClearType::All))?;
        Ok((w, h))
    }

    pub fn cleanup() -> io::Result<()> {
        let mut stdout = io::stdout();
        execute!(stdout, Show, LeaveAlternateScreen)?;
        disable_raw_mode()?;
        Ok(())
    }

    pub fn check_input() -> io::Result<InputAction> {
        let mut action = InputAction::None;
        while poll(Duration::from_millis(0))? {
            if let Event::Key(key_event) = read()? {
                if key_event.kind == crossterm::event::KeyEventKind::Press {
                    match key_event.code {
                        KeyCode::Char(' ') | KeyCode::Char('p') | KeyCode::Char('P') => {
                            action = InputAction::Pause;
                        }
                        KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
                            action = InputAction::Quit;
                        }
                        KeyCode::Char('c')
                            if key_event
                                .modifiers
                                .contains(crossterm::event::KeyModifiers::CONTROL) =>
                        {
                            action = InputAction::Quit;
                        }
                        _ => {}
                    }
                }
            }
        }
        Ok(action)
    }
}

#[cfg(target_os = "wasi")]
mod sys {
    use super::*;

    pub fn init() -> io::Result<(u16, u16)> {
        let w = std::env::var("COLUMNS")
            .ok()
            .and_then(|v| v.parse().ok())
            .unwrap_or(80);
        let h = std::env::var("LINES")
            .ok()
            .and_then(|v| v.parse().ok())
            .unwrap_or(24);

        let mut stdout = io::stdout();
        write!(stdout, "\x1B[?1049h\x1B[?25l\x1B[2J")?;
        stdout.flush()?;
        Ok((w, h))
    }

    pub fn cleanup() -> io::Result<()> {
        let mut stdout = io::stdout();
        write!(stdout, "\x1B[?25h\x1B[?1049l")?;
        stdout.flush()?;
        Ok(())
    }

    pub fn check_input() -> io::Result<InputAction> {
        Ok(InputAction::None)
    }
}

impl Terminal {
    pub fn new() -> io::Result<Self> {
        let (width, height) = sys::init()?;
        Ok(Terminal { width, height })
    }

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

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

    /// Render frame atomically - all commands in one execute!()
    pub fn render_frame(&self, frame: String) -> io::Result<()> {
        let mut stdout = io::stdout();
        write!(stdout, "{}", frame)?;
        stdout.flush()
    }

    pub fn cleanup(&self) -> io::Result<()> {
        sys::cleanup()
    }

    pub fn check_input(&self) -> io::Result<InputAction> {
        sys::check_input()
    }
}

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