Skip to main content

saturnus_objects/
cmdline.rs

1//! The command line being edited and the state of the ROM's editor (entry
2//! mode, alpha, shifts), read from RAM without pressing a key.
3//!
4//! The ROM keeps the text reversed and NUL-terminated just below its
5//! temporary environments, with no length field; the cursor and the
6//! editor's flags sit in system RAM. Locations per model were found by
7//! observation (wiki: hardware/command-line).
8
9use crate::error::Error;
10use saturnus_core::{Machine, Model};
11use serde::Serialize;
12
13use crate::charset;
14use crate::object::Memory;
15
16type Result<T, E = Error> = std::result::Result<T, E>;
17
18/// Most characters read (the 49G's RAM could hold more, but no line the
19/// ROM edits is that long; more means the pointers are not the ROM's).
20pub const MAX_CHARS: usize = 65_536;
21
22/// Where a model's ROM keeps its editor in RAM (wiki:
23/// hardware/command-line). `*_ptr` fields hold 5-nibble pointers; the
24/// others are nibbles whose bits are listed with them.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct EditorLayout {
27    /// Pointer just past the text's first character (the temporary
28    /// environments, `local_vars` in the 1991 address list).
29    pub text_end_ptr: u32,
30    /// Pointer past the data stack, the lowest the text may reach.
31    pub stack_end_ptr: u32,
32    /// The cursor, 5 nibbles, in characters from the start.
33    pub cursor: u32,
34    /// Bit 1 insert mode, bit 2 algebraic entry.
35    pub entry: u32,
36    /// Bit 1 a command line is open, bit 2 lowercase lock.
37    pub line: u32,
38    /// Bit 0 left shift, bit 1 right shift, bit 2 alpha.
39    pub keys: u32,
40    /// Bit 0 alpha lock.
41    pub alpha_lock: u32,
42    /// Bit 0 program entry.
43    pub program: u32,
44    /// The message nibble: on the 48 bit 0 is set while the header shows a
45    /// message (an error); on the 49G it, or the nibble after it, reads
46    /// #F while an error box has the keyboard.
47    pub message: u32,
48    /// Errors show in a box (49G), not in the header (48).
49    pub message_box: bool,
50    /// The byte the 49G's CHARS application opens on (it remembers the
51    /// last one); `None` where CHARS always opens on 128 (48GX) or does
52    /// not exist (48SX).
53    pub chars_position: Option<u32>,
54}
55
56impl EditorLayout {
57    /// 48SX (ROM J).
58    pub const HP48SX: EditorLayout = EditorLayout {
59        text_end_ptr: 0x70583,
60        stack_end_ptr: 0x7057E,
61        cursor: 0x70704,
62        entry: 0x70685,
63        line: 0x70687,
64        keys: 0x706C4,
65        alpha_lock: 0x70793,
66        program: 0x70794,
67        message: 0x7068B,
68        message_box: false,
69        chars_position: None,
70    };
71    /// 48GX (ROM R).
72    pub const HP48GX: EditorLayout = EditorLayout {
73        text_end_ptr: 0x80702,
74        stack_end_ptr: 0x806FD,
75        cursor: 0x80882,
76        entry: 0x80803,
77        line: 0x80805,
78        keys: 0x80842,
79        alpha_lock: 0x80911,
80        program: 0x80912,
81        message: 0x80809,
82        message_box: false,
83        chars_position: None,
84    };
85    /// 49G (ROM 2.10).
86    pub const HP49G: EditorLayout = EditorLayout {
87        text_end_ptr: 0x80702,
88        stack_end_ptr: 0x806FD,
89        cursor: 0x80F61,
90        entry: 0x80EC2,
91        line: 0x80EC4,
92        keys: 0x80F01,
93        alpha_lock: 0x80FF0,
94        program: 0x80FF1,
95        message: 0x80EC8,
96        message_box: true,
97        chars_position: Some(0x818CF),
98    };
99
100    /// The layout of `model`, or `None` for the aplet models, the Pioneers (42S, 27S,
101    /// 17BII, 17B), the 28S and the 19BII.
102    pub fn of(model: Model) -> Option<EditorLayout> {
103        match model {
104            Model::Hp48sx => Some(Self::HP48SX),
105            Model::Hp48gx => Some(Self::HP48GX),
106            Model::Hp49g => Some(Self::HP49G),
107            Model::Hp38g
108            | Model::Hp39g
109            | Model::Hp40g
110            | Model::Hp42s
111            | Model::Hp27s
112            | Model::Hp17bii
113            | Model::Hp17b
114            | Model::Hp28s
115            | Model::Hp19bii => None,
116        }
117    }
118
119    /// The layout of `model` or [`Error::NoCommandLine`], which names what
120    /// the model lacks.
121    pub fn require(model: Model) -> Result<EditorLayout> {
122        Self::of(model).ok_or(Error::NoCommandLine(model))
123    }
124}
125
126/// The editor as the ROM left it.
127#[derive(Clone, Debug, Default, PartialEq, Eq)]
128pub struct Editor {
129    /// A command line is open (also an empty one, and in `EDIT`).
130    pub active: bool,
131    /// The text as HP bytes, empty when no line is open.
132    pub text: Vec<u8>,
133    /// The cursor in characters, 0 before the first.
134    pub cursor: usize,
135    /// Insert mode (off: typing replaces the character at the cursor).
136    pub insert: bool,
137    /// Algebraic entry mode (after `'`).
138    pub algebraic: bool,
139    /// Program entry mode (after `«`, or ENTRY).
140    pub program: bool,
141    /// Lowercase lock.
142    pub lowercase: bool,
143    /// Alpha is on (for one key, or locked).
144    pub alpha: bool,
145    /// Alpha is locked.
146    pub alpha_lock: bool,
147    /// The left shift is pending.
148    pub left_shift: bool,
149    /// The right shift is pending.
150    pub right_shift: bool,
151    /// The ROM shows an error: in the header (48) or in a box (49G).
152    pub message: bool,
153}
154
155/// The `commandLine` protocol answer.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
157pub struct CommandLine {
158    /// A command line is open.
159    pub active: bool,
160    /// Its text (empty when none is open).
161    pub text: String,
162    /// The cursor in calculator characters (0 before the first).
163    pub cursor: usize,
164}
165
166fn nibble(mem: &dyn Memory, addr: u32) -> Result<u8> {
167    mem.nibble(addr).ok_or(Error::Unreadable(addr))
168}
169
170fn field(mem: &dyn Memory, addr: u32, len: u32) -> Result<u32> {
171    let mut v = 0;
172    for i in (0..len).rev() {
173        v = (v << 4) | u32::from(nibble(mem, addr + i)?);
174    }
175    Ok(v)
176}
177
178fn bit(mem: &dyn Memory, addr: u32, b: u8) -> Result<bool> {
179    Ok(nibble(mem, addr)? & (1 << b) != 0)
180}
181
182/// Read the editor through `layout`.
183pub fn read(mem: &dyn Memory, layout: &EditorLayout) -> Result<Editor> {
184    let active = bit(mem, layout.line, 1)?;
185    let mut text = Vec::new();
186    if active {
187        let end = field(mem, layout.text_end_ptr, 5)?;
188        let floor = field(mem, layout.stack_end_ptr, 5)?;
189        if floor >= end {
190            return Err(Error::Memory(format!(
191                "the command line pointers (#{floor:05X}, #{end:05X}) are not the ROM's"
192            )));
193        }
194        let mut a = end;
195        while a >= floor + 2 {
196            a -= 2;
197            let b = field(mem, a, 2)? as u8;
198            if b == 0 {
199                break;
200            }
201            if text.len() >= MAX_CHARS {
202                return Err(Error::Memory(format!(
203                    "the command line is longer than {MAX_CHARS} characters"
204                )));
205            }
206            text.push(b);
207        }
208    }
209    let cursor = field(mem, layout.cursor, 5)? as usize;
210    let keys = nibble(mem, layout.keys)?;
211    let message = if layout.message_box {
212        !active && (nibble(mem, layout.message)? == 0xF || nibble(mem, layout.message + 1)? == 0xF)
213    } else {
214        bit(mem, layout.message, 0)?
215    };
216    Ok(Editor {
217        message,
218        active,
219        cursor: if active { cursor.min(text.len()) } else { 0 },
220        text,
221        insert: bit(mem, layout.entry, 1)?,
222        algebraic: bit(mem, layout.entry, 2)?,
223        program: bit(mem, layout.program, 0)?,
224        lowercase: bit(mem, layout.line, 2)?,
225        alpha: keys & 4 != 0,
226        alpha_lock: bit(mem, layout.alpha_lock, 0)?,
227        left_shift: keys & 1 != 0,
228        right_shift: keys & 2 != 0,
229    })
230}
231
232/// The editor of `machine` (48SX, 48GX, 49G).
233pub fn editor(machine: &Machine) -> Result<Editor> {
234    read(machine, &EditorLayout::require(machine.model())?)
235}
236
237/// The command line of `machine`: open or not, its text and cursor.
238pub fn command_line(machine: &Machine) -> Result<CommandLine> {
239    let e = editor(machine)?;
240    Ok(CommandLine {
241        active: e.active,
242        text: charset::decode(&e.text),
243        cursor: e.cursor,
244    })
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use std::collections::HashMap;
251
252    #[derive(Default)]
253    struct Ram(HashMap<u32, u8>);
254
255    impl Memory for Ram {
256        fn nibble(&self, addr: u32) -> Option<u8> {
257            Some(self.0.get(&addr).copied().unwrap_or(0))
258        }
259    }
260
261    impl Ram {
262        fn put(&mut self, addr: u32, value: u32, len: u32) {
263            for i in 0..len {
264                self.0.insert(addr + i, ((value >> (4 * i)) & 15) as u8);
265            }
266        }
267    }
268
269    #[test]
270    fn reads_the_reversed_text_and_the_flags() {
271        let l = EditorLayout::HP48SX;
272        let mut ram = Ram::default();
273        // "ABCDEF" as the 48SX keeps it (observed: 645444342414 at #7FED7).
274        ram.put(l.text_end_ptr, 0x7FEE3, 5);
275        ram.put(l.stack_end_ptr, 0x7FEB3, 5);
276        for (i, b) in b"FEDCBA".iter().enumerate() {
277            ram.put(0x7FED7 + 2 * i as u32, u32::from(*b), 2);
278        }
279        ram.put(l.cursor, 4, 5);
280        ram.put(l.line, 3, 1);
281        ram.put(l.entry, 3, 1);
282        ram.put(l.keys, 4, 1);
283        let e = read(&ram, &l).unwrap();
284        assert!(e.active && e.insert && !e.algebraic && e.alpha && !e.alpha_lock);
285        assert_eq!(e.text, b"ABCDEF");
286        assert_eq!(e.cursor, 4);
287        // Closed: the stale text is not reported.
288        ram.put(l.line, 1, 1);
289        let e = read(&ram, &l).unwrap();
290        assert!(!e.active && e.text.is_empty() && e.cursor == 0);
291    }
292
293    #[test]
294    fn the_text_stops_at_the_stack() {
295        let l = EditorLayout::HP48GX;
296        let mut ram = Ram::default();
297        ram.put(l.text_end_ptr, 0x90010, 5);
298        ram.put(l.stack_end_ptr, 0x9000C, 5);
299        ram.put(0x9000C, 0x4242, 4);
300        ram.put(0x9000E, 0x41, 2);
301        ram.put(l.line, 2, 1);
302        assert_eq!(read(&ram, &l).unwrap().text, b"AB");
303        ram.put(l.stack_end_ptr, 0x90020, 5);
304        assert!(read(&ram, &l).is_err());
305    }
306
307    #[test]
308    fn aplet_models_have_none() {
309        assert!(EditorLayout::of(Model::Hp38g).is_none());
310        let err = EditorLayout::require(Model::Hp42s).unwrap_err();
311        assert!(err.to_string().contains("42S has no RPL command line"));
312    }
313}