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::{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 and the 42S.
99    pub fn of(model: Model) -> Option<EditorLayout> {
100        match model {
101            Model::Hp48sx => Some(Self::HP48SX),
102            Model::Hp48gx => Some(Self::HP48GX),
103            Model::Hp49g => Some(Self::HP49G),
104            Model::Hp38g | Model::Hp39g | Model::Hp40g | Model::Hp42s => None,
105        }
106    }
107
108    /// The layout of `model` or an error naming what the model lacks.
109    pub fn require(model: Model) -> Result<EditorLayout> {
110        Self::of(model).with_context(|| {
111            format!(
112                "the {} has no RPL command line to read or type into (48SX, 48GX and 49G only)",
113                model.name().to_uppercase()
114            )
115        })
116    }
117}
118
119/// The editor as the ROM left it.
120#[derive(Clone, Debug, Default, PartialEq, Eq)]
121pub struct Editor {
122    /// A command line is open (also an empty one, and in `EDIT`).
123    pub active: bool,
124    /// The text as HP bytes, empty when no line is open.
125    pub text: Vec<u8>,
126    /// The cursor in characters, 0 before the first.
127    pub cursor: usize,
128    /// Insert mode (off: typing replaces the character at the cursor).
129    pub insert: bool,
130    /// Algebraic entry mode (after `'`).
131    pub algebraic: bool,
132    /// Program entry mode (after `«`, or ENTRY).
133    pub program: bool,
134    /// Lowercase lock.
135    pub lowercase: bool,
136    /// Alpha is on (for one key, or locked).
137    pub alpha: bool,
138    /// Alpha is locked.
139    pub alpha_lock: bool,
140    /// The left shift is pending.
141    pub left_shift: bool,
142    /// The right shift is pending.
143    pub right_shift: bool,
144    /// The ROM shows an error: in the header (48) or in a box (49G).
145    pub message: bool,
146}
147
148/// The `commandLine` protocol answer.
149#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
150pub struct CommandLine {
151    /// A command line is open.
152    pub active: bool,
153    /// Its text (empty when none is open).
154    pub text: String,
155    /// The cursor in calculator characters (0 before the first).
156    pub cursor: usize,
157}
158
159fn nibble(mem: &dyn Memory, addr: u32) -> Result<u8> {
160    mem.nibble(addr)
161        .with_context(|| format!("#{addr:05X} is not readable"))
162}
163
164fn field(mem: &dyn Memory, addr: u32, len: u32) -> Result<u32> {
165    let mut v = 0;
166    for i in (0..len).rev() {
167        v = (v << 4) | u32::from(nibble(mem, addr + i)?);
168    }
169    Ok(v)
170}
171
172fn bit(mem: &dyn Memory, addr: u32, b: u8) -> Result<bool> {
173    Ok(nibble(mem, addr)? & (1 << b) != 0)
174}
175
176/// Read the editor through `layout`.
177pub fn read(mem: &dyn Memory, layout: &EditorLayout) -> Result<Editor> {
178    let active = bit(mem, layout.line, 1)?;
179    let mut text = Vec::new();
180    if active {
181        let end = field(mem, layout.text_end_ptr, 5)?;
182        let floor = field(mem, layout.stack_end_ptr, 5)?;
183        ensure!(
184            floor < end,
185            "the command line pointers (#{floor:05X}, #{end:05X}) are not the ROM's"
186        );
187        let mut a = end;
188        while a >= floor + 2 {
189            a -= 2;
190            let b = field(mem, a, 2)? as u8;
191            if b == 0 {
192                break;
193            }
194            ensure!(
195                text.len() < MAX_CHARS,
196                "the command line is longer than {MAX_CHARS} characters"
197            );
198            text.push(b);
199        }
200    }
201    let cursor = field(mem, layout.cursor, 5)? as usize;
202    let keys = nibble(mem, layout.keys)?;
203    let message = if layout.message_box {
204        !active && (nibble(mem, layout.message)? == 0xF || nibble(mem, layout.message + 1)? == 0xF)
205    } else {
206        bit(mem, layout.message, 0)?
207    };
208    Ok(Editor {
209        message,
210        active,
211        cursor: if active { cursor.min(text.len()) } else { 0 },
212        text,
213        insert: bit(mem, layout.entry, 1)?,
214        algebraic: bit(mem, layout.entry, 2)?,
215        program: bit(mem, layout.program, 0)?,
216        lowercase: bit(mem, layout.line, 2)?,
217        alpha: keys & 4 != 0,
218        alpha_lock: bit(mem, layout.alpha_lock, 0)?,
219        left_shift: keys & 1 != 0,
220        right_shift: keys & 2 != 0,
221    })
222}
223
224/// The editor of `machine` (48SX, 48GX, 49G).
225pub fn editor(machine: &Machine) -> Result<Editor> {
226    read(machine, &EditorLayout::require(machine.model())?)
227}
228
229/// The command line of `machine`: open or not, its text and cursor.
230pub fn command_line(machine: &Machine) -> Result<CommandLine> {
231    let e = editor(machine)?;
232    Ok(CommandLine {
233        active: e.active,
234        text: charset::decode(&e.text),
235        cursor: e.cursor,
236    })
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::collections::HashMap;
243
244    #[derive(Default)]
245    struct Ram(HashMap<u32, u8>);
246
247    impl Memory for Ram {
248        fn nibble(&self, addr: u32) -> Option<u8> {
249            Some(self.0.get(&addr).copied().unwrap_or(0))
250        }
251    }
252
253    impl Ram {
254        fn put(&mut self, addr: u32, value: u32, len: u32) {
255            for i in 0..len {
256                self.0.insert(addr + i, ((value >> (4 * i)) & 15) as u8);
257            }
258        }
259    }
260
261    #[test]
262    fn reads_the_reversed_text_and_the_flags() {
263        let l = EditorLayout::HP48SX;
264        let mut ram = Ram::default();
265        // "ABCDEF" as the 48SX keeps it (observed: 645444342414 at #7FED7).
266        ram.put(l.text_end_ptr, 0x7FEE3, 5);
267        ram.put(l.stack_end_ptr, 0x7FEB3, 5);
268        for (i, b) in b"FEDCBA".iter().enumerate() {
269            ram.put(0x7FED7 + 2 * i as u32, u32::from(*b), 2);
270        }
271        ram.put(l.cursor, 4, 5);
272        ram.put(l.line, 3, 1);
273        ram.put(l.entry, 3, 1);
274        ram.put(l.keys, 4, 1);
275        let e = read(&ram, &l).unwrap();
276        assert!(e.active && e.insert && !e.algebraic && e.alpha && !e.alpha_lock);
277        assert_eq!(e.text, b"ABCDEF");
278        assert_eq!(e.cursor, 4);
279        // Closed: the stale text is not reported.
280        ram.put(l.line, 1, 1);
281        let e = read(&ram, &l).unwrap();
282        assert!(!e.active && e.text.is_empty() && e.cursor == 0);
283    }
284
285    #[test]
286    fn the_text_stops_at_the_stack() {
287        let l = EditorLayout::HP48GX;
288        let mut ram = Ram::default();
289        ram.put(l.text_end_ptr, 0x90010, 5);
290        ram.put(l.stack_end_ptr, 0x9000C, 5);
291        ram.put(0x9000C, 0x4242, 4);
292        ram.put(0x9000E, 0x41, 2);
293        ram.put(l.line, 2, 1);
294        assert_eq!(read(&ram, &l).unwrap().text, b"AB");
295        ram.put(l.stack_end_ptr, 0x90020, 5);
296        assert!(read(&ram, &l).is_err());
297    }
298
299    #[test]
300    fn aplet_models_have_none() {
301        assert!(EditorLayout::of(Model::Hp38g).is_none());
302        let err = EditorLayout::require(Model::Hp42s).unwrap_err();
303        assert!(err.to_string().contains("42S has no RPL command line"));
304    }
305}