Skip to main content

endbasic_terminal/
lib.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Crossterm-based console for terminal interaction.
18
19use async_channel::{Receiver, Sender, TryRecvError};
20use async_trait::async_trait;
21use crossterm::event::{self, KeyEventKind};
22use crossterm::tty::IsTty;
23use crossterm::{QueueableCommand, cursor, style, terminal};
24use endbasic_std::Signal;
25use endbasic_std::console::graphics::InputOps;
26use endbasic_std::console::{
27    CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars,
28};
29use std::cmp::Ordering;
30use std::collections::VecDeque;
31use std::io::{self, StdoutLock, Write};
32
33/// Implementation of the EndBASIC console to interact with stdin and stdout.
34pub struct TerminalConsole {
35    /// Whether stdin and stdout are attached to a TTY.  When this is true, the console is put in
36    /// raw mode for finer-grained control.
37    is_tty: bool,
38
39    /// Current foreground color.
40    fg_color: Option<u8>,
41
42    /// Current background color.
43    bg_color: Option<u8>,
44
45    /// Whether the cursor is visible or not.
46    cursor_visible: bool,
47
48    /// Whether we are in the alternate console or not.
49    alt_active: bool,
50
51    /// Whether video syncing is enabled or not.
52    sync_enabled: bool,
53
54    /// Channel to receive key presses from the terminal.
55    on_key_rx: Receiver<Key>,
56}
57
58impl Drop for TerminalConsole {
59    fn drop(&mut self) {
60        if self.is_tty {
61            terminal::disable_raw_mode().unwrap();
62        }
63    }
64}
65
66impl TerminalConsole {
67    /// Creates a new console based on the properties of stdin/stdout.
68    ///
69    /// This spawns a background task to handle console input so this must be run in the context of
70    /// an Tokio runtime.
71    pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
72        let (terminal, _on_key_tx) = Self::from_stdio_with_injector(signals_tx)?;
73        Ok(terminal)
74    }
75
76    /// Creates a new console based on the properties of stdin/stdout.
77    ///
78    /// This spawns a background task to handle console input so this must be run in the context of
79    /// an Tokio runtime.
80    ///
81    /// Compared to `from_stdio`, this also returns a key sender to inject extra events into the
82    /// queue maintained by the terminal.
83    pub fn from_stdio_with_injector(signals_tx: Sender<Signal>) -> io::Result<(Self, Sender<Key>)> {
84        let (on_key_tx, on_key_rx) = async_channel::unbounded();
85
86        let is_tty = io::stdin().is_tty() && io::stdout().is_tty();
87
88        if is_tty {
89            terminal::enable_raw_mode()?;
90            tokio::task::spawn(TerminalConsole::raw_key_handler(on_key_tx.clone(), signals_tx));
91        } else {
92            tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx.clone()));
93        }
94
95        Ok((
96            Self {
97                is_tty,
98                fg_color: None,
99                bg_color: None,
100                cursor_visible: true,
101                alt_active: false,
102                sync_enabled: true,
103                on_key_rx,
104            },
105            on_key_tx,
106        ))
107    }
108
109    /// Async task to wait for key events on a raw terminal and translate them into events for the
110    /// console or the machine.
111    async fn raw_key_handler(on_key_tx: Sender<Key>, signals_tx: Sender<Signal>) {
112        use event::{KeyCode, KeyModifiers};
113
114        loop {
115            let key = match event::read() {
116                Ok(event::Event::Key(ev)) => {
117                    if ev.kind != KeyEventKind::Press {
118                        continue;
119                    }
120
121                    match ev.code {
122                        KeyCode::Backspace => Key::Backspace,
123                        KeyCode::End => Key::End,
124                        KeyCode::Esc => Key::Escape,
125                        KeyCode::Home => Key::Home,
126                        KeyCode::Tab => Key::Tab,
127                        KeyCode::Up => Key::ArrowUp,
128                        KeyCode::Down => Key::ArrowDown,
129                        KeyCode::Left => Key::ArrowLeft,
130                        KeyCode::Right => Key::ArrowRight,
131                        KeyCode::PageDown => Key::PageDown,
132                        KeyCode::PageUp => Key::PageUp,
133                        KeyCode::Char('a') if ev.modifiers == KeyModifiers::CONTROL => Key::Home,
134                        KeyCode::Char('b') if ev.modifiers == KeyModifiers::CONTROL => {
135                            Key::ArrowLeft
136                        }
137                        KeyCode::Char('c') if ev.modifiers == KeyModifiers::CONTROL => {
138                            Key::Interrupt
139                        }
140                        KeyCode::Char('d') if ev.modifiers == KeyModifiers::CONTROL => Key::Eof,
141                        KeyCode::Char('e') if ev.modifiers == KeyModifiers::CONTROL => Key::End,
142                        KeyCode::Char('f') if ev.modifiers == KeyModifiers::CONTROL => {
143                            Key::ArrowRight
144                        }
145                        KeyCode::Char('j') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
146                        KeyCode::Char('m') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
147                        KeyCode::Char('n') if ev.modifiers == KeyModifiers::CONTROL => {
148                            Key::ArrowDown
149                        }
150                        KeyCode::Char('p') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowUp,
151                        KeyCode::Char(ch) => Key::Char(ch),
152                        KeyCode::Enter => Key::NewLine,
153                        _ => Key::Unknown,
154                    }
155                }
156                Ok(_) => {
157                    // Not a key event; ignore and try again.
158                    continue;
159                }
160                Err(_) => {
161                    // There is not much we can do if we get an error from crossterm.
162                    Key::Unknown
163                }
164            };
165
166            if key == Key::Interrupt {
167                // Handling CTRL+C in this way isn't great because this is not the same as handling
168                // SIGINT on Unix builds.  First, we are unable to stop long-running operations like
169                // sleeps; and second, a real SIGINT will kill the interpreter completely instead of
170                // coming this way.  We need a real signal handler and we probably should not be
171                // running in raw mode all the time.
172                if signals_tx.send(Signal::Break).await.is_err() {
173                    break;
174                }
175            }
176
177            // Exit the background task if the console receiver has gone away.
178            if on_key_tx.send(key).await.is_err() {
179                break;
180            }
181        }
182    }
183
184    /// Async task to wait for key events on a non-raw terminal and translate them into events for
185    /// the console or the machine.
186    async fn stdio_key_handler(on_key_tx: Sender<Key>) {
187        // TODO(jmmv): We should probably install a signal handler here to capture SIGINT and
188        // funnel it to the Machine via signals_rx, as we do in the raw_key_handler.  This would
189        // help ensure both consoles behave in the same way, but there is strictly no need for this
190        // because, when we do not configure the terminal in raw mode, we aren't capturing CTRL+C
191        // and the default system handler will work.
192
193        let mut buffer = VecDeque::default();
194
195        let mut done = false;
196        while !done {
197            let key = match read_key_from_stdin(&mut buffer) {
198                Ok(key) => key,
199                Err(_) => {
200                    // There is not much we can do if we get an error from stdin.
201                    Key::Unknown
202                }
203            };
204
205            done = key == Key::Eof;
206
207            // This should never fail but can if the receiver outruns the console because we don't
208            // await for the handler to terminate (which we cannot do safely because `Drop` is not
209            // async).
210            let _ = on_key_tx.send(key).await;
211        }
212
213        on_key_tx.close();
214    }
215
216    /// Flushes the console, which has already been written to via `lock`, if syncing is enabled.
217    fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> {
218        if self.sync_enabled { lock.flush() } else { Ok(()) }
219    }
220}
221
222#[async_trait(?Send)]
223impl InputOps for TerminalConsole {
224    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
225        match self.on_key_rx.try_recv() {
226            Ok(k) => Ok(Some(k)),
227            Err(TryRecvError::Empty) => Ok(None),
228            Err(TryRecvError::Closed) => Ok(Some(Key::Eof)),
229        }
230    }
231
232    async fn read_key(&mut self) -> io::Result<Key> {
233        match self.on_key_rx.recv().await {
234            Ok(k) => Ok(k),
235            Err(_) => Ok(Key::Eof),
236        }
237    }
238}
239
240#[async_trait(?Send)]
241impl Console for TerminalConsole {
242    fn clear(&mut self, how: ClearType) -> io::Result<()> {
243        let how = match how {
244            ClearType::All => terminal::ClearType::All,
245            ClearType::CurrentLine => terminal::ClearType::CurrentLine,
246            ClearType::PreviousChar => {
247                let stdout = io::stdout();
248                let mut stdout = stdout.lock();
249                stdout.write_all(b"\x08 \x08")?;
250                return self.maybe_flush(stdout);
251            }
252            ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
253        };
254        let stdout = io::stdout();
255        let mut stdout = stdout.lock();
256        stdout.queue(terminal::Clear(how))?;
257        if how == terminal::ClearType::All {
258            stdout.queue(cursor::MoveTo(0, 0))?;
259        }
260        self.maybe_flush(stdout)
261    }
262
263    fn color(&self) -> (Option<u8>, Option<u8>) {
264        (self.fg_color, self.bg_color)
265    }
266
267    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
268        if fg == self.fg_color && bg == self.bg_color {
269            return Ok(());
270        }
271
272        let stdout = io::stdout();
273        let mut stdout = stdout.lock();
274        if fg != self.fg_color {
275            let ct_fg = match fg {
276                None => style::Color::Reset,
277                Some(color) => style::Color::AnsiValue(color),
278            };
279            stdout.queue(style::SetForegroundColor(ct_fg))?;
280            self.fg_color = fg;
281        }
282        if bg != self.bg_color {
283            let ct_bg = match bg {
284                None => style::Color::Reset,
285                Some(color) => style::Color::AnsiValue(color),
286            };
287            stdout.queue(style::SetBackgroundColor(ct_bg))?;
288            self.bg_color = bg;
289        }
290        self.maybe_flush(stdout)
291    }
292
293    fn enter_alt(&mut self) -> io::Result<()> {
294        if !self.alt_active {
295            let stdout = io::stdout();
296            let mut stdout = stdout.lock();
297            stdout.queue(terminal::EnterAlternateScreen)?;
298            self.alt_active = true;
299            self.maybe_flush(stdout)
300        } else {
301            Ok(())
302        }
303    }
304
305    fn hide_cursor(&mut self) -> io::Result<()> {
306        if self.cursor_visible {
307            let stdout = io::stdout();
308            let mut stdout = stdout.lock();
309            stdout.queue(cursor::Hide)?;
310            self.cursor_visible = false;
311            self.maybe_flush(stdout)
312        } else {
313            Ok(())
314        }
315    }
316
317    fn is_interactive(&self) -> bool {
318        self.is_tty
319    }
320
321    fn leave_alt(&mut self) -> io::Result<()> {
322        if self.alt_active {
323            let stdout = io::stdout();
324            let mut stdout = stdout.lock();
325            stdout.queue(terminal::LeaveAlternateScreen)?;
326            self.alt_active = false;
327            self.maybe_flush(stdout)
328        } else {
329            Ok(())
330        }
331    }
332
333    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
334        #[cfg(debug_assertions)]
335        {
336            let size = self.size_chars()?;
337            assert!(pos.x < size.x);
338            assert!(pos.y < size.y);
339        }
340
341        let stdout = io::stdout();
342        let mut stdout = stdout.lock();
343        stdout.queue(cursor::MoveTo(pos.x, pos.y))?;
344        self.maybe_flush(stdout)
345    }
346
347    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
348        let stdout = io::stdout();
349        let mut stdout = stdout.lock();
350        match off.cmp(&0) {
351            Ordering::Less => stdout.queue(cursor::MoveLeft(-off as u16)),
352            Ordering::Equal => return Ok(()),
353            Ordering::Greater => stdout.queue(cursor::MoveRight(off as u16)),
354        }?;
355        self.maybe_flush(stdout)
356    }
357
358    fn print(&mut self, text: &str) -> io::Result<()> {
359        let text = remove_control_chars(text.to_owned());
360
361        let stdout = io::stdout();
362        let mut stdout = stdout.lock();
363        stdout.write_all(text.as_bytes())?;
364        if self.is_tty {
365            stdout.write_all(b"\r\n")?;
366        } else {
367            stdout.write_all(b"\n")?;
368        }
369        Ok(())
370    }
371
372    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
373        (self as &mut dyn InputOps).poll_key().await
374    }
375
376    async fn read_key(&mut self) -> io::Result<Key> {
377        (self as &mut dyn InputOps).read_key().await
378    }
379
380    fn show_cursor(&mut self) -> io::Result<()> {
381        if !self.cursor_visible {
382            let stdout = io::stdout();
383            let mut stdout = stdout.lock();
384            stdout.queue(cursor::Show)?;
385            self.cursor_visible = true;
386            self.maybe_flush(stdout)
387        } else {
388            Ok(())
389        }
390    }
391
392    fn size_chars(&self) -> io::Result<CharsXY> {
393        // Must be careful to not query the terminal size if both LINES and COLUMNS are set, because
394        // the query fails when we don't have a PTY and we still need to run under these conditions
395        // for testing purposes.
396        let lines = get_env_var_as_u16("LINES");
397        let columns = get_env_var_as_u16("COLUMNS");
398        let size = match (lines, columns) {
399            (Some(l), Some(c)) => CharsXY::new(c, l),
400            (l, c) => {
401                let (actual_columns, actual_lines) = terminal::size()?;
402                CharsXY::new(c.unwrap_or(actual_columns), l.unwrap_or(actual_lines))
403            }
404        };
405        Ok(size)
406    }
407
408    fn write(&mut self, text: &str) -> io::Result<()> {
409        let text = remove_control_chars(text.to_owned());
410
411        let stdout = io::stdout();
412        let mut stdout = stdout.lock();
413        stdout.write_all(text.as_bytes())?;
414        self.maybe_flush(stdout)
415    }
416
417    fn sync_now(&mut self) -> io::Result<()> {
418        if self.sync_enabled { Ok(()) } else { io::stdout().flush() }
419    }
420
421    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
422        if !self.sync_enabled {
423            io::stdout().flush()?;
424        }
425        let previous = self.sync_enabled;
426        self.sync_enabled = enabled;
427        Ok(previous)
428    }
429}