endbasic_terminal/
lib.rs

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