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