Skip to main content

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