1use 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
19pub const MAX_CHARS: usize = 65_536;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct EditorLayout {
28 pub text_end_ptr: u32,
31 pub stack_end_ptr: u32,
33 pub cursor: u32,
35 pub entry: u32,
37 pub line: u32,
39 pub keys: u32,
41 pub alpha_lock: u32,
43 pub program: u32,
45 pub message: u32,
49 pub message_box: bool,
51 pub chars_position: Option<u32>,
55}
56
57impl EditorLayout {
58 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 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 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 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 pub fn require(model: Model) -> Result<EditorLayout> {
123 Self::of(model).ok_or(Error::NoCommandLine(model))
124 }
125}
126
127#[derive(Clone, Copy, Debug)]
132pub struct Hp28;
133
134impl Hp28 {
135 pub const TEXT_END_PTR: u32 = 0xC006E;
137 pub const FLOOR_PTR: u32 = 0xC0069;
139 pub const CURSOR: u32 = 0xC0126;
141 pub const OPEN: u32 = 0xC015D;
143 pub const ENTRY: u32 = 0xC00EE;
146 pub const ALPHA_ENTRY: u32 = 0xC00ED;
148 pub const LOWERCASE: u32 = 0xC00F0;
150}
151
152#[derive(Clone, Debug, Default, PartialEq, Eq)]
154pub struct Editor {
155 pub active: bool,
157 pub text: Vec<u8>,
159 pub cursor: usize,
161 pub insert: bool,
163 pub algebraic: bool,
165 pub program: bool,
168 pub lowercase: bool,
170 pub alpha: bool,
172 pub alpha_lock: bool,
174 pub left_shift: bool,
176 pub right_shift: bool,
178 pub message: bool,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
184pub struct CommandLine {
185 pub active: bool,
187 pub text: String,
189 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
209fn 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
237pub 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
268pub 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
291pub 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
299pub 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 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 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 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 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}