Skip to main content

badterm_core/
petscii.rs

1//! Commodore 64 BBSes: PETSCII, on a 40x25 screen in the C64's colors.
2//!
3//! Printable bytes become screen codes (the order the C64's characters are
4//! stored in, which BadTerm's C64 fonts use; reverse video is the second
5//! half of the font), and control bytes move the cursor, change the color,
6//! switch between the upper case/graphics and lower/upper case sets, and so
7//! on, as the C64's screen editor does.
8
9use crate::ansi::TermEvent;
10use crate::palette::Rgb;
11use crate::screen::{Color, Screen};
12
13pub const COLS: usize = 40;
14pub const ROWS: usize = 25;
15/// The CTerm font IDs of the two character sets (as SyncTERM numbers them).
16pub const FONT_UPPER: u8 = 32;
17pub const FONT_LOWER: u8 = 33;
18
19/// The C64's 16 colors (the "Pepto" measurements).
20pub const PALETTE: [Rgb; 16] = [
21    0x000000, 0xFFFFFF, 0x68372B, 0x70A4B2, 0x6F3D86, 0x588D43, 0x352879, 0xB8C76F, 0x6F4F25, 0x433900, 0x9A6759,
22    0x444444, 0x6C6C6C, 0x9AD284, 0x6C5EB5, 0x959595,
23];
24/// Light blue text on black, as C64 terminal programs start.
25const DEFAULT_FG: usize = 14;
26
27pub fn color(i: usize) -> Color {
28    Color::Rgb(PALETTE[i & 15])
29}
30
31/// The C64 screen editor's state.
32#[derive(Debug, Default)]
33pub struct Petscii {
34    reverse: bool,
35}
36
37impl Petscii {
38    /// A fresh C64 screen: cleared, colors and character set as at power-on.
39    pub fn start(&mut self, s: &mut Screen) {
40        self.reverse = false;
41        s.pen.fg = color(DEFAULT_FG);
42        s.pen.bg = Color::Rgb(PALETTE[0]);
43        s.look.fonts[0] = FONT_UPPER;
44        s.base_font = FONT_UPPER;
45        s.autowrap = true;
46        s.erase_display(2);
47        s.move_to(0, 0);
48    }
49
50    pub fn byte(&mut self, s: &mut Screen, b: u8, events: &mut Vec<TermEvent>) {
51        let ink = |s: &mut Screen, i: usize| s.pen.fg = color(i);
52        match b {
53            0x05 => ink(s, 1),
54            0x07 => events.push(TermEvent::Bell),
55            0x0D | 0x8D => {
56                s.carriage_return();
57                s.line_feed();
58                self.reverse = false;
59            }
60            0x0E => s.look.fonts[0] = FONT_LOWER,
61            0x8E => s.look.fonts[0] = FONT_UPPER,
62            0x11 => s.move_by(0, 1),
63            0x91 => s.move_by(0, -1),
64            0x1D => right(s),
65            0x9D => left(s),
66            0x12 => self.reverse = true,
67            0x92 => self.reverse = false,
68            0x13 => s.move_to(0, 0),
69            0x93 => {
70                s.erase_display(2);
71                s.move_to(0, 0);
72            }
73            // DEL: back one, closing up the line.
74            0x14 => {
75                if s.cursor().0 > 0 {
76                    s.move_by(-1, 0);
77                    s.delete_chars(1);
78                }
79            }
80            0x94 => s.insert_chars(1),
81            0x1C => ink(s, 2),
82            0x1E => ink(s, 5),
83            0x1F => ink(s, 6),
84            0x81 => ink(s, 8),
85            0x90 => ink(s, 0),
86            0x95 => ink(s, 9),
87            0x96 => ink(s, 10),
88            0x97 => ink(s, 11),
89            0x98 => ink(s, 12),
90            0x99 => ink(s, 13),
91            0x9A => ink(s, 14),
92            0x9B => ink(s, 15),
93            0x9C => ink(s, 4),
94            0x9E => ink(s, 7),
95            0x9F => ink(s, 3),
96            _ => {
97                if let Some(code) = screen_code(b) {
98                    s.put(if self.reverse { code | 0x80 } else { code });
99                }
100            }
101        }
102    }
103}
104
105/// Cursor right, on to the next line at the end of one.
106fn right(s: &mut Screen) {
107    let (x, y) = s.cursor();
108    if x + 1 < s.cols() {
109        s.move_by(1, 0);
110    } else if y + 1 < s.rows() {
111        s.move_to(0, y + 1);
112    } else {
113        s.carriage_return();
114        s.line_feed();
115    }
116}
117
118/// Cursor left, back to the end of the line above at the start of one.
119fn left(s: &mut Screen) {
120    let (x, y) = s.cursor();
121    if x > 0 {
122        s.move_by(-1, 0);
123    } else if y > 0 {
124        s.move_to(s.cols() - 1, y - 1);
125    }
126}
127
128/// The screen code of a printable PETSCII byte; None for the controls.
129pub fn screen_code(b: u8) -> Option<u8> {
130    Some(match b {
131        0x20..=0x3F => b,
132        0x40..=0x5F => b - 0x40,
133        0x60..=0x7F => b - 0x20,
134        0xA0..=0xBF => b - 0x40,
135        0xC0..=0xFE => b - 0x80,
136        0xFF => 0x5E,
137        _ => return None,
138    })
139}
140
141/// What a typed character sends to a C64 BBS. In the C64's default case,
142/// unshifted letters show as capitals, so lower-case letters send those
143/// codes and capitals the shifted ones.
144pub fn encode_char(c: char) -> Option<u8> {
145    match c {
146        'a'..='z' => Some(c as u8 - b'a' + 0x41),
147        'A'..='Z' => Some(c as u8 - b'A' + 0xC1),
148        ' '..='@' | '[' | ']' => Some(c as u8),
149        '\u{a3}' => Some(0x5C),
150        '\u{2191}' | '^' => Some(0x5E),
151        '\u{2190}' | '_' => Some(0x5F),
152        _ => None,
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::screen::Screen;
160
161    fn run(bytes: &[u8]) -> (Screen, Petscii) {
162        let mut s = Screen::new(COLS, ROWS, 0);
163        let mut p = Petscii::default();
164        p.start(&mut s);
165        let mut ev = Vec::new();
166        for &b in bytes {
167            p.byte(&mut s, b, &mut ev);
168        }
169        (s, p)
170    }
171
172    #[test]
173    fn text_colors_and_reverse() {
174        // "HI" in white, then reversed red "A", then a newline.
175        let (s, _) = run(b"\x05HI\x1c\x12A\x92\x0dX");
176        let row = s.row(0);
177        assert_eq!((row[0].ch, row[1].ch), (0x08, 0x09));
178        assert_eq!(row[0].fg, color(1));
179        assert_eq!((row[2].ch, row[2].fg), (0x01 | 0x80, color(2)));
180        assert_eq!(s.row(1)[0].ch, 0x18);
181        assert_eq!(s.cursor(), (1, 1));
182    }
183
184    #[test]
185    fn cursor_clear_and_charsets() {
186        let (s, _) = run(b"ABC\x9d\x9d\x14\x13\x11\x1d*\x0e");
187        // On B, DEL takes the A before it and closes up the line.
188        assert_eq!((s.row(0)[0].ch, s.row(0)[1].ch), (0x02, 0x03));
189        assert_eq!(s.row(1)[1].ch, b'*');
190        assert_eq!(s.look.fonts[0], FONT_LOWER);
191        let (s, _) = run(b"ABC\x93");
192        assert!(s.row(0).iter().all(|c| c.ch == b' '));
193        assert_eq!(s.cursor(), (0, 0));
194        // Graphics: shifted bytes and their copies land on the same glyph.
195        assert_eq!(screen_code(0xC1), screen_code(0x61));
196        assert_eq!(screen_code(0xA0), Some(0x60));
197        assert_eq!((encode_char('a'), encode_char('A'), encode_char('1')), (Some(0x41), Some(0xC1), Some(b'1')));
198    }
199}