Skip to main content

saturnus_objects/
softkeys.rs

1//! What the six menu keys show now (48SX, 48GX, 49G), read from RAM
2//! without pressing a key: the current menu (its pointer, wiki:
3//! hardware/hp48-system-ram, "Menu") and its page (the first key shown,
4//! same section), the menu's keys from the ROM's own definition
5//! ([`crate::menus`]), the VAR menu's from the current directory.
6//!
7//! Only while the stack has the keyboard ([`crate::screen`]): an input
8//! form, a choose box or another application draws its own keys. A menu
9//! the ROM builds in RAM (the 49G's TOOL menu, a form's menu), the CST
10//! menu and a menu whose definition gives no keys read as unknown.
11
12use saturnus_core::Machine;
13use serde::Serialize;
14
15use crate::error::Error;
16use crate::menus::{MenuKey, Menus};
17use crate::ram::{UserMemory, Variable};
18
19type Result<T, E = Error> = std::result::Result<T, E>;
20
21/// Keys on one page of a menu.
22pub const PAGE: usize = 6;
23
24/// The VAR menu's number (`2 MENU`).
25pub const VAR_MENU: u32 = 2;
26
27/// One menu key as the screen shows it.
28#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ShownKey {
31    /// What its label says: the command's or variable's name, or the
32    /// definition's own label.
33    pub label: String,
34    /// The one command it runs.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub command: Option<String>,
37    /// It opens another menu.
38    #[serde(skip_serializing_if = "std::ops::Not::not")]
39    pub submenu: bool,
40    /// A variable's type, as `G D` names it (`Real Number`, `Directory`).
41    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
42    pub kind: Option<String>,
43}
44
45impl ShownKey {
46    fn of_key(k: &MenuKey) -> Option<ShownKey> {
47        Some(ShownKey {
48            label: k.text()?.to_string(),
49            command: k.command.clone(),
50            submenu: k.submenu.is_some(),
51            kind: None,
52        })
53    }
54
55    fn of_variable(v: &Variable) -> ShownKey {
56        ShownKey {
57            label: v.name.clone(),
58            command: None,
59            submenu: false,
60            kind: Some(v.kind.clone()),
61        }
62    }
63}
64
65/// The page that starts at key `first` (from 0) of a menu of `count`
66/// keys: the six keys' indexes, `None` past the end; `None` for a page
67/// that does not start inside the menu.
68fn page(first: usize, count: usize) -> Option<[Option<usize>; PAGE]> {
69    if first >= count.max(1) || !first.is_multiple_of(PAGE) {
70        return None;
71    }
72    Some(std::array::from_fn(|i| {
73        let k = first + i;
74        (k < count).then_some(k)
75    }))
76}
77
78/// The six keys of menu `number` on the page starting at key `first`:
79/// from the definition in `defs`, or for the VAR menu from `variables`
80/// (the current directory's, newest first). `None` where the keys are
81/// not known.
82pub fn shown(
83    defs: &Menus,
84    number: u32,
85    first: usize,
86    variables: Option<&[Variable]>,
87) -> Option<Vec<Option<ShownKey>>> {
88    if number == VAR_MENU {
89        let vars = variables?;
90        let keys = page(first, vars.len())?;
91        return Some(
92            keys.iter()
93                .map(|k| k.map(|i| ShownKey::of_variable(&vars[i])))
94                .collect(),
95        );
96    }
97    let def = defs.get(number)?;
98    if def.keys.is_empty() {
99        return None;
100    }
101    let keys = page(first, def.keys.len())?;
102    Some(
103        keys.iter()
104            .map(|k| k.and_then(|i| ShownKey::of_key(&def.keys[i])))
105            .collect(),
106    )
107}
108
109/// What identifies the menu shown and what its keys depend on, read
110/// cheaply: the menu pointer, the page, the current directory, the end
111/// of HOME, the data stack's top and the return stack's top. When none
112/// of these moved, [`read`] gives the same keys.
113pub fn signature(machine: &Machine) -> Result<[u32; 6]> {
114    let u = UserMemory::of(machine)?;
115    let l = u.layout();
116    let screen = crate::screen::ScreenLayout::of(machine.model())
117        .ok_or(Error::NoCommandLine(machine.model()))?;
118    let p = |a: u32| -> Result<u32> {
119        (0..5).rev().try_fold(0u32, |v, i| {
120            let n =
121                crate::object::Memory::nibble(machine, a + i).ok_or(Error::Unreadable(a + i))?;
122            Ok((v << 4) | u32::from(n))
123        })
124    };
125    Ok([
126        p(l.menu_ptr)?,
127        p(l.menu_page)?,
128        p(l.context_ptr)?,
129        p(l.home_end_ptr)?,
130        p(l.stack_ptr)?,
131        p(screen.return_top_ptr)?,
132    ])
133}
134
135/// The six menu keys the screen shows now, read from RAM while the ROM
136/// waits for a key (48SX, 48GX, 49G); `None` where they are not known
137/// (another screen has the keyboard, a menu built in RAM, CST). `defs`
138/// are the ROM's menus ([`crate::menus::read`]).
139pub fn read(machine: &Machine, defs: &Menus) -> Result<Option<Vec<Option<ShownKey>>>> {
140    if !crate::screen::screen(machine)?.stack_in_control {
141        return Ok(None);
142    }
143    let u = UserMemory::of(machine)?;
144    let Some(number) = defs.number(u.menu_pointer()?, machine) else {
145        return Ok(None);
146    };
147    let first = u.menu_first_key()?;
148    let vars = if number == VAR_MENU {
149        Some(u.current_variables()?)
150    } else {
151        None
152    };
153    Ok(shown(defs, number, first, vars.as_deref()))
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    fn var(name: &str, kind: &str) -> Variable {
161        Variable {
162            name: name.into(),
163            kind: kind.into(),
164            size: 0.0,
165            checksum: 0,
166            address: 0,
167            variables: None,
168        }
169    }
170
171    #[test]
172    fn pages_of_six() {
173        assert_eq!(
174            page(0, 3),
175            Some([Some(0), Some(1), Some(2), None, None, None])
176        );
177        assert_eq!(page(6, 8), Some([Some(6), Some(7), None, None, None, None]));
178        assert_eq!(page(6, 6), None, "no second page");
179        assert_eq!(page(3, 12), None, "not a page start");
180        assert_eq!(page(0, 0), Some([None; PAGE]), "an empty menu");
181    }
182
183    #[test]
184    fn the_var_menu_names_variables_and_their_types() {
185        let defs = Menus::default();
186        let vars: Vec<Variable> = (0..8)
187            .map(|i| {
188                var(
189                    &format!("V{i}"),
190                    if i == 7 { "Directory" } else { "Real Number" },
191                )
192            })
193            .collect();
194        let keys = shown(&defs, VAR_MENU, 6, Some(&vars)).unwrap();
195        assert_eq!(keys.len(), PAGE);
196        assert_eq!(
197            keys[0],
198            Some(ShownKey {
199                label: "V6".into(),
200                kind: Some("Real Number".into()),
201                ..Default::default()
202            })
203        );
204        assert_eq!(keys[1].as_ref().unwrap().kind.as_deref(), Some("Directory"));
205        assert!(keys[2..].iter().all(Option::is_none));
206        assert_eq!(shown(&defs, VAR_MENU, 0, None), None, "no variables read");
207        assert_eq!(shown(&defs, 3, 0, None), None, "no definition");
208    }
209
210    #[test]
211    fn serialized_as_the_page_reads_it() {
212        let k = ShownKey {
213            label: "VECTR".into(),
214            submenu: true,
215            ..Default::default()
216        };
217        assert_eq!(
218            serde_json::to_string(&k).unwrap(),
219            r#"{"label":"VECTR","submenu":true}"#
220        );
221        let c = ShownKey {
222            label: "EXPAN".into(),
223            command: Some("EXPAN".into()),
224            ..Default::default()
225        };
226        assert_eq!(
227            serde_json::to_string(&c).unwrap(),
228            r#"{"label":"EXPAN","command":"EXPAN"}"#
229        );
230    }
231}