Skip to main content

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
18use async_channel::{Receiver, Sender, TryRecvError};
19use async_trait::async_trait;
20use crossterm::event::{self, KeyEventKind};
21use crossterm::tty::IsTty;
22use crossterm::{QueueableCommand, cursor, style, terminal};
23use endbasic_core::exec::Signal;
24use endbasic_std::console::graphics::InputOps;
25use endbasic_std::console::{
26    CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars,
27};
28use std::cmp::Ordering;
29use std::collections::VecDeque;
30use std::io::{self, StdoutLock, Write};
31
32/// Implementation of the EndBASIC console to interact with stdin and stdout.
33pub struct TerminalConsole {
34    /// Whether stdin and stdout are attached to a TTY.  When this is true, the console is put in
35    /// raw mode for finer-grained control.
36    is_tty: bool,
37
38    /// Current foreground color.
39    fg_color: Option<u8>,
40
41    /// Current background color.
42    bg_color: Option<u8>,
43
44    /// Whether the cursor is visible or not.
45    cursor_visible: bool,
46
47    /// Whether we are in the alternate console or not.
48    alt_active: bool,
49
50    /// Whether video syncing is enabled or not.
51    sync_enabled: bool,
52
53    /// Channel to receive key presses from the terminal.
54    on_key_rx: Receiver<Key>,
55}
56
57impl Drop for TerminalConsole {
58    fn drop(&mut self) {
59        if self.is_tty {
60            terminal::disable_raw_mode().unwrap();
61        }
62    }
63}
64
65impl TerminalConsole {
66    /// Creates a new console based on the properties of stdin/stdout.
67    ///
68    /// This spawns a background task to handle console input so this must be run in the context of
69    /// an Tokio runtime.
70    pub fn from_stdio(signals_tx: Sender<Signal>) -> io::Result<Self> {
71        let (terminal, _on_key_tx) = Self::from_stdio_with_injector(signals_tx)?;
72        Ok(terminal)
73    }
74
75    /// Creates a new console based on the properties of stdin/stdout.
76    ///
77    /// This spawns a background task to handle console input so this must be run in the context of
78    /// an Tokio runtime.
79    ///
80    /// Compared to `from_stdio`, this also returns a key sender to inject extra events into the
81    /// queue maintained by the terminal.
82    pub fn from_stdio_with_injector(signals_tx: Sender<Signal>) -> io::Result<(Self, Sender<Key>)> {
83        let (on_key_tx, on_key_rx) = async_channel::unbounded();
84
85        let is_tty = io::stdin().is_tty() && io::stdout().is_tty();
86
87        if is_tty {
88            terminal::enable_raw_mode()?;
89            tokio::task::spawn(TerminalConsole::raw_key_handler(on_key_tx.clone(), signals_tx));
90        } else {
91            tokio::task::spawn(TerminalConsole::stdio_key_handler(on_key_tx.clone()));
92        }
93
94        Ok((
95            Self {
96                is_tty,
97                fg_color: None,
98                bg_color: None,
99                cursor_visible: true,
100                alt_active: false,
101                sync_enabled: true,
102                on_key_rx,
103            },
104            on_key_tx,
105        ))
106    }
107
108    /// Async task to wait for key events on a raw terminal and translate them into events for the
109    /// console or the machine.
110    async fn raw_key_handler(on_key_tx: Sender<Key>, signals_tx: Sender<Signal>) {
111        use event::{KeyCode, KeyModifiers};
112
113        let mut done = false;
114        while !done {
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            done = key == Key::Eof;
167            if key == Key::Interrupt {
168                // Handling CTRL+C in this way isn't great because this is not the same as handling
169                // SIGINT on Unix builds.  First, we are unable to stop long-running operations like
170                // sleeps; and second, a real SIGINT will kill the interpreter completely instead of
171                // coming this way.  We need a real signal handler and we probably should not be
172                // running in raw mode all the time.
173                signals_tx
174                    .send(Signal::Break)
175                    .await
176                    .expect("Send to unbounded channel should not have failed")
177            }
178
179            // This should never fail but can if the receiver outruns the console because we
180            // don't await for the handler to terminate (which we cannot do safely because
181            // `Drop` is not async).
182            let _ = on_key_tx.send(key).await;
183        }
184
185        signals_tx.close();
186        on_key_tx.close();
187    }
188
189    /// Async task to wait for key events on a non-raw terminal and translate them into events for
190    /// the console or the machine.
191    async fn stdio_key_handler(on_key_tx: Sender<Key>) {
192        // TODO(jmmv): We should probably install a signal handler here to capture SIGINT and
193        // funnel it to the Machine via signals_rx, as we do in the raw_key_handler.  This would
194        // help ensure both consoles behave in the same way, but there is strictly no need for this
195        // because, when we do not configure the terminal in raw mode, we aren't capturing CTRL+C
196        // and the default system handler will work.
197
198        let mut buffer = VecDeque::default();
199
200        let mut done = false;
201        while !done {
202            let key = match read_key_from_stdin(&mut buffer) {
203                Ok(key) => key,
204                Err(_) => {
205                    // There is not much we can do if we get an error from stdin.
206                    Key::Unknown
207                }
208            };
209
210            done = key == Key::Eof;
211
212            // This should never fail but can if the receiver outruns the console because we don't
213            // await for the handler to terminate (which we cannot do safely because `Drop` is not
214            // async).
215            let _ = on_key_tx.send(key).await;
216        }
217
218        on_key_tx.close();
219    }
220
221    /// Flushes the console, which has already been written to via `lock`, if syncing is enabled.
222    fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> {
223        if self.sync_enabled { lock.flush() } else { Ok(()) }
224    }
225}
226
227#[async_trait(?Send)]
228impl InputOps for TerminalConsole {
229    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
230        match self.on_key_rx.try_recv() {
231            Ok(k) => Ok(Some(k)),
232            Err(TryRecvError::Empty) => Ok(None),
233            Err(TryRecvError::Closed) => Ok(Some(Key::Eof)),
234        }
235    }
236
237    async fn read_key(&mut self) -> io::Result<Key> {
238        match self.on_key_rx.recv().await {
239            Ok(k) => Ok(k),
240            Err(_) => Ok(Key::Eof),
241        }
242    }
243}
244
245#[async_trait(?Send)]
246impl Console for TerminalConsole {
247    fn clear(&mut self, how: ClearType) -> io::Result<()> {
248        let how = match how {
249            ClearType::All => terminal::ClearType::All,
250            ClearType::CurrentLine => terminal::ClearType::CurrentLine,
251            ClearType::PreviousChar => {
252                let stdout = io::stdout();
253                let mut stdout = stdout.lock();
254                stdout.write_all(b"\x08 \x08")?;
255                return self.maybe_flush(stdout);
256            }
257            ClearType::UntilNewLine => terminal::ClearType::UntilNewLine,
258        };
259        let stdout = io::stdout();
260        let mut stdout = stdout.lock();
261        stdout.queue(terminal::Clear(how))?;
262        if how == terminal::ClearType::All {
263            stdout.queue(cursor::MoveTo(0, 0))?;
264        }
265        self.maybe_flush(stdout)
266    }
267
268    fn color(&self) -> (Option<u8>, Option<u8>) {
269        (self.fg_color, self.bg_color)
270    }
271
272    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()> {
273        if fg == self.fg_color && bg == self.bg_color {
274            return Ok(());
275        }
276
277        let stdout = io::stdout();
278        let mut stdout = stdout.lock();
279        if fg != self.fg_color {
280            let ct_fg = match fg {
281                None => style::Color::Reset,
282                Some(color) => style::Color::AnsiValue(color),
283            };
284            stdout.queue(style::SetForegroundColor(ct_fg))?;
285            self.fg_color = fg;
286        }
287        if bg != self.bg_color {
288            let ct_bg = match bg {
289                None => style::Color::Reset,
290                Some(color) => style::Color::AnsiValue(color),
291            };
292            stdout.queue(style::SetBackgroundColor(ct_bg))?;
293            self.bg_color = bg;
294        }
295        self.maybe_flush(stdout)
296    }
297
298    fn enter_alt(&mut self) -> io::Result<()> {
299        if !self.alt_active {
300            let stdout = io::stdout();
301            let mut stdout = stdout.lock();
302            stdout.queue(terminal::EnterAlternateScreen)?;
303            self.alt_active = true;
304            self.maybe_flush(stdout)
305        } else {
306            Ok(())
307        }
308    }
309
310    fn hide_cursor(&mut self) -> io::Result<()> {
311        if self.cursor_visible {
312            let stdout = io::stdout();
313            let mut stdout = stdout.lock();
314            stdout.queue(cursor::Hide)?;
315            self.cursor_visible = false;
316            self.maybe_flush(stdout)
317        } else {
318            Ok(())
319        }
320    }
321
322    fn is_interactive(&self) -> bool {
323        self.is_tty
324    }
325
326    fn leave_alt(&mut self) -> io::Result<()> {
327        if self.alt_active {
328            let stdout = io::stdout();
329            let mut stdout = stdout.lock();
330            stdout.queue(terminal::LeaveAlternateScreen)?;
331            self.alt_active = false;
332            self.maybe_flush(stdout)
333        } else {
334            Ok(())
335        }
336    }
337
338    fn locate(&mut self, pos: CharsXY) -> io::Result<()> {
339        #[cfg(debug_assertions)]
340        {
341            let size = self.size_chars()?;
342            assert!(pos.x < size.x);
343            assert!(pos.y < size.y);
344        }
345
346        let stdout = io::stdout();
347        let mut stdout = stdout.lock();
348        stdout.queue(cursor::MoveTo(pos.x, pos.y))?;
349        self.maybe_flush(stdout)
350    }
351
352    fn move_within_line(&mut self, off: i16) -> io::Result<()> {
353        let stdout = io::stdout();
354        let mut stdout = stdout.lock();
355        match off.cmp(&0) {
356            Ordering::Less => stdout.queue(cursor::MoveLeft(-off as u16)),
357            Ordering::Equal => return Ok(()),
358            Ordering::Greater => stdout.queue(cursor::MoveRight(off as u16)),
359        }?;
360        self.maybe_flush(stdout)
361    }
362
363    fn print(&mut self, text: &str) -> io::Result<()> {
364        let text = remove_control_chars(text.to_owned());
365
366        let stdout = io::stdout();
367        let mut stdout = stdout.lock();
368        stdout.write_all(text.as_bytes())?;
369        if self.is_tty {
370            stdout.write_all(b"\r\n")?;
371        } else {
372            stdout.write_all(b"\n")?;
373        }
374        Ok(())
375    }
376
377    async fn poll_key(&mut self) -> io::Result<Option<Key>> {
378        (self as &mut dyn InputOps).poll_key().await
379    }
380
381    async fn read_key(&mut self) -> io::Result<Key> {
382        (self as &mut dyn InputOps).read_key().await
383    }
384
385    fn show_cursor(&mut self) -> io::Result<()> {
386        if !self.cursor_visible {
387            let stdout = io::stdout();
388            let mut stdout = stdout.lock();
389            stdout.queue(cursor::Show)?;
390            self.cursor_visible = true;
391            self.maybe_flush(stdout)
392        } else {
393            Ok(())
394        }
395    }
396
397    fn size_chars(&self) -> io::Result<CharsXY> {
398        // Must be careful to not query the terminal size if both LINES and COLUMNS are set, because
399        // the query fails when we don't have a PTY and we still need to run under these conditions
400        // for testing purposes.
401        let lines = get_env_var_as_u16("LINES");
402        let columns = get_env_var_as_u16("COLUMNS");
403        let size = match (lines, columns) {
404            (Some(l), Some(c)) => CharsXY::new(c, l),
405            (l, c) => {
406                let (actual_columns, actual_lines) = terminal::size()?;
407                CharsXY::new(c.unwrap_or(actual_columns), l.unwrap_or(actual_lines))
408            }
409        };
410        Ok(size)
411    }
412
413    fn write(&mut self, text: &str) -> io::Result<()> {
414        let text = remove_control_chars(text.to_owned());
415
416        let stdout = io::stdout();
417        let mut stdout = stdout.lock();
418        stdout.write_all(text.as_bytes())?;
419        self.maybe_flush(stdout)
420    }
421
422    fn sync_now(&mut self) -> io::Result<()> {
423        if self.sync_enabled { Ok(()) } else { io::stdout().flush() }
424    }
425
426    fn set_sync(&mut self, enabled: bool) -> io::Result<bool> {
427        if !self.sync_enabled {
428            io::stdout().flush()?;
429        }
430        let previous = self.sync_enabled;
431        self.sync_enabled = enabled;
432        Ok(previous)
433    }
434}