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