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