1use 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
21pub const PAGE: usize = 6;
23
24pub const VAR_MENU: u32 = 2;
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ShownKey {
31 pub label: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub command: Option<String>,
37 #[serde(skip_serializing_if = "std::ops::Not::not")]
39 pub submenu: bool,
40 #[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
65fn 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
78pub 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
109pub 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
135pub 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}