endbasic_std/console/
mod.rs

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