1use anyhow::{Context, Result, ensure};
10use saturnus_core::{Machine, Model};
11use serde::Serialize;
12
13use crate::charset;
14use crate::object::Memory;
15
16pub const MAX_CHARS: usize = 65_536;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct EditorLayout {
25 pub text_end_ptr: u32,
28 pub stack_end_ptr: u32,
30 pub cursor: u32,
32 pub entry: u32,
34 pub line: u32,
36 pub keys: u32,
38 pub alpha_lock: u32,
40 pub program: u32,
42 pub message: u32,
46 pub message_box: bool,
48 pub chars_position: Option<u32>,
52}
53
54impl EditorLayout {
55 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 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 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 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 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
121pub struct Editor {
122 pub active: bool,
124 pub text: Vec<u8>,
126 pub cursor: usize,
128 pub insert: bool,
130 pub algebraic: bool,
132 pub program: bool,
134 pub lowercase: bool,
136 pub alpha: bool,
138 pub alpha_lock: bool,
140 pub left_shift: bool,
142 pub right_shift: bool,
144 pub message: bool,
146}
147
148#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
150pub struct CommandLine {
151 pub active: bool,
153 pub text: String,
155 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
176pub 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
224pub fn editor(machine: &Machine) -> Result<Editor> {
226 read(machine, &EditorLayout::require(machine.model())?)
227}
228
229pub 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 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 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}