console_traits/
lib.rs

1//! # Console Traits
2//!
3//! Contains a trait which describes a console. A console is a rectangular monospaced text display, of a certain width and height. You can write Unicode text to it.
4//!
5//! Currently we assume UNIX LF sematics - that is a sole LF implies a new
6//! line *and* carriage return (as distinct to Windows semantics where you
7//! would need to send a CRLF pair).
8//!
9//! Implementors should handle the following Unicode characters specially:
10//!
11//! * 0x08 (BS)  - Backspaces one character (and erases it)
12//! * 0x09 (TAB) - Move to next tab stop, or the end of the line if no tab stops left.
13//! * 0x0A (LF)  - Line feed.
14//! * 0x0D (CR)  - Carriage return.
15//! * 0x7F (DEL) - Ignored.
16#![cfg_attr(not(test), no_std)]
17
18#[cfg(test)]
19use std as core;
20
21/// Identifies a horizontal row on the screen. Zero is at the top.
22#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Copy, Clone)]
23pub struct Row(pub u8);
24
25/// Describes a vertical column on the screen. Zero is on the left.
26#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Copy, Clone)]
27pub struct Col(pub u8);
28
29/// Describes a place on the screen. (0, 0) is the top left.
30#[derive(Debug, Copy, Clone)]
31pub struct Position {
32    /// The horizontal row (zero at the top)
33    pub row: Row,
34    /// The vertical column (zero on the left)
35    pub col: Col,
36}
37
38#[derive(Debug, Copy, Clone)]
39pub enum EscapeCharMode {
40    Waiting,
41    Seen,
42}
43
44/// How to handle Control Characters
45#[derive(Debug, Copy, Clone)]
46pub enum ControlCharMode {
47    Interpret,
48    Display,
49}
50
51#[derive(Debug, Copy, Clone)]
52/// Special types of character we need to interpret
53pub enum SpecialChar {
54    Linefeed,
55    CarriageReturn,
56    Tab,
57    Backspace,
58    Delete,
59    Escape,
60}
61
62/// Abstraction for our console. We can move the cursor around and write text
63/// to it. You should use either `UnicodeConsole` or `AsciiConsole` depending
64/// on whether you want full Unicode support (`&str`, `char`, etc), or just
65/// 8-bit characters (`&[u8]` and `u8`).
66pub trait BaseConsole {
67    type Error;
68
69    /// Gets the last col on the screen.
70    fn get_width(&self) -> Col;
71
72    /// Gets the last row on the screen.
73    fn get_height(&self) -> Row;
74
75    /// Set the horizontal position for the next text output.
76    fn set_col(&mut self, col: Col) -> Result<(), Self::Error>;
77
78    /// Set the vertical position for the next text output.
79    fn set_row(&mut self, row: Row) -> Result<(), Self::Error>;
80
81    /// Set the horizontal and vertical position for the next text output.
82    fn set_pos(&mut self, pos: Position) -> Result<(), Self::Error>;
83
84    /// Set the horizontal and vertical position for the next text output.
85    /// Don't bounds check the value, we've already done it.
86    fn set_pos_unbounded(&mut self, pos: Position) {
87        let _ = self.set_pos(pos);
88    }
89
90    /// Get the current screen position.
91    fn get_pos(&self) -> Position;
92
93    /// Set the control char mode
94    fn set_control_char_mode(&mut self, mode: ControlCharMode);
95
96    /// Get the current control char mode
97    fn get_control_char_mode(&self) -> ControlCharMode;
98
99    /// Set the escape char mode
100    fn set_escape_char_mode(&mut self, mode: EscapeCharMode);
101
102    /// Get the current escape char mode
103    fn get_escape_char_mode(&self) -> EscapeCharMode;
104
105    /// Called when the screen needs to scroll up one row.
106    fn scroll_screen(&mut self) -> Result<(), Self::Error>;
107
108    /// Move the current cursor right one position. Wraps at the end of the
109    /// line. Returns Ok(true) if the screen needs to scroll, or Ok(false)
110    /// if it does not.
111    fn move_cursor_right(&mut self) -> Result<(), Self::Error> {
112        let mut pos = self.get_pos();
113        if pos.col < self.get_width() {
114            // we'll still be on screen
115            pos.col.incr();
116            // no scroll needed
117            self.set_pos_unbounded(pos);
118        } else {
119            // We're going off the right hand edge
120            pos.col = Col::origin();
121            if pos.row == self.get_height() {
122                // We're at the bottom
123                self.set_pos_unbounded(pos);
124                self.scroll_screen()?;
125            } else {
126                // We're not at the bottom (yet)
127                pos.row.incr();
128                self.set_pos_unbounded(pos);
129            }
130        }
131        Ok(())
132    }
133}
134
135/// Refinement of `BaseConsole` which supports 8-bit characters. Use this is
136/// you are implementing an old-fashioned ASCII console (including extended
137/// ASCII, like Code Page 850, or ISO 8859-1).
138pub trait AsciiConsole: BaseConsole {
139    /// Write a single 8-bit char to the screen at the given position
140    /// without updating the current position.
141    fn write_char_at(&mut self, ch: u8, pos: Position) -> Result<(), Self::Error>;
142
143    /// Called when they've used an escape character in the string. Currently
144    /// you can only escape a single byte. The escape character is `0x1B`.
145    /// This function returns 'true' when the escape sequence is complete.
146    fn handle_escape(&mut self, escaped_char: u8) -> bool;
147
148    /// Write an 8-bit string to the screen at the given position. Updates the
149    /// current position to the end of the string. Strings will wrap across
150    /// the end of the screen and scroll the screen if they reach the bottom.
151    fn write_string(&mut self, s: &[u8]) -> Result<(), Self::Error> {
152        for ch in s.iter() {
153            self.write_character(*ch)?;
154        }
155        Ok(())
156    }
157
158    /// Write a single 8-bit char to the screen at the current position.
159    fn write_character(&mut self, ch: u8) -> Result<(), Self::Error> {
160        match self.get_escape_char_mode() {
161            EscapeCharMode::Seen => {
162                if self.handle_escape(ch) {
163                    self.set_escape_char_mode(EscapeCharMode::Waiting);
164                }
165            }
166            EscapeCharMode::Waiting => {
167                let mut pos = self.get_pos();
168                match self.is_special(ch) {
169                    // Go to start of next row
170                    Some(SpecialChar::Linefeed) => {
171                        pos.col = Col::origin();
172                        if pos.row == self.get_height() {
173                            self.set_pos_unbounded(pos);
174                            self.scroll_screen()?;
175                        } else {
176                            pos.row.incr();
177                            self.set_pos_unbounded(pos);
178                        }
179                    }
180                    // Go to start of this row
181                    Some(SpecialChar::CarriageReturn) => {
182                        pos.col = Col::origin();
183                        self.set_pos_unbounded(pos);
184                    }
185                    // Go to next tab stop
186                    Some(SpecialChar::Tab) => {
187                        let tabs = pos.col.0 / 9;
188                        pos.col.0 = (tabs + 1) * 9;
189                        pos.col.bound(self.get_width());
190                        self.set_pos_unbounded(pos);
191                    }
192                    // Go back one space (but don't erase anything there)
193                    Some(SpecialChar::Backspace) => {
194                        if pos.col > Col::origin() {
195                            pos.col.decr();
196                            self.set_pos_unbounded(pos);
197                        }
198                    }
199                    // Delete is ignored
200                    Some(SpecialChar::Delete) => {}
201                    // Escape the next char
202                    Some(SpecialChar::Escape) => {
203                        self.set_escape_char_mode(EscapeCharMode::Seen);
204                    }
205                    None => {
206                        self.write_char_at(ch, pos)?;
207                        self.move_cursor_right()?;
208                    }
209                }
210            }
211        }
212        Ok(())
213    }
214
215    /// Write an 8-bit string to the screen at the given position. Updates the
216    /// current position to the end of the string. Strings will wrap across
217    /// the end of the screen and scroll the screen if they reach the bottom.
218    fn write_string_at(&mut self, s: &[u8], pos: Position) -> Result<(), Self::Error> {
219        self.set_pos(pos)?;
220        self.write_string(s)?;
221        Ok(())
222    }
223
224    /// Check if an 8-bit char is special
225    fn is_special(&self, ch: u8) -> Option<SpecialChar> {
226        match self.get_control_char_mode() {
227            ControlCharMode::Interpret => match ch {
228                b'\n' => Some(SpecialChar::Linefeed),
229                b'\r' => Some(SpecialChar::CarriageReturn),
230                b'\t' => Some(SpecialChar::Tab),
231                0x1b => Some(SpecialChar::Escape),
232                0x7f => Some(SpecialChar::Delete),
233                0x08 => Some(SpecialChar::Backspace),
234                _ => None,
235            },
236            _ => None,
237        }
238    }
239}
240
241/// Refinement of `BaseConsole` which supports Unicode characters. Use this is
242/// you are implementing a modern console with Unicode support.
243pub trait UnicodeConsole: BaseConsole {
244    /// Write a single Unicode char to the screen at the given position
245    /// without updating the current position.
246    fn write_char_at(&mut self, ch: char, pos: Position) -> Result<(), Self::Error>;
247
248    /// Called when they've used an escape character in the string. Currently
249    /// you can only escape a single byte. The escape character is `0x1B`.
250    /// This function returns 'true' when the escape sequence is complete.
251    fn handle_escape(&mut self, escaped_char: char) -> bool;
252
253    /// Write a string to the screen at the given position. Updates the
254    /// current position to the end of the string. Strings will wrap across
255    /// the end of the screen and scroll the screen if they reach the bottom.
256    fn write_string(&mut self, s: &str) -> Result<(), Self::Error> {
257        for ch in s.chars() {
258            self.write_character(ch)?;
259        }
260        Ok(())
261    }
262
263    /// Write a single Unicode char to the screen at the current position.
264    fn write_character(&mut self, ch: char) -> Result<(), Self::Error> {
265        match self.get_escape_char_mode() {
266            EscapeCharMode::Seen => {
267                if self.handle_escape(ch) {
268                    self.set_escape_char_mode(EscapeCharMode::Waiting);
269                }
270            }
271            EscapeCharMode::Waiting => {
272                let mut pos = self.get_pos();
273                match self.is_special(ch) {
274                    // Go to start of next row
275                    Some(SpecialChar::Linefeed) => {
276                        pos.col = Col::origin();
277                        if pos.row == self.get_height() {
278                            self.set_pos_unbounded(pos);
279                            self.scroll_screen()?;
280                        } else {
281                            pos.row.incr();
282                            self.set_pos_unbounded(pos);
283                        }
284                    }
285                    // Go to start of this row
286                    Some(SpecialChar::CarriageReturn) => {
287                        pos.col = Col::origin();
288                        self.set_pos_unbounded(pos);
289                    }
290                    // Go to next tab stop
291                    Some(SpecialChar::Tab) => {
292                        let tabs = pos.col.0 / 9;
293                        pos.col.0 = (tabs + 1) * 9;
294                        pos.col.bound(self.get_width());
295                        self.set_pos_unbounded(pos);
296                    }
297                    // Go back one space (but don't erase anything there)
298                    Some(SpecialChar::Backspace) => {
299                        if pos.col > Col::origin() {
300                            pos.col.decr();
301                            self.set_pos_unbounded(pos);
302                        }
303                    }
304                    // Delete is ignored
305                    Some(SpecialChar::Delete) => {}
306                    // Escape the next char
307                    Some(SpecialChar::Escape) => {
308                        self.set_escape_char_mode(EscapeCharMode::Seen);
309                    }
310                    None => {
311                        self.write_char_at(ch, pos)?;
312                        self.move_cursor_right()?;
313                    }
314                }
315            }
316        }
317        Ok(())
318    }
319
320    /// Write a string to the screen at the given position. Updates the
321    /// current position to the end of the string. Strings will wrap across
322    /// the end of the screen and scroll the screen if they reach the bottom.
323    fn write_string_at(&mut self, s: &str, pos: Position) -> Result<(), Self::Error> {
324        self.set_pos(pos)?;
325        self.write_string(s)?;
326        Ok(())
327    }
328
329    /// Check if a char is special
330    fn is_special(&self, ch: char) -> Option<SpecialChar> {
331        match self.get_control_char_mode() {
332            ControlCharMode::Interpret => match ch {
333                '\n' => Some(SpecialChar::Linefeed),
334                '\r' => Some(SpecialChar::CarriageReturn),
335                '\t' => Some(SpecialChar::Tab),
336                '\u{001b}' => Some(SpecialChar::Escape),
337                '\u{007f}' => Some(SpecialChar::Delete),
338                '\u{0008}' => Some(SpecialChar::Backspace),
339                _ => None,
340            },
341            _ => None,
342        }
343    }
344}
345
346impl Position {
347    /// Create a new position
348    pub fn new(row: Row, col: Col) -> Position {
349        Position { row, col }
350    }
351
352    /// Get the origin (0, 0)
353    pub fn origin() -> Position {
354        Position {
355            row: Row::origin(),
356            col: Col::origin(),
357        }
358    }
359}
360
361impl Col {
362    /// Get the origin
363    pub fn origin() -> Col {
364        Col(0)
365    }
366
367    pub fn incr(&mut self) -> &mut Self {
368        self.0 += 1;
369        self
370    }
371
372    pub fn decr(&mut self) -> &mut Self {
373        self.0 -= 1;
374        self
375    }
376
377    pub fn bound(&mut self, other: Col) -> &mut Self {
378        if self.0 > other.0 {
379            self.0 = other.0;
380        }
381        self
382    }
383}
384
385impl Row {
386    /// Get the origin
387    pub fn origin() -> Row {
388        Row(0)
389    }
390
391    pub fn incr(&mut self) -> &mut Self {
392        self.0 += 1;
393        self
394    }
395
396    pub fn decr(&mut self) -> &mut Self {
397        self.0 -= 1;
398        self
399    }
400
401    pub fn bound(&mut self, other: Row) -> &mut Self {
402        if self.0 > other.0 {
403            self.0 = other.0;
404        }
405        self
406    }
407}
408
409impl core::convert::From<u8> for Row {
410    fn from(num: u8) -> Row {
411        Row(num)
412    }
413}
414
415impl core::convert::From<usize> for Row {
416    fn from(num: usize) -> Row {
417        Row(num as u8)
418    }
419}
420
421impl core::convert::From<u8> for Col {
422    fn from(num: u8) -> Col {
423        Col(num)
424    }
425}
426
427impl core::convert::From<usize> for Col {
428    fn from(num: usize) -> Col {
429        Col(num as u8)
430    }
431}
432
433#[cfg(test)]
434mod test {
435    use super::*;
436    use core::fmt::Write;
437
438    const WIDTH: u8 = 80;
439    const HEIGHT: u8 = 25;
440
441    #[derive(Copy, Clone)]
442    struct Line {
443        chars: [char; WIDTH as usize],
444    }
445
446    struct TestConsole {
447        pos: Position,
448        lines: [Line; HEIGHT as usize],
449        mode: ControlCharMode,
450    }
451
452    impl TestConsole {
453        fn new() -> TestConsole {
454            let line = Line {
455                chars: [' '; WIDTH as usize],
456            };
457            TestConsole {
458                lines: [line; HEIGHT as usize],
459                pos: Position::origin(),
460                mode: ControlCharMode::Interpret,
461            }
462        }
463    }
464
465    impl Console for TestConsole {
466        type Error = ();
467
468        /// Gets the last col on the screen.
469        fn get_width(&self) -> Col {
470            Col(WIDTH - 1)
471        }
472
473        /// Gets the last row on the screen.
474        fn get_height(&self) -> Row {
475            Row(HEIGHT - 1)
476        }
477
478        /// Set the horizontal position for the next text output.
479        fn set_col(&mut self, col: Col) -> Result<(), Self::Error> {
480            self.pos.col = col;
481            Ok(())
482        }
483
484        /// Set the vertical position for the next text output.
485        fn set_row(&mut self, row: Row) -> Result<(), Self::Error> {
486            self.pos.row = row;
487            Ok(())
488        }
489
490        /// Set the horizontal and vertical position for the next text output.
491        fn set_pos(&mut self, pos: Position) -> Result<(), Self::Error> {
492            self.pos = pos;
493            Ok(())
494        }
495
496        /// Get the current screen position.
497        fn get_pos(&self) -> Position {
498            self.pos
499        }
500
501        /// Set the control char mode
502        fn set_control_char_mode(&mut self, mode: ControlCharMode) {
503            self.mode = mode;
504        }
505
506        /// Get the current control char mode
507        fn get_control_char_mode(&self) -> ControlCharMode {
508            self.mode
509        }
510
511        /// Called when the screen needs to scroll up one row.
512        fn scroll_screen(&mut self) -> Result<(), Self::Error> {
513            for row in 0..HEIGHT - 1 {
514                self.lines[row as usize] = self.lines[(row as usize) + 1];
515                self.lines[(HEIGHT as usize) - 1] = Line {
516                    chars: [' '; WIDTH as usize],
517                };
518            }
519            Ok(())
520        }
521
522        /// Write a single Unicode char to the screen at the given position
523        /// without updating the current position.
524        fn write_char_at(&mut self, ch: char, pos: Position) -> Result<(), Self::Error> {
525            self.lines[pos.row.0 as usize].chars[pos.col.0 as usize] = ch;
526            Ok(())
527        }
528    }
529
530    impl core::fmt::Write for TestConsole {
531        fn write_str(&mut self, s: &str) -> core::fmt::Result {
532            self.write_string(s).unwrap();
533            Ok(())
534        }
535    }
536
537    #[test]
538    fn test_write() {
539        let mut c = TestConsole::new();
540        c.write_str("Hello").unwrap();
541        assert_eq!(
542            &c.lines[0].chars[0..10],
543            &"Hello     ".chars().collect::<Vec<char>>()[..]
544        );
545        assert_eq!(c.pos.row, Row::origin());
546        assert_eq!(c.pos.col, Col(5));
547    }
548
549    #[test]
550    fn test_lf() {
551        let mut c = TestConsole::new();
552        c.write_str("Hello\n").unwrap();
553        assert_eq!(
554            &c.lines[0].chars[0..10],
555            &"Hello     ".chars().collect::<Vec<char>>()[..]
556        );
557        assert_eq!(c.pos.row, Row(1));
558        assert_eq!(c.pos.col, Col(0));
559    }
560
561    #[test]
562    fn test_cr() {
563        let mut c = TestConsole::new();
564        c.write_str("Hello\r123").unwrap();
565        assert_eq!(
566            &c.lines[0].chars[0..10],
567            &"123lo     ".chars().collect::<Vec<char>>()[..]
568        );
569        assert_eq!(c.pos.row, Row(0));
570        assert_eq!(c.pos.col, Col(3));
571    }
572
573    #[test]
574    fn test_bs() {
575        let mut c = TestConsole::new();
576        c.write_str("Hello~\u{0008}!").unwrap();
577        assert_eq!(
578            &c.lines[0].chars[0..10],
579            &"Hello!    ".chars().collect::<Vec<char>>()[..]
580        );
581        assert_eq!(c.pos.row, Row(0));
582        assert_eq!(c.pos.col, Col(6));
583    }
584
585    #[test]
586    fn test_tab() {
587        let mut c = TestConsole::new();
588        c.write_str("1\t2\tHello\t4").unwrap();
589        assert_eq!(
590            &c.lines[0].chars[0..28],
591            &"1        2        Hello    4"
592                .chars()
593                .collect::<Vec<char>>()[..]
594        );
595        assert_eq!(c.pos.row, Row(0));
596        assert_eq!(c.pos.col, Col(28));
597    }
598
599    #[test]
600    fn test_wrap() {
601        let mut c = TestConsole::new();
602        for line in 0..HEIGHT {
603            writeln!(c, "{}", line).unwrap();
604        }
605        // First line should have a 1 in it, not a 0
606        assert_eq!(
607            &c.lines[0].chars[0..4],
608            &"1   ".chars().collect::<Vec<char>>()[..]
609        );
610        assert_eq!(c.pos.row, Row(HEIGHT - 1));
611        assert_eq!(c.pos.col, Col(0));
612    }
613
614}
615
616// End of file