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::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
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 endbasic_std::sound::BEEP_TONE;
30use futures_util::StreamExt;
31use std::cmp::Ordering;
32use std::collections::VecDeque;
33use std::io::{self, StdoutLock, Write};
34use std::thread;
35
36/// Converts a crossterm `KeyEvent` given in `ev` for key presses to our own `Key` type.
37fn parse_key_event(ev: KeyEvent) -> Option<Key> {
38    if ev.kind != KeyEventKind::Press {
39        return None;
40    }
41
42    let key = match ev.code {
43        KeyCode::Backspace => Key::Backspace,
44        KeyCode::Delete => Key::Delete,
45        KeyCode::End => Key::End,
46        KeyCode::Esc => Key::Escape,
47        KeyCode::Home => Key::Home,
48        KeyCode::Tab => Key::Tab,
49        KeyCode::Up => Key::ArrowUp,
50        KeyCode::Down => Key::ArrowDown,
51        KeyCode::Left => Key::ArrowLeft,
52        KeyCode::Right => Key::ArrowRight,
53        KeyCode::PageDown => Key::PageDown,
54        KeyCode::PageUp => Key::PageUp,
55        KeyCode::Char('a') if ev.modifiers == KeyModifiers::CONTROL => Key::Home,
56        KeyCode::Char('b') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowLeft,
57        KeyCode::Char('c') if ev.modifiers == KeyModifiers::CONTROL => Key::Interrupt,
58        KeyCode::Char('d') if ev.modifiers == KeyModifiers::CONTROL => Key::EofOrDelete,
59        KeyCode::Char('e') if ev.modifiers == KeyModifiers::CONTROL => Key::End,
60        KeyCode::Char('f') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowRight,
61        KeyCode::Char('j') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
62        KeyCode::Char('m') if ev.modifiers == KeyModifiers::CONTROL => Key::NewLine,
63        KeyCode::Char('n') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowDown,
64        KeyCode::Char('p') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowUp,
65        KeyCode::Char(ch) => Key::Char(ch),
66        KeyCode::Enter => Key::NewLine,
67        _ => Key::Unknown,
68    };
69    Some(key)
70}
71
72/// Implementation of the EndBASIC console to interact with stdin and stdout.
73pub struct TerminalConsole {
74    /// Whether stdin and stdout are attached to a TTY.  When this is true, the console is put in
75    /// raw mode for finer-grained control.
76    is_tty: bool,
77
78    /// Current foreground color.
79    fg_color: Option<u8>,
80
81    /// Current background color.
82    bg_color: Option<u8>,
83
84    /// Whether the cursor is visible or not.
85    cursor_visible: bool,
86
87    /// Whether we are in the alternate console or not.
88    alt_active: bool,
89
90    /// Whether video syncing is enabled or not.
91    sync_enabled: bool,
92
93    /// Channel to receive key presses from the terminal.
94    on_key_rx: Receiver<Key>,
95}
96
97impl Drop for TerminalConsole {
98    fn drop(&mut self) {
99        self.on_key_rx.close();
100        if self.is_tty {
101            terminal::disable_raw_mode().unwrap();
102        }
103    }
104}
105
106impl TerminalConsole {
107    /// Creates a new console based on the properties of stdin/stdout.
108    ///
109    /// This spawns a background task to handle console input so this must be run in the context of
110    /// an Tokio runtime.
111    pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
112        let (terminal, _on_key_tx) = Self::from_stdio_with_injector(signals_tx)?;
113        Ok(terminal)
114    }
115
116    /// Creates a new console based on the properties of stdin/stdout.
117    ///
118    /// This spawns a background task to handle console input so this must be run in the context of
119    /// an Tokio runtime.
120    ///
121    /// Compared to `from_stdio`, this also returns a key sender to inject extra events into the
122    /// queue maintained by the terminal.
123    pub fn from_stdio_with_injector(signals_tx: Sender<Signal>) -> io::Result<(Self, Sender<Key>)> {
124        let (on_key_tx, on_key_rx) = async_channel::unbounded();
125
126        let is_tty = io::stdin().is_tty() && io::stdout().is_tty();
127
128        if is_tty {
129            terminal::enable_raw_mode()?;
130            tokio::task::spawn(TerminalConsole::raw_key_handler(
131                EventStream::new(),
132                on_key_tx.clone(),
133                signals_tx,
134            ));
135        } else {
136            tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx.clone()));
137        }
138
139        Ok((
140            Self {
141                is_tty,
142                fg_color: None,
143                bg_color: None,
144                cursor_visible: true,
145                alt_active: false,
146                sync_enabled: true,
147                on_key_rx,
148            },
149            on_key_tx,
150        ))
151    }
152
153    /// Async task to wait for key events on a raw terminal and translate them into events for the
154    /// console or the machine.
155    async fn raw_key_handler<S>(mut events: S, on_key_tx: Sender<Key>, signals_tx: Sender<Signal>)
156    where
157        S: futures_util::stream::Stream<Item = io::Result<Event>> + Unpin,
158    {
159        loop {
160            let key = tokio::select! {
161                _ = on_key_tx.closed() => break,
162                maybe_event = events.next() => match maybe_event {
163                    Some(Ok(Event::Key(ev))) => match parse_key_event(ev) {
164                        Some(key) => key,
165                        None => continue,
166                    },
167                    Some(Ok(_)) => continue,
168                    Some(Err(_)) => Key::Unknown,
169                    None => break,
170                }
171            };
172
173            if key == Key::Interrupt {
174                // Handling CTRL+C in this way isn't great because this is not the same as handling
175                // SIGINT on Unix builds.  First, we are unable to stop long-running operations like
176                // sleeps; and second, a real SIGINT will kill the interpreter completely instead of
177                // coming this way.  We need a real signal handler and we probably should not be
178                // running in raw mode all the time.
179                if signals_tx.send(Signal::Break).await.is_err() {
180                    break;
181                }
182            }
183
184            // Exit the background task if the console receiver has gone away.
185            if on_key_tx.send(key).await.is_err() {
186                break;
187            }
188        }
189    }
190
191    /// Async task to wait for key events on a non-raw terminal and translate them into events for
192    /// the console or the machine.
193    async fn stdio_key_handler(on_key_tx: Sender<Key>) {
194        // TODO(jmmv): We should probably install a signal handler here to capture SIGINT and
195        // funnel it to the Machine via signals_rx, as we do in the raw_key_handler.  This would
196        // help ensure both consoles behave in the same way, but there is strictly no need for this
197        // because, when we do not configure the terminal in raw mode, we aren't capturing CTRL+C
198        // and the default system handler will work.
199
200        let mut buffer = VecDeque::default();
201
202        let mut done = false;
203        while !done {
204            if on_key_tx.is_closed() {
205                break;
206            }
207
208            let key = match read_key_from_stdin(&mut buffer) {
209                Ok(key) => key,
210                Err(_) => {
211                    // There is not much we can do if we get an error from stdin.
212                    Key::Unknown
213                }
214            };
215
216            done = key == Key::EofOrDelete;
217
218            // This should never fail but can if the receiver outruns the console because we don't
219            // await for the handler to terminate (which we cannot do safely because `Drop` is not
220            // async).
221            if on_key_tx.send(key).await.is_err() {
222                break;
223            }
224        }
225
226        on_key_tx.close();
227    }
228
229    /// Flushes the console, which has already been written to via `lock`, if syncing is enabled.
230    fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> {
231        if self.sync_enabled { lock.flush() } else { Ok(()) }
232    }
233}
234
235#[async_trait(?Send)]
236impl InputOps for TerminalConsole {
237    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
238        match self.on_key_rx.try_recv() {
239            Ok(k) => Ok(Some(k)),
240            Err(TryRecvError::Empty) => Ok(None),
241            Err(TryRecvError::Closed) => Ok(Some(Key::EofOrDelete)),
242        }
243    }
244
245    async fn read_key(&mut self) -> io::Result<Key> {
246        match self.on_key_rx.recv().await {
247            Ok(k) => Ok(k),
248            Err(_) => Ok(Key::EofOrDelete),
249        }
250    }
251}
252
253#[async_trait(?Send)]
254impl Console for TerminalConsole {
255    fn clear(&mut self, how: ClearType) -> io::Result<()> {
256        let how = match how {
257            ClearType::All => terminal::ClearType::All,
258            ClearType::CurrentLine => terminal::ClearType::CurrentLine,
259            ClearType::PreviousChar => {
260                let stdout = io::stdout();
261                let mut stdout = stdout.lock();
262                stdout.write_all(b"\x08 \x08")?;
263                return self.maybe_flush(stdout);
264            }
265            ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
266        };
267        let stdout = io::stdout();
268        let mut stdout = stdout.lock();
269        stdout.queue(terminal::Clear(how))?;
270        if how == terminal::ClearType::All {
271            stdout.queue(cursor::MoveTo(0, 0))?;
272        }
273        self.maybe_flush(stdout)
274    }
275
276    fn color(&self) -> (Option<u8>, Option<u8>) {
277        (self.fg_color, self.bg_color)
278    }
279
280    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
281        if fg == self.fg_color && bg == self.bg_color {
282            return Ok(());
283        }
284
285        let stdout = io::stdout();
286        let mut stdout = stdout.lock();
287        if fg != self.fg_color {
288            let ct_fg = match fg {
289                None => style::Color::Reset,
290                Some(color) => style::Color::AnsiValue(color),
291            };
292            stdout.queue(style::SetForegroundColor(ct_fg))?;
293            self.fg_color = fg;
294        }
295        if bg != self.bg_color {
296            let ct_bg = match bg {
297                None => style::Color::Reset,
298                Some(color) => style::Color::AnsiValue(color),
299            };
300            stdout.queue(style::SetBackgroundColor(ct_bg))?;
301            self.bg_color = bg;
302        }
303        self.maybe_flush(stdout)
304    }
305
306    fn enter_alt(&mut self) -> io::Result<()> {
307        if !self.alt_active {
308            let stdout = io::stdout();
309            let mut stdout = stdout.lock();
310            stdout.queue(terminal::EnterAlternateScreen)?;
311            self.alt_active = true;
312            self.maybe_flush(stdout)
313        } else {
314            Ok(())
315        }
316    }
317
318    fn hide_cursor(&mut self) -> io::Result<()> {
319        if self.cursor_visible {
320            let stdout = io::stdout();
321            let mut stdout = stdout.lock();
322            stdout.queue(cursor::Hide)?;
323            self.cursor_visible = false;
324            self.maybe_flush(stdout)
325        } else {
326            Ok(())
327        }
328    }
329
330    fn is_interactive(&self) -> bool {
331        self.is_tty
332    }
333
334    fn leave_alt(&mut self) -> io::Result<()> {
335        if self.alt_active {
336            let stdout = io::stdout();
337            let mut stdout = stdout.lock();
338            stdout.queue(terminal::LeaveAlternateScreen)?;
339            self.alt_active = false;
340            self.maybe_flush(stdout)
341        } else {
342            Ok(())
343        }
344    }
345
346    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
347        #[cfg(debug_assertions)]
348        {
349            let size = self.size_chars()?;
350            assert!(pos.x < size.x);
351            assert!(pos.y < size.y);
352        }
353
354        let stdout = io::stdout();
355        let mut stdout = stdout.lock();
356        stdout.queue(cursor::MoveTo(pos.x, pos.y))?;
357        self.maybe_flush(stdout)
358    }
359
360    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
361        let stdout = io::stdout();
362        let mut stdout = stdout.lock();
363        match off.cmp(&0) {
364            Ordering::Less => stdout.queue(cursor::MoveLeft(-off as u16)),
365            Ordering::Equal => return Ok(()),
366            Ordering::Greater => stdout.queue(cursor::MoveRight(off as u16)),
367        }?;
368        self.maybe_flush(stdout)
369    }
370
371    fn print(&mut self, text: &str) -> io::Result<()> {
372        let text = remove_control_chars(text.to_owned());
373
374        let stdout = io::stdout();
375        let mut stdout = stdout.lock();
376        stdout.write_all(text.as_bytes())?;
377        if self.is_tty {
378            stdout.write_all(b"\r\n")?;
379        } else {
380            stdout.write_all(b"\n")?;
381        }
382        Ok(())
383    }
384
385    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
386        (self as &mut dyn InputOps).poll_key().await
387    }
388
389    async fn read_key(&mut self) -> io::Result<Key> {
390        (self as &mut dyn InputOps).read_key().await
391    }
392
393    fn show_cursor(&mut self) -> io::Result<()> {
394        if !self.cursor_visible {
395            let stdout = io::stdout();
396            let mut stdout = stdout.lock();
397            stdout.queue(cursor::Show)?;
398            self.cursor_visible = true;
399            self.maybe_flush(stdout)
400        } else {
401            Ok(())
402        }
403    }
404
405    fn size_chars(&self) -> io::Result<CharsXY> {
406        // Must be careful to not query the terminal size if both LINES and COLUMNS are set, because
407        // the query fails when we don't have a PTY and we still need to run under these conditions
408        // for testing purposes.
409        let lines = get_env_var_as_u16("LINES");
410        let columns = get_env_var_as_u16("COLUMNS");
411        let size = match (lines, columns) {
412            (Some(l), Some(c)) => CharsXY::new(c, l),
413            (l, c) => {
414                let (actual_columns, actual_lines) = terminal::size()?;
415                CharsXY::new(c.unwrap_or(actual_columns), l.unwrap_or(actual_lines))
416            }
417        };
418        Ok(size)
419    }
420
421    fn write(&mut self, text: &str) -> io::Result<()> {
422        let text = remove_control_chars(text.to_owned());
423
424        let stdout = io::stdout();
425        let mut stdout = stdout.lock();
426        stdout.write_all(text.as_bytes())?;
427        self.maybe_flush(stdout)
428    }
429
430    fn sync_now(&mut self) -> io::Result<()> {
431        if self.sync_enabled { Ok(()) } else { io::stdout().flush() }
432    }
433
434    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
435        if !self.sync_enabled {
436            io::stdout().flush()?;
437        }
438        let previous = self.sync_enabled;
439        self.sync_enabled = enabled;
440        Ok(previous)
441    }
442
443    async fn beep(&mut self) -> io::Result<()> {
444        let stdout = io::stdout();
445        let mut stdout = stdout.lock();
446        stdout.write_all(b"\x07")?;
447        stdout.flush()?;
448        thread::sleep(BEEP_TONE.duration);
449        Ok(())
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use futures_util::stream;
457    use std::thread;
458
459    #[test]
460    fn test_parse_key_event_delete() {
461        assert_eq!(
462            Some(Key::Delete),
463            parse_key_event(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE))
464        );
465    }
466
467    #[test]
468    fn test_drive_raw_keys_eof_or_delete_does_not_terminate_worker() {
469        let (on_key_tx, on_key_rx) = async_channel::unbounded();
470        let (signals_tx, _signals_rx) = async_channel::unbounded();
471        let events = stream::iter([Ok(Event::Key(KeyEvent::new(
472            KeyCode::Char('d'),
473            KeyModifiers::CONTROL,
474        )))]);
475
476        let handle = thread::spawn(move || {
477            let runtime =
478                tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
479            runtime.block_on(TerminalConsole::raw_key_handler(events, on_key_tx, signals_tx));
480        });
481
482        assert_eq!(Key::EofOrDelete, on_key_rx.recv_blocking().unwrap());
483
484        on_key_rx.close();
485        handle.join().unwrap();
486    }
487
488    #[test]
489    fn test_drive_raw_keys_shutdown_without_extra_key() {
490        let (on_key_tx, on_key_rx) = async_channel::unbounded();
491        let (signals_tx, _signals_rx) = async_channel::unbounded();
492        let events = stream::pending();
493
494        let handle = thread::spawn(move || {
495            let runtime =
496                tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
497            runtime.block_on(TerminalConsole::raw_key_handler(events, on_key_tx, signals_tx));
498        });
499
500        on_key_rx.close();
501        handle.join().unwrap();
502
503        assert_eq!(Err(TryRecvError::Closed), on_key_rx.try_recv());
504    }
505}