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