Skip to main content

endbasic_std/console/
mod.rs

1// EndBASIC
2// Copyright 2020 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//! Console representation and manipulation.
18
19use crate::Clearable;
20use crate::sound::{BEEP_TONE, Tone};
21use async_trait::async_trait;
22use std::cell::RefCell;
23use std::collections::VecDeque;
24use std::env;
25use std::io;
26use std::rc::Rc;
27use std::str;
28
29mod cmds;
30pub(crate) use cmds::add_all;
31mod colors;
32pub use colors::{AnsiColor, RGB, ansi_color_to_rgb, rgb_to_ansi_color};
33pub mod drawing;
34mod format;
35pub(crate) use format::refill_and_page;
36pub use format::refill_and_print;
37pub mod graphics;
38pub use graphics::GraphicsConsole;
39mod linebuffer;
40pub use linebuffer::LineBuffer;
41mod pager;
42pub(crate) use pager::Pager;
43mod readline;
44pub use readline::{read_line, read_line_secure};
45mod spec;
46pub use spec::{ConsoleSpec, ParseError, Resolution};
47mod trivial;
48pub use trivial::TrivialConsole;
49
50/// Decoded key presses as returned by the console.
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum Key {
53    /// The cursor down key.
54    ArrowDown,
55
56    /// The cursor left key.
57    ArrowLeft,
58
59    /// The cursor right key.
60    ArrowRight,
61
62    /// The cursor up key.
63    ArrowUp,
64
65    /// Deletes the previous character.
66    Backspace,
67
68    /// Accepts the current line.
69    CarriageReturn,
70
71    /// A printable character.
72    Char(char),
73
74    /// Deletes the current character.
75    Delete,
76
77    /// The end key or `Ctrl-E`.
78    End,
79
80    /// Indicates `Ctrl-D` in interactive consoles, or an end-of-input condition in raw ones.
81    ///
82    /// Consumers may interpret this as EOF or as delete depending on context.
83    EofOrDelete,
84
85    /// The escape key.
86    Escape,
87
88    /// Indicates a request for interrupt (e.g. `Ctrl-C`).
89    // TODO(jmmv): This (and maybe EofOrDelete too) should probably be represented as a more
90    // Control(char) value so that we can represent other control sequences and allow the logic in
91    // here to determine what to do with each.
92    Interrupt,
93
94    /// The home key or `Ctrl-A`.
95    Home,
96
97    /// Accepts the current line.
98    NewLine,
99
100    /// The Page Down key.
101    PageDown,
102
103    /// The Page Up key.
104    PageUp,
105
106    /// The Tab key.
107    Tab,
108
109    /// An unknown character or sequence.
110    Unknown,
111}
112
113/// Indicates what part of the console to clear on a `Console::clear()` call.
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub enum ClearType {
116    /// Clears the whole console and moves the cursor to the top left corner.
117    All,
118
119    /// Clears only the current line without moving the cursor.
120    CurrentLine,
121
122    /// Clears the previous character.
123    PreviousChar,
124
125    /// Clears from the cursor position to the end of the line without moving the cursor.
126    UntilNewLine,
127}
128
129/// Represents a coordinate for character-based console operations.
130#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
131pub struct CharsXY {
132    /// The column number, starting from zero.
133    pub x: u16,
134
135    /// The row number, starting from zero.
136    pub y: u16,
137}
138
139impl CharsXY {
140    /// Constructs a new coordinate at the given `(x, y)` position.
141    pub fn new(x: u16, y: u16) -> Self {
142        Self { x, y }
143    }
144}
145
146/// Represents a coordinate for pixel-based console operations.
147///
148/// Coordinates can be off-screen, which means they can be negative and/or can exceed the
149/// bottom-right margin.
150#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
151pub struct PixelsXY {
152    /// The column number.
153    pub x: i16,
154
155    /// The row number.
156    pub y: i16,
157}
158
159impl PixelsXY {
160    /// Constructs a new coordinate at the given `(x, y)` position.
161    pub fn new(x: i16, y: i16) -> Self {
162        Self { x, y }
163    }
164}
165
166#[cfg(test)]
167impl PixelsXY {
168    pub(crate) const TOP_LEFT: Self = Self { x: i16::MIN, y: i16::MIN };
169    pub(crate) const TOP_RIGHT: Self = Self { x: i16::MAX, y: i16::MIN };
170    pub(crate) const BOTTOM_LEFT: Self = Self { x: i16::MIN, y: i16::MAX };
171    pub(crate) const BOTTOM_RIGHT: Self = Self { x: i16::MAX, y: i16::MAX };
172}
173
174/// Represents a rectangular size in pixels.
175#[derive(Clone, Copy, Debug, PartialEq)]
176#[non_exhaustive]
177pub struct SizeInPixels {
178    /// The width in pixels.
179    pub width: u16,
180
181    /// The height in pixels.
182    pub height: u16,
183}
184
185impl SizeInPixels {
186    /// Construts a new size in pixels, validating that the quantities are non-zero.
187    pub fn new(width: u16, height: u16) -> Self {
188        debug_assert!(width > 0, "Zero widths don't make sense");
189        debug_assert!(height > 0, "Zero heights don't make sense");
190        Self { width, height }
191    }
192}
193
194#[cfg(test)]
195impl SizeInPixels {
196    pub(crate) const MAX: Self = Self { width: u16::MAX, height: u16::MAX };
197}
198
199/// Hooks to implement the commands that manipulate the console.
200#[async_trait(?Send)]
201pub trait Console {
202    /// Clears the part of the console given by `how`.
203    fn clear(&mut self, how: ClearType) -> io::Result<()>;
204
205    /// Gets the console's current foreground and background colors.
206    fn color(&self) -> (Option<u8>, Option<u8>);
207
208    /// Sets the console's foreground and background colors to `fg` and `bg`.
209    ///
210    /// If any of the colors is `None`, the color is left unchanged.
211    // TODO(jmmv): Clarify/fix this API contract.  Implementations currently treat `None` as a
212    // request to restore the default color instead of leaving the color unchanged.
213    fn set_color(&mut self, fg: Option<u8>, bg: Option<u8>) -> io::Result<()>;
214
215    /// Enters the alternate console.
216    // TODO(jmmv): This API leads to misuse as callers can forget to leave the alternate console.
217    fn enter_alt(&mut self) -> io::Result<()>;
218
219    /// Hides the cursor.
220    // TODO(jmmv): This API leads to misuse as callers can forget to show the cursor again.
221    fn hide_cursor(&mut self) -> io::Result<()>;
222
223    /// Returns true if the console is attached to an interactive terminal.  This controls whether
224    /// reading a line echoes back user input, for example.
225    fn is_interactive(&self) -> bool;
226
227    /// Leaves the alternate console.
228    fn leave_alt(&mut self) -> io::Result<()>;
229
230    /// Moves the cursor to the given position, which must be within the screen.
231    fn locate(&mut self, pos: CharsXY) -> io::Result<()>;
232
233    /// Moves the cursor within the line.  Positive values move right, negative values move left.
234    fn move_within_line(&mut self, off: i16) -> io::Result<()>;
235
236    /// Writes `text` to the console, followed by a newline or CRLF pair depending on the needs of
237    /// the console to advance a line.
238    ///
239    /// The input `text` is not supposed to contain any control characters, such as CR or LF.
240    // TODO(jmmv): Remove this in favor of write?
241    fn print(&mut self, text: &str) -> io::Result<()>;
242
243    /// Returns the next key press if any is available.
244    async fn poll_key(&mut self) -> io::Result<Option<Key>>;
245
246    /// Waits for and returns the next key press.
247    async fn read_key(&mut self) -> io::Result<Key>;
248
249    /// Shows the cursor.
250    fn show_cursor(&mut self) -> io::Result<()>;
251
252    /// Queries the size of the text console.
253    ///
254    /// The returned position represents the first row and column that lay *outside* of the console.
255    fn size_chars(&self) -> io::Result<CharsXY>;
256
257    /// Queries the size of the graphical console.
258    fn size_pixels(&self) -> io::Result<SizeInPixels> {
259        Err(io::Error::other("No graphics support in this console"))
260    }
261
262    /// Queries the size of a single glyph in the graphical console.
263    fn glyph_size(&self) -> io::Result<SizeInPixels> {
264        Err(io::Error::other("No graphics support in this console"))
265    }
266
267    /// Writes the text into the console at the position of the cursor.
268    ///
269    fn write(&mut self, text: &str) -> io::Result<()>;
270
271    /// Fills the 4-connected region around `_xy` using the current drawing color.
272    fn bucket_fill(&mut self, _xy: PixelsXY) -> io::Result<()> {
273        Err(io::Error::other("No graphics support in this console"))
274    }
275
276    /// Draws the outline of a circle at `_center` with `_radius` using the current drawing color.
277    fn draw_circle(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> {
278        Err(io::Error::other("No graphics support in this console"))
279    }
280
281    /// Draws a filled circle at `_center` with `_radius` using the current drawing color.
282    fn draw_circle_filled(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> {
283        Err(io::Error::other("No graphics support in this console"))
284    }
285
286    /// Draws a line from `_x1y1` to `_x2y2` using the current drawing color.
287    fn draw_line(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY) -> io::Result<()> {
288        Err(io::Error::other("No graphics support in this console"))
289    }
290
291    /// Draws a single pixel at `_xy` using the current drawing color.
292    fn draw_pixel(&mut self, _xy: PixelsXY) -> io::Result<()> {
293        Err(io::Error::other("No graphics support in this console"))
294    }
295
296    /// Draws the outline of a polygon using the current drawing color.
297    fn draw_poly(&mut self, _points: &[PixelsXY]) -> io::Result<()> {
298        Err(io::Error::other("No graphics support in this console"))
299    }
300
301    /// Draws a filled polygon using the current drawing color.
302    fn draw_poly_filled(&mut self, _points: &[PixelsXY]) -> io::Result<()> {
303        Err(io::Error::other("No graphics support in this console"))
304    }
305
306    /// Draws the outline of a rectangle from `_x1y1` to `_x2y2` using the current drawing color.
307    fn draw_rect(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY) -> io::Result<()> {
308        Err(io::Error::other("No graphics support in this console"))
309    }
310
311    /// Draws a filled rectangle from `_x1y1` to `_x2y2` using the current drawing color.
312    fn draw_rect_filled(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY) -> io::Result<()> {
313        Err(io::Error::other("No graphics support in this console"))
314    }
315
316    /// Draws the outline of a triangle using the current drawing color.
317    fn draw_tri(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY, _x3y3: PixelsXY) -> io::Result<()> {
318        Err(io::Error::other("No graphics support in this console"))
319    }
320
321    /// Draws a filled triangle using the current drawing color.
322    fn draw_tri_filled(
323        &mut self,
324        _x1y1: PixelsXY,
325        _x2y2: PixelsXY,
326        _x3y3: PixelsXY,
327    ) -> io::Result<()> {
328        Err(io::Error::other("No graphics support in this console"))
329    }
330
331    /// Returns the color number of the pixel at `_xy` if it is in bounds and exactly mappable.
332    fn peek_pixel(&self, _xy: PixelsXY) -> io::Result<Option<u8>> {
333        Err(io::Error::other("No graphics support in this console"))
334    }
335
336    /// Causes any buffered output to be synced.
337    ///
338    /// This is a no-op when video syncing is enabled because output is never buffered in that case.
339    fn sync_now(&mut self) -> io::Result<()>;
340
341    /// Enables or disables video syncing.
342    ///
343    /// When enabled, all graphical operations immediately updated the rendering target, which is
344    /// useful for interactive behavior because we want to see an immediate response.  When
345    /// disabled, all operations are buffered, which is useful for scripts (as otherwise rendering
346    /// is too slow).
347    ///
348    /// Flushes any pending updates when enabled.
349    ///
350    /// Returns the previous status of the video syncing flag.
351    fn set_sync(&mut self, _enabled: bool) -> io::Result<bool>;
352
353    /// Reproduces the standard canned beep tone.
354    async fn beep(&mut self) -> io::Result<()> {
355        self.play_tone(BEEP_TONE).await
356    }
357
358    /// Reproduces `tone` and returns once playback completes.
359    async fn play_tone(&mut self, _tone: Tone) -> io::Result<()> {
360        Err(io::Error::other("No audio support in this console"))
361    }
362}
363
364/// Trait to run the console's host on the main UI thread (if needed).
365pub trait ConsoleHost {
366    /// Executes the console's host event loop.
367    fn run(self: Box<Self>) -> io::Result<()>;
368}
369
370/// A console host that does nothing for all consoles which can run on a secondary thread.
371pub struct NoopConsoleHost;
372
373impl ConsoleHost for NoopConsoleHost {
374    fn run(self: Box<Self>) -> io::Result<()> {
375        Ok(())
376    }
377}
378
379/// Trait for an object that is able to instantiate a console.
380///
381/// Console objects are not `Send` so we must instantiate them within the thread that runs
382/// the REPL.  But in some cases, we need to create the console in a thread that's different than
383/// the one that will run it.  The factory allows us to prepare the console elsewhere, passing the
384/// Send-safe factory into the REPL thread.
385pub trait ConsoleFactory: Send {
386    /// Creates a new console and wires it up to inject signals into `signals_tx`.
387    fn build(self: Box<Self>) -> io::Result<Rc<RefCell<dyn Console>>>;
388}
389
390/// Resets the state of a console in a best-effort manner.
391pub(crate) struct ConsoleClearable {
392    console: Rc<RefCell<dyn Console>>,
393}
394
395impl ConsoleClearable {
396    /// Creates a new clearable for `console`.
397    pub(crate) fn new(console: Rc<RefCell<dyn Console>>) -> Box<Self> {
398        Box::from(Self { console })
399    }
400}
401
402impl Clearable for ConsoleClearable {
403    fn reset_state(&self) {
404        let mut console = self.console.borrow_mut();
405        let _ = console.leave_alt();
406        let _ = console.set_color(None, None);
407        let _ = console.show_cursor();
408        let _ = console.set_sync(true);
409    }
410}
411
412/// Checks if a given string has control characters.
413pub fn has_control_chars(s: &str) -> bool {
414    for ch in s.chars() {
415        if ch.is_control() {
416            return true;
417        }
418    }
419    false
420}
421
422/// Removes control characters from a string to make it suitable for printing.
423pub fn remove_control_chars<S: Into<String>>(s: S) -> String {
424    let s = s.into();
425
426    // Handle the expected common case first.  We use this function to strip control characters
427    // before printing them to the console, and thus we expect such input strings to rarely include
428    // control characters.
429    if !has_control_chars(&s) {
430        return s;
431    }
432
433    let mut o = String::with_capacity(s.len());
434    for ch in s.chars() {
435        if ch.is_control() {
436            o.push(' ');
437        } else {
438            o.push(ch);
439        }
440    }
441    o
442}
443
444/// Gets the value of the environment variable `name` and interprets it as a `u16`.  Returns
445/// `None` if the variable is not set or if its contents are invalid.
446pub fn get_env_var_as_u16(name: &str) -> Option<u16> {
447    match env::var_os(name) {
448        Some(value) => value.as_os_str().to_string_lossy().parse::<u16>().map(Some).unwrap_or(None),
449        None => None,
450    }
451}
452
453/// Converts a line of text into a collection of keys.
454fn line_to_keys(s: String) -> VecDeque<Key> {
455    let mut keys = VecDeque::default();
456    for ch in s.chars() {
457        if ch == '\x1b' {
458            keys.push_back(Key::Escape);
459        } else if ch == '\n' {
460            keys.push_back(Key::NewLine);
461        } else if ch == '\r' {
462            // Ignore.  When we run under Windows and use golden test input files, we end up
463            // seeing two separate characters to terminate a newline (CRLF) and these confuse
464            // our tests.  I am not sure why this doesn't seem to be a problem for interactive
465            // usage though, but it might just be that crossterm hides this from us.
466        } else if !ch.is_control() {
467            keys.push_back(Key::Char(ch));
468        } else {
469            keys.push_back(Key::Unknown);
470        }
471    }
472    keys
473}
474
475/// Reads a single key from stdin when not attached to a TTY.  Because characters are not
476/// visible to us until a newline is received, this reads complete lines and buffers them in
477/// memory inside the given `buffer`.
478pub fn read_key_from_stdin(buffer: &mut VecDeque<Key>) -> io::Result<Key> {
479    if buffer.is_empty() {
480        let mut line = String::new();
481        if io::stdin().read_line(&mut line)? == 0 {
482            return Ok(Key::EofOrDelete);
483        }
484        *buffer = line_to_keys(line);
485    }
486    match buffer.pop_front() {
487        Some(key) => Ok(key),
488        None => Ok(Key::EofOrDelete),
489    }
490}
491
492/// Returns true if the console is too narrow for the standard interface.
493///
494/// A narrow console is defined as one that cannot fit the welcome message.
495pub fn is_narrow(console: &dyn Console) -> bool {
496    match console.size_chars() {
497        Ok(size) => size.x < 50,
498        Err(_) => false,
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    #[test]
507    fn test_has_control_chars() {
508        assert!(!has_control_chars(""));
509        assert!(!has_control_chars("foo bar^baz"));
510
511        assert!(has_control_chars("foo\nbar"));
512        assert!(has_control_chars("foo\rbar"));
513        assert!(has_control_chars("foo\x08bar"));
514    }
515
516    #[test]
517    fn test_remove_control_chars() {
518        assert_eq!("", remove_control_chars(""));
519        assert_eq!("foo bar", remove_control_chars("foo bar"));
520        assert_eq!("foo  bar baz ", remove_control_chars("foo\r\nbar\rbaz\n"));
521    }
522}