Skip to main content

badterm_core/
viewdata.rs

1//! Viewdata BBSes (Prestel, and the teletext look): a 40x24 page of text
2//! and 2x3 block "mosaic" graphics in eight colors, as a Prestel terminal
3//! showed them, following CTerm's description of its Prestel mode.
4//!
5//! Colors and the like are *serial attributes*: control codes that take a
6//! cell of their own and change the cells after them on the same row.
7//! So, as a real decoder does, the page keeps the bytes as received and
8//! each row is drawn again from left to right when anything changes;
9//! overwriting a code part way along a row recolors the rest of it.
10//!
11//! The glyphs are the font with ID [`FONT`], which the front end makes:
12//! teletext's alphanumerics at their own codes, drawn from the CP437 font,
13//! the UK set's £, ½, ¼, ¾, ‖, ─, ■ and ÷ in the control-code slots (see
14//! [`glyph`]), and the mosaics at 0x80 + the six bits of the pattern
15//! (contiguous) and 0xC0 + them (separated).
16
17use crate::ansi::TermEvent;
18use crate::palette::Rgb;
19use crate::screen::{Cell, Color, Screen, flags};
20
21pub const COLS: usize = 40;
22pub const ROWS: usize = 24;
23/// The (BadTerm-only) font ID of the teletext glyphs.
24pub const FONT: u8 = 250;
25
26/// Black, red, green, yellow, blue, magenta, cyan, white.
27pub const PALETTE: [Rgb; 8] = [0x000000, 0xFF0000, 0x00FF00, 0xFFFF00, 0x0000FF, 0xFF00FF, 0x00FFFF, 0xFFFFFF];
28
29fn color(i: u8) -> Color {
30    Color::Rgb(PALETTE[(i & 7) as usize])
31}
32
33/// The glyph (in the [`FONT`] font) for a teletext alphanumeric, 0x20-0x7F.
34/// UK Prestel's set differs from ASCII in a few places.
35pub fn glyph(c: u8) -> u8 {
36    match c {
37        b'#' => GLYPH_POUND,
38        b'[' => 0x1B, // ←, as in CP437
39        b'\\' => GLYPH_HALF,
40        b']' => 0x1A, // →
41        b'^' => 0x18, // ↑
42        b'_' => b'#',
43        b'`' => GLYPH_DASH,
44        b'{' => GLYPH_QUARTER,
45        b'|' => GLYPH_BARS,
46        b'}' => GLYPH_THREE_QUARTERS,
47        b'~' => GLYPH_DIVIDE,
48        0x7F => GLYPH_BLOCK,
49        c => c,
50    }
51}
52
53/// Where the [`FONT`] font keeps the UK set's own characters.
54pub const GLYPH_POUND: u8 = 0x01;
55pub const GLYPH_HALF: u8 = 0x02;
56pub const GLYPH_QUARTER: u8 = 0x03;
57pub const GLYPH_BARS: u8 = 0x04;
58pub const GLYPH_DASH: u8 = 0x05;
59pub const GLYPH_BLOCK: u8 = 0x06;
60pub const GLYPH_THREE_QUARTERS: u8 = 0x07;
61pub const GLYPH_DIVIDE: u8 = 0x0E;
62
63/// A byte that is a serial attribute, stored as its code minus 0x40.
64fn is_code(b: u8) -> bool {
65    b < 0x20
66}
67
68/// Which teletext terminal: a Prestel one, or a BBC Micro in Mode 7.
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub enum Kind {
71    #[default]
72    Prestel,
73    /// The BBC Micro's Mode 7 (CTerm's "BEEB"): 40x25; attributes only as
74    /// bytes 0x80-0x9F (ESC starts the BBC's own VDU codes); the screen
75    /// scrolls rather than wrapping at its top and bottom; `#`, `_` and
76    /// `` ` `` are swapped as the BBC's character set has them; and a
77    /// double-height row's lower half is the row below's own text.
78    Beeb,
79}
80
81/// BBC Mode 7 has a 25th row.
82pub const BEEB_ROWS: usize = 25;
83
84#[derive(Debug)]
85pub struct Viewdata {
86    pub kind: Kind,
87    /// What was received, by position: printable bytes, or attribute
88    /// codes (0x00-0x1F).
89    page: Vec<[u8; COLS]>,
90    x: usize,
91    y: usize,
92    /// ESC was the last byte: the next is an attribute (Prestel).
93    escape: bool,
94    /// Concealed text shown (Reveal).
95    pub reveal: bool,
96    /// The BBC's multi-byte codes: what's still to come (VDU 23 takes 9
97    /// bytes, VDU 31, cursor position, 2), and what's come.
98    vdu: (u8, usize, Vec<u8>),
99}
100
101impl Default for Viewdata {
102    fn default() -> Viewdata {
103        Viewdata::new(Kind::Prestel)
104    }
105}
106
107impl Viewdata {
108    pub fn new(kind: Kind) -> Viewdata {
109        let rows = if kind == Kind::Beeb { BEEB_ROWS } else { ROWS };
110        Viewdata {
111            kind,
112            page: vec![[b' '; COLS]; rows],
113            x: 0,
114            y: 0,
115            escape: false,
116            reveal: false,
117            vdu: (0, 0, Vec::new()),
118        }
119    }
120
121    fn rows(&self) -> usize {
122        self.page.len()
123    }
124
125    pub fn start(&mut self, s: &mut Screen) {
126        *self = Viewdata::new(self.kind);
127        s.look.fonts[0] = FONT;
128        s.base_font = FONT;
129        s.autowrap = false;
130        s.ice_colors = false;
131        self.draw(s);
132    }
133
134    /// Show concealed text, or hide it again.
135    pub fn toggle_reveal(&mut self, s: &mut Screen) -> bool {
136        self.reveal = !self.reveal;
137        self.draw(s);
138        self.reveal
139    }
140
141    /// Bytes from the BBS: the page changes, then the screen is drawn from it.
142    pub fn feed(&mut self, s: &mut Screen, data: &[u8], events: &mut Vec<TermEvent>) {
143        for &b in data {
144            match self.kind {
145                Kind::Prestel => self.byte(s, b, events),
146                Kind::Beeb => self.beeb_byte(s, b, events),
147            }
148        }
149        self.draw(s);
150    }
151
152    fn byte(&mut self, s: &mut Screen, b: u8, events: &mut Vec<TermEvent>) {
153        let b = if b >= 0xA0 { b & 0x7F } else { b };
154        if self.escape {
155            self.escape = false;
156            // ESC and a code: a serial attribute (other ESC sequences,
157            // Prestel's memory programming, are ignored).
158            if (0x40..=0x5F).contains(&b) {
159                self.put(b - 0x40);
160            }
161            return;
162        }
163        let rows = self.rows();
164        match b {
165            0x07 => events.push(TermEvent::Bell),
166            0x08 => self.back(),
167            0x09 => self.forward(),
168            0x0A => self.y = (self.y + 1) % rows,
169            0x0B => self.y = (self.y + rows - 1) % rows,
170            0x0C => self.clear(),
171            0x0D => self.x = 0,
172            0x11 => s.cursor_visible = true,
173            0x14 => s.cursor_visible = false,
174            0x1B => self.escape = true,
175            0x1E => (self.x, self.y) = (0, 0),
176            0x20..=0x7F => self.put(b),
177            // Attributes sent as C1 bytes.
178            0x80..=0x9F => self.put(b - 0x80),
179            _ => {}
180        }
181    }
182
183    /// A byte to a BBC Micro in Mode 7.
184    fn beeb_byte(&mut self, s: &mut Screen, b: u8, events: &mut Vec<TermEvent>) {
185        // The rest of a VDU 23 (cursor on or off) or VDU 31 (position).
186        if self.vdu.1 > 0 {
187            self.vdu.2.push(b);
188            self.vdu.1 -= 1;
189            if self.vdu.1 == 0 {
190                let args = std::mem::take(&mut self.vdu.2);
191                match self.vdu.0 {
192                    23 if args.first() == Some(&1) => s.cursor_visible = args.get(1).copied().unwrap_or(1) != 0,
193                    28 => {
194                        let (x, y) = (args[0].saturating_sub(0x20) as usize, args[1].saturating_sub(0x20) as usize);
195                        if x < COLS && y < self.rows() {
196                            (self.x, self.y) = (x, y);
197                        }
198                    }
199                    _ => {}
200                }
201            }
202            return;
203        }
204        let b = if b >= 0xA0 { b & 0x7F } else { b };
205        match b {
206            0x07 => events.push(TermEvent::Bell),
207            0x08 => self.beeb_back(),
208            0x09 => self.beeb_forward(),
209            0x0A => self.beeb_down(),
210            0x0B => {
211                if self.y == 0 {
212                    self.page.rotate_right(1);
213                    self.page[0] = [b' '; COLS];
214                } else {
215                    self.y -= 1;
216                }
217            }
218            0x0C => self.clear(),
219            0x0D => self.x = 0,
220            0x17 => self.vdu = (23, 9, Vec::new()),
221            0x1C => self.vdu = (28, 2, Vec::new()),
222            0x1E => (self.x, self.y) = (0, 0),
223            // A destructive backspace.
224            0x7F => {
225                self.beeb_back();
226                self.page[self.y][self.x] = b' ';
227            }
228            // The BBC's #, _ and ` are where the teletext chip has #, the
229            // long dash and £.
230            0x23 => self.beeb_put(0x5F),
231            0x5F => self.beeb_put(0x60),
232            0x60 => self.beeb_put(0x23),
233            0x20..=0x7E => self.beeb_put(b),
234            0x80..=0x9F => self.beeb_put(b - 0x80),
235            _ => {}
236        }
237    }
238
239    fn beeb_put(&mut self, b: u8) {
240        self.page[self.y][self.x] = b;
241        self.beeb_forward();
242    }
243
244    /// Down a row, scrolling the page up at the bottom.
245    fn beeb_down(&mut self) {
246        if self.y + 1 == self.rows() {
247            self.page.rotate_left(1);
248            let last = self.rows() - 1;
249            self.page[last] = [b' '; COLS];
250        } else {
251            self.y += 1;
252        }
253    }
254
255    fn beeb_forward(&mut self) {
256        self.x += 1;
257        if self.x == COLS {
258            self.x = 0;
259            self.beeb_down();
260        }
261    }
262
263    fn beeb_back(&mut self) {
264        if self.x > 0 {
265            self.x -= 1;
266            return;
267        }
268        self.x = COLS - 1;
269        if self.y == 0 {
270            self.page.rotate_right(1);
271            self.page[0] = [b' '; COLS];
272        } else {
273            self.y -= 1;
274        }
275    }
276
277    fn clear(&mut self) {
278        self.page.fill([b' '; COLS]);
279        (self.x, self.y) = (0, 0);
280        self.reveal = false;
281    }
282
283    fn put(&mut self, b: u8) {
284        self.page[self.y][self.x] = b;
285        self.forward();
286    }
287
288    /// The next position: rows wrap, and the bottom row wraps to the top.
289    fn forward(&mut self) {
290        self.x += 1;
291        if self.x == COLS {
292            self.x = 0;
293            self.y = (self.y + 1) % self.rows();
294        }
295    }
296
297    fn back(&mut self) {
298        if self.x == 0 {
299            self.x = COLS - 1;
300            self.y = (self.y + self.rows() - 1) % self.rows();
301        } else {
302            self.x -= 1;
303        }
304    }
305
306    /// Draw the page on the screen, row by row.
307    pub fn draw(&self, s: &mut Screen) {
308        let rows = self.rows();
309        let mut above: Option<[Cell; COLS]> = None;
310        for y in 0..rows.min(s.rows()) {
311            let (row, top) = draw_row(&self.page[y], self.reveal);
312            let cells = match above.take() {
313                // The row under a double-height row shows bottom halves:
314                // of the row above's cells on Prestel, of its own on a BBC.
315                Some(top_cells) if self.kind == Kind::Prestel => top_cells.map(|c| {
316                    if c.flags & flags::DOUBLE_TOP != 0 {
317                        Cell { flags: (c.flags & !flags::DOUBLE_TOP) | flags::DOUBLE_BOTTOM, ..c }
318                    } else {
319                        Cell { ch: b' ', flags: c.flags & !flags::BLINK, ..c }
320                    }
321                }),
322                Some(_) => row.map(|c| {
323                    if c.flags & flags::DOUBLE_TOP != 0 {
324                        Cell { flags: (c.flags & !flags::DOUBLE_TOP) | flags::DOUBLE_BOTTOM, ..c }
325                    } else {
326                        c
327                    }
328                }),
329                None => {
330                    if top && y + 1 < rows {
331                        above = Some(row);
332                    }
333                    if top && y + 1 == rows {
334                        row.map(|c| Cell { flags: c.flags & !flags::DOUBLE_TOP, ..c })
335                    } else {
336                        row
337                    }
338                }
339            };
340            for (x, c) in cells.iter().enumerate() {
341                if let Some(cell) = s.cell_mut(x, y) {
342                    *cell = *c;
343                }
344            }
345        }
346        s.move_to(self.x, self.y);
347    }
348}
349
350/// One row of the page as cells, applying its serial attributes from the
351/// left; and whether it has double height in it.
352fn draw_row(page: &[u8; COLS], reveal: bool) -> ([Cell; COLS], bool) {
353    let (mut fg, mut bg) = (7u8, 0u8);
354    let (mut mosaic, mut separated, mut hold, mut flash, mut conceal, mut double) =
355        (false, false, false, false, false, false);
356    let mut held = b' ';
357    let mut any_double = false;
358    let mut cells = [Cell::default(); COLS];
359    for (x, &b) in page.iter().enumerate() {
360        let ch = if is_code(b) {
361            // Before: these take effect in the code's own cell.
362            match b {
363                0x09 => flash = false,
364                0x0C => {
365                    double = false;
366                    hold = false;
367                    held = b' ';
368                }
369                0x18 => conceal = true,
370                0x19 => separated = false,
371                0x1A => separated = true,
372                0x1C => bg = 0,
373                0x1D => bg = fg,
374                0x1E => hold = true,
375                _ => {}
376            }
377            if hold && mosaic { held } else { b' ' }
378        } else if mosaic && (b & 0x20 != 0) {
379            // A 2x3 block: bits 0-4 and 6 of the byte are its six cells.
380            let pattern = (b & 0x1F) | ((b & 0x40) >> 1);
381            let g = if separated { 0xC0 } else { 0x80 } | pattern;
382            held = g;
383            g
384        } else {
385            glyph(b)
386        };
387        let mut f = 0;
388        if flash {
389            f |= flags::BLINK;
390        }
391        if conceal && !reveal {
392            f |= flags::CONCEAL;
393        }
394        if double {
395            f |= flags::DOUBLE_TOP;
396        }
397        cells[x] = Cell { ch, fg: color(fg), bg: color(bg), flags: f, tile: 0 };
398        if is_code(b) {
399            // After: from the next cell on.
400            match b {
401                0x01..=0x07 => {
402                    fg = b;
403                    mosaic = false;
404                    conceal = false;
405                    hold = false;
406                    held = b' ';
407                }
408                0x11..=0x17 => {
409                    fg = b - 0x10;
410                    mosaic = true;
411                    conceal = false;
412                }
413                0x08 => flash = true,
414                0x0D => {
415                    double = true;
416                    any_double = true;
417                    hold = false;
418                    held = b' ';
419                }
420                0x1F => {
421                    hold = false;
422                    held = b' ';
423                }
424                _ => {}
425            }
426        }
427    }
428    (cells, any_double)
429}
430
431/// A typed character as a BBC Micro sends it: its `£` is 0x60.
432pub fn beeb_encode_char(c: char) -> Option<u8> {
433    match c {
434        '£' => Some(0x60),
435        '\r' => Some(0x0D),
436        c if c.is_ascii() && !c.is_ascii_control() => Some(c as u8),
437        _ => None,
438    }
439}
440
441/// A typed character as a Prestel keyboard sends it: its `#` key is
442/// 0x5F, and `£` is 0x23. Enter is the `#` key, which ends input.
443pub fn encode_char(c: char) -> Option<u8> {
444    match c {
445        '\r' | '#' => Some(0x5F),
446        '£' => Some(0x23),
447        c if c.is_ascii() && !c.is_ascii_control() => Some(c as u8),
448        _ => None,
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    fn screen(data: &[u8]) -> Screen {
457        let mut s = Screen::new(COLS, ROWS, 0);
458        let mut v = Viewdata::default();
459        v.start(&mut s);
460        v.feed(&mut s, data, &mut Vec::new());
461        s
462    }
463
464    #[test]
465    fn serial_attributes_take_a_cell_and_color_the_rest() {
466        // Red text, then blue on a red background ("new background").
467        let s = screen(b"\x1bAHi\x1bD\x1b]X");
468        let row = s.row(0);
469        assert_eq!(row[0].ch, b' ', "the code's own cell is a space");
470        assert_eq!((row[1].ch, row[1].fg), (b'H', color(1)));
471        assert_eq!(row[3].ch, b' ');
472        // New background takes the current color (blue) in its own cell.
473        assert_eq!(row[4].bg, color(4));
474        assert_eq!((row[5].ch, row[5].fg, row[5].bg), (b'X', color(4), color(4)));
475        // The next row starts over: white on black.
476        assert_eq!(s.row(1)[0].fg, color(7));
477    }
478
479    #[test]
480    fn mosaics_hold_and_separation() {
481        // Mosaic green, a full block (0x7F) and the top-left cell (0x21);
482        // then separated, and held over a color change.
483        let s = screen(b"\x1bR\x7f!\x1bZ\x7f\x1b^\x1bS");
484        let row = s.row(0);
485        assert_eq!(row[1].ch, 0x80 | 0x3F);
486        assert_eq!(row[2].ch, 0x80 | 0x01);
487        assert_eq!(row[4].ch, 0xC0 | 0x3F, "separated");
488        // Hold mosaics: the yellow code's cell repeats the last block.
489        assert_eq!(row[6].ch, 0xC0 | 0x3F);
490        // Capital letters stay letters in mosaic mode.
491        let s = screen(b"\x1bQA");
492        assert_eq!(s.row(0)[1].ch, b'A');
493    }
494
495    #[test]
496    fn overwriting_a_code_recolors_the_row() {
497        let mut s = Screen::new(COLS, ROWS, 0);
498        let mut v = Viewdata::default();
499        v.start(&mut s);
500        v.feed(&mut s, b"\x1bAred text", &mut Vec::new());
501        assert_eq!(s.row(0)[5].fg, color(1));
502        // Back to the start of the row, and make that code green.
503        v.feed(&mut s, b"\r\x1bB", &mut Vec::new());
504        assert_eq!(s.row(0)[5].fg, color(2));
505    }
506
507    #[test]
508    fn reveal_shows_concealed_text() {
509        let mut s = Screen::new(COLS, ROWS, 0);
510        let mut v = Viewdata::default();
511        v.start(&mut s);
512        v.feed(&mut s, b"\x1bXsecret", &mut Vec::new());
513        assert!(s.row(0)[1].flags & flags::CONCEAL != 0);
514        assert!(v.toggle_reveal(&mut s));
515        assert!(s.row(0)[1].flags & flags::CONCEAL == 0);
516        // A new page hides it again.
517        v.feed(&mut s, b"\x0c\x1bXagain", &mut Vec::new());
518        assert!(s.row(0)[1].flags & flags::CONCEAL != 0);
519    }
520
521    #[test]
522    fn bbc_mode_7() {
523        let mut s = Screen::new(COLS, BEEB_ROWS, 0);
524        let mut v = Viewdata::new(Kind::Beeb);
525        v.start(&mut s);
526        // Attributes as C1 bytes (red text); ESC isn't one; # is #.
527        v.feed(&mut s, b"\x81Hi #", &mut Vec::new());
528        assert_eq!((s.row(0)[1].ch, s.row(0)[1].fg), (b'H', color(1)));
529        assert_eq!(s.row(0)[4].ch, b'#');
530        // VDU 31 (here 28, with 0x20 added) moves; the bottom row scrolls.
531        v.feed(&mut s, b"\x1c\x20\x38last\n", &mut Vec::new());
532        assert_eq!(Screen::row_text(s.row(23)), "last");
533        assert_eq!(Screen::row_text(s.row(24)), "");
534        assert_eq!(s.cursor(), (4, 24));
535        // Double height: the row below shows the lower halves of its own text.
536        v.feed(&mut s, b"\x0c\x8dBIG\r\n\x8dBIG", &mut Vec::new());
537        assert!(s.row(1)[1].flags & flags::DOUBLE_BOTTOM != 0);
538        assert_eq!(s.row(1)[1].ch, b'B');
539        // VDU 23,1,0;0;0;0; hides the cursor.
540        v.feed(&mut s, &[23, 1, 0, 0, 0, 0, 0, 0, 0, 0], &mut Vec::new());
541        assert!(!s.cursor_visible);
542    }
543
544    #[test]
545    fn double_height_and_the_page_wrapping() {
546        let s = screen(b"\x1bMBig\r\nsmall");
547        assert!(s.row(0)[1].flags & flags::DOUBLE_TOP != 0);
548        // The row under it shows the bottom halves, not its own text.
549        assert_eq!(s.row(1)[1].ch, b'B');
550        assert!(s.row(1)[1].flags & flags::DOUBLE_BOTTOM != 0);
551        // Up from the top row is the bottom row; the UK set's £.
552        let s = screen(b"\x0b#");
553        assert_eq!(s.row(ROWS - 1)[0].ch, GLYPH_POUND);
554        assert_eq!(s.cursor(), (1, ROWS - 1));
555        // Writing the last cell goes on to the top.
556        let mut data = vec![0x1E, 0x0B];
557        data.extend([b'x'; COLS]);
558        assert_eq!(screen(&data).cursor(), (0, 0));
559    }
560}