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}