Skip to main content

sl/
terminal.rs

1use std::io::{self, Write};
2
3#[cfg(not(target_os = "wasi"))]
4use std::time::Duration;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum InputAction {
8    None,
9    Pause,
10    Quit,
11}
12
13pub struct Terminal {
14    width: u16,
15    height: u16,
16}
17
18#[cfg(not(target_os = "wasi"))]
19mod sys {
20    use super::*;
21    use crossterm::{
22        execute,
23        terminal::{EnterAlternateScreen, LeaveAlternateScreen, Clear, ClearType, enable_raw_mode, disable_raw_mode},
24        cursor::{Hide, Show},
25        event::{poll, read, Event},
26    };
27    use crossterm::event::KeyCode;
28
29    pub fn init() -> io::Result<(u16, u16)> {
30        enable_raw_mode()?;
31        let (w, h) = crossterm::terminal::size()?;
32        let mut stdout = io::stdout();
33        execute!(
34            stdout,
35            EnterAlternateScreen,
36            Hide,
37            Clear(ClearType::All)
38        )?;
39        Ok((w, h))
40    }
41
42    pub fn cleanup() -> io::Result<()> {
43        let mut stdout = io::stdout();
44        execute!(
45            stdout,
46            Show,
47            LeaveAlternateScreen
48        )?;
49        disable_raw_mode()?;
50        Ok(())
51    }
52
53    pub fn check_input() -> io::Result<InputAction> {
54        let mut action = InputAction::None;
55        while poll(Duration::from_millis(0))? {
56            if let Event::Key(key_event) = read()? {
57                if key_event.kind == crossterm::event::KeyEventKind::Press {
58                    match key_event.code {
59                        KeyCode::Char(' ') | KeyCode::Char('p') | KeyCode::Char('P') => {
60                            action = InputAction::Pause;
61                        }
62                        KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => {
63                            action = InputAction::Quit;
64                        }
65                        KeyCode::Char('c') if key_event.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) => {
66                            action = InputAction::Quit;
67                        }
68                        _ => {}
69                    }
70                }
71            }
72        }
73        Ok(action)
74    }
75}
76
77#[cfg(target_os = "wasi")]
78mod sys {
79    use super::*;
80
81    pub fn init() -> io::Result<(u16, u16)> {
82        let w = std::env::var("COLUMNS").ok().and_then(|v| v.parse().ok()).unwrap_or(80);
83        let h = std::env::var("LINES").ok().and_then(|v| v.parse().ok()).unwrap_or(24);
84
85        let mut stdout = io::stdout();
86        write!(stdout, "\x1B[?1049h\x1B[?25l\x1B[2J")?;
87        stdout.flush()?;
88        Ok((w, h))
89    }
90
91    pub fn cleanup() -> io::Result<()> {
92        let mut stdout = io::stdout();
93        write!(stdout, "\x1B[?25h\x1B[?1049l")?;
94        stdout.flush()?;
95        Ok(())
96    }
97
98    pub fn check_input() -> io::Result<InputAction> {
99        Ok(InputAction::None)
100    }
101}
102
103impl Terminal {
104    pub fn new() -> io::Result<Self> {
105        let (width, height) = sys::init()?;
106        Ok(Terminal { width, height })
107    }
108
109    pub fn width(&self) -> u16 {
110        self.width
111    }
112
113    pub fn height(&self) -> u16 {
114        self.height
115    }
116
117    /// Render frame atomically - all commands in one execute!()
118    pub fn render_frame(&self, frame: String) -> io::Result<()> {
119        let mut stdout = io::stdout();
120        write!(stdout, "{}", frame)?;
121        stdout.flush()
122    }
123
124    pub fn cleanup(&self) -> io::Result<()> {
125        sys::cleanup()
126    }
127
128    pub fn check_input(&self) -> io::Result<InputAction> {
129        sys::check_input()
130    }
131}
132
133impl Drop for Terminal {
134    fn drop(&mut self) {
135        let _ = self.cleanup();
136    }
137}