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#[derive(Debug)]
69pub struct Viewdata {
70    /// What was received, by position: printable bytes, or attribute
71    /// codes (0x00-0x1F).
72    page: [[u8; COLS]; ROWS],
73    x: usize,
74    y: usize,
75    /// ESC was the last byte: the next is an attribute.
76    escape: bool,
77}
78
79impl Default for Viewdata {
80    fn default() -> Viewdata {
81        Viewdata { page: [[b' '; COLS]; ROWS], x: 0, y: 0, escape: false }
82    }
83}
84
85impl Viewdata {
86    pub fn start(&mut self, s: &mut Screen) {
87        *self = Viewdata::default();
88        s.look.fonts[0] = FONT;
89        s.base_font = FONT;
90        s.autowrap = false;
91        s.ice_colors = false;
92        self.draw(s);
93    }
94
95    /// Bytes from the BBS: the page changes, then the screen is drawn from it.
96    pub fn feed(&mut self, s: &mut Screen, data: &[u8], events: &mut Vec<TermEvent>) {
97        for &b in data {
98            self.byte(s, b, events);
99        }
100        self.draw(s);
101    }
102
103    fn byte(&mut self, s: &mut Screen, b: u8, events: &mut Vec<TermEvent>) {
104        let b = if b >= 0xA0 { b & 0x7F } else { b };
105        if self.escape {
106            self.escape = false;
107            // ESC and a code: a serial attribute (other ESC sequences,
108            // Prestel's memory programming, are ignored).
109            if (0x40..=0x5F).contains(&b) {
110                self.put(b - 0x40);
111            }
112            return;
113        }
114        match b {
115            0x07 => events.push(TermEvent::Bell),
116            0x08 => self.back(),
117            0x09 => self.forward(),
118            0x0A => self.y = (self.y + 1) % ROWS,
119            0x0B => self.y = (self.y + ROWS - 1) % ROWS,
120            0x0C => {
121                self.page = [[b' '; COLS]; ROWS];
122                (self.x, self.y) = (0, 0);
123            }
124            0x0D => self.x = 0,
125            0x11 => s.cursor_visible = true,
126            0x14 => s.cursor_visible = false,
127            0x1B => self.escape = true,
128            0x1E => (self.x, self.y) = (0, 0),
129            0x20..=0x7F => self.put(b),
130            // Attributes sent as C1 bytes.
131            0x80..=0x9F => self.put(b - 0x80),
132            _ => {}
133        }
134    }
135
136    fn put(&mut self, b: u8) {
137        self.page[self.y][self.x] = b;
138        self.forward();
139    }
140
141    /// The next position: rows wrap, and the bottom row wraps to the top.
142    fn forward(&mut self) {
143        self.x += 1;
144        if self.x == COLS {
145            self.x = 0;
146            self.y = (self.y + 1) % ROWS;
147        }
148    }
149
150    fn back(&mut self) {
151        if self.x == 0 {
152            self.x = COLS - 1;
153            self.y = (self.y + ROWS - 1) % ROWS;
154        } else {
155            self.x -= 1;
156        }
157    }
158
159    /// Draw the page on the screen, row by row.
160    pub fn draw(&self, s: &mut Screen) {
161        let mut above: Option<[Cell; COLS]> = None;
162        for y in 0..ROWS.min(s.rows()) {
163            let (row, top) = draw_row(&self.page[y]);
164            let cells = match above.take() {
165                // The row under a double-height row shows its bottom half.
166                Some(top_cells) => top_cells.map(|c| {
167                    if c.flags & flags::DOUBLE_TOP != 0 {
168                        Cell { flags: (c.flags & !flags::DOUBLE_TOP) | flags::DOUBLE_BOTTOM, ..c }
169                    } else {
170                        Cell { ch: b' ', flags: c.flags & !flags::BLINK, ..c }
171                    }
172                }),
173                None => {
174                    if top && y + 1 < ROWS {
175                        above = Some(row);
176                    }
177                    if top && y + 1 == ROWS {
178                        row.map(|c| Cell { flags: c.flags & !flags::DOUBLE_TOP, ..c })
179                    } else {
180                        row
181                    }
182                }
183            };
184            for (x, c) in cells.iter().enumerate() {
185                if let Some(cell) = s.cell_mut(x, y) {
186                    *cell = *c;
187                }
188            }
189        }
190        s.move_to(self.x, self.y);
191    }
192}
193
194/// One row of the page as cells, applying its serial attributes from the
195/// left; and whether it has double height in it.
196fn draw_row(page: &[u8; COLS]) -> ([Cell; COLS], bool) {
197    let (mut fg, mut bg) = (7u8, 0u8);
198    let (mut mosaic, mut separated, mut hold, mut flash, mut conceal, mut double) =
199        (false, false, false, false, false, false);
200    let mut held = b' ';
201    let mut any_double = false;
202    let mut cells = [Cell::default(); COLS];
203    for (x, &b) in page.iter().enumerate() {
204        let ch = if is_code(b) {
205            // Before: these take effect in the code's own cell.
206            match b {
207                0x09 => flash = false,
208                0x0C => {
209                    double = false;
210                    hold = false;
211                    held = b' ';
212                }
213                0x18 => conceal = true,
214                0x19 => separated = false,
215                0x1A => separated = true,
216                0x1C => bg = 0,
217                0x1D => bg = fg,
218                0x1E => hold = true,
219                _ => {}
220            }
221            if hold && mosaic { held } else { b' ' }
222        } else if mosaic && (b & 0x20 != 0) {
223            // A 2x3 block: bits 0-4 and 6 of the byte are its six cells.
224            let pattern = (b & 0x1F) | ((b & 0x40) >> 1);
225            let g = if separated { 0xC0 } else { 0x80 } | pattern;
226            held = g;
227            g
228        } else {
229            glyph(b)
230        };
231        let mut f = 0;
232        if flash {
233            f |= flags::BLINK;
234        }
235        if conceal {
236            f |= flags::CONCEAL;
237        }
238        if double {
239            f |= flags::DOUBLE_TOP;
240        }
241        cells[x] = Cell { ch, fg: color(fg), bg: color(bg), flags: f, tile: 0 };
242        if is_code(b) {
243            // After: from the next cell on.
244            match b {
245                0x01..=0x07 => {
246                    fg = b;
247                    mosaic = false;
248                    conceal = false;
249                    hold = false;
250                    held = b' ';
251                }
252                0x11..=0x17 => {
253                    fg = b - 0x10;
254                    mosaic = true;
255                    conceal = false;
256                }
257                0x08 => flash = true,
258                0x0D => {
259                    double = true;
260                    any_double = true;
261                    hold = false;
262                    held = b' ';
263                }
264                0x1F => {
265                    hold = false;
266                    held = b' ';
267                }
268                _ => {}
269            }
270        }
271    }
272    (cells, any_double)
273}
274
275/// A typed character as a Prestel keyboard sends it: its `#` key is
276/// 0x5F, and `£` is 0x23. Enter is the `#` key, which ends input.
277pub fn encode_char(c: char) -> Option<u8> {
278    match c {
279        '\r' | '#' => Some(0x5F),
280        '£' => Some(0x23),
281        c if c.is_ascii() && !c.is_ascii_control() => Some(c as u8),
282        _ => None,
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    fn screen(data: &[u8]) -> Screen {
291        let mut s = Screen::new(COLS, ROWS, 0);
292        let mut v = Viewdata::default();
293        v.start(&mut s);
294        v.feed(&mut s, data, &mut Vec::new());
295        s
296    }
297
298    #[test]
299    fn serial_attributes_take_a_cell_and_color_the_rest() {
300        // Red text, then blue on a red background ("new background").
301        let s = screen(b"\x1bAHi\x1bD\x1b]X");
302        let row = s.row(0);
303        assert_eq!(row[0].ch, b' ', "the code's own cell is a space");
304        assert_eq!((row[1].ch, row[1].fg), (b'H', color(1)));
305        assert_eq!(row[3].ch, b' ');
306        // New background takes the current color (blue) in its own cell.
307        assert_eq!(row[4].bg, color(4));
308        assert_eq!((row[5].ch, row[5].fg, row[5].bg), (b'X', color(4), color(4)));
309        // The next row starts over: white on black.
310        assert_eq!(s.row(1)[0].fg, color(7));
311    }
312
313    #[test]
314    fn mosaics_hold_and_separation() {
315        // Mosaic green, a full block (0x7F) and the top-left cell (0x21);
316        // then separated, and held over a color change.
317        let s = screen(b"\x1bR\x7f!\x1bZ\x7f\x1b^\x1bS");
318        let row = s.row(0);
319        assert_eq!(row[1].ch, 0x80 | 0x3F);
320        assert_eq!(row[2].ch, 0x80 | 0x01);
321        assert_eq!(row[4].ch, 0xC0 | 0x3F, "separated");
322        // Hold mosaics: the yellow code's cell repeats the last block.
323        assert_eq!(row[6].ch, 0xC0 | 0x3F);
324        // Capital letters stay letters in mosaic mode.
325        let s = screen(b"\x1bQA");
326        assert_eq!(s.row(0)[1].ch, b'A');
327    }
328
329    #[test]
330    fn overwriting_a_code_recolors_the_row() {
331        let mut s = Screen::new(COLS, ROWS, 0);
332        let mut v = Viewdata::default();
333        v.start(&mut s);
334        v.feed(&mut s, b"\x1bAred text", &mut Vec::new());
335        assert_eq!(s.row(0)[5].fg, color(1));
336        // Back to the start of the row, and make that code green.
337        v.feed(&mut s, b"\r\x1bB", &mut Vec::new());
338        assert_eq!(s.row(0)[5].fg, color(2));
339    }
340
341    #[test]
342    fn double_height_and_the_page_wrapping() {
343        let s = screen(b"\x1bMBig\r\nsmall");
344        assert!(s.row(0)[1].flags & flags::DOUBLE_TOP != 0);
345        // The row under it shows the bottom halves, not its own text.
346        assert_eq!(s.row(1)[1].ch, b'B');
347        assert!(s.row(1)[1].flags & flags::DOUBLE_BOTTOM != 0);
348        // Up from the top row is the bottom row; the UK set's £.
349        let s = screen(b"\x0b#");
350        assert_eq!(s.row(ROWS - 1)[0].ch, GLYPH_POUND);
351        assert_eq!(s.cursor(), (1, ROWS - 1));
352        // Writing the last cell goes on to the top.
353        let mut data = vec![0x1E, 0x0B];
354        data.extend([b'x'; COLS]);
355        assert_eq!(screen(&data).cursor(), (0, 0));
356    }
357}