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> {
101 match model {
102 Model::Hp48sx => Some(Self::HP48SX),
103 Model::Hp48gx => Some(Self::HP48GX),
104 Model::Hp49g => Some(Self::HP49G),
105 Model::Hp38g
106 | Model::Hp39g
107 | Model::Hp40g
108 | Model::Hp42s
109 | Model::Hp27s
110 | Model::Hp17bii
111 | Model::Hp17b
112 | Model::Hp28s => None,
113 }
114 }
115
116 pub fn require(model: Model) -> Result<EditorLayout> {
118 Self::of(model).with_context(|| {
119 let what = if model == Model::Hp28s {
120 "has a command line this cannot read or type into yet"
121 } else {
122 "has no RPL command line to read or type into"
123 };
124 format!(
125 "the {} {what} (48SX, 48GX and 49G only)",
126 model.name().to_uppercase()
127 )
128 })
129 }
130}
131
132#[derive(Clone, Debug, Default, PartialEq, Eq)]
134pub struct Editor {
135 pub active: bool,
137 pub text: Vec<u8>,
139 pub cursor: usize,
141 pub insert: bool,
143 pub algebraic: bool,
145 pub program: bool,
147 pub lowercase: bool,
149 pub alpha: bool,
151 pub alpha_lock: bool,
153 pub left_shift: bool,
155 pub right_shift: bool,
157 pub message: bool,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
163pub struct CommandLine {
164 pub active: bool,
166 pub text: String,
168 pub cursor: usize,
170}
171
172fn nibble(mem: &dyn Memory, addr: u32) -> Result<u8> {
173 mem.nibble(addr)
174 .with_context(|| format!("#{addr:05X} is not readable"))
175}
176
177fn field(mem: &dyn Memory, addr: u32, len: u32) -> Result<u32> {
178 let mut v = 0;
179 for i in (0..len).rev() {
180 v = (v << 4) | u32::from(nibble(mem, addr + i)?);
181 }
182 Ok(v)
183}
184
185fn bit(mem: &dyn Memory, addr: u32, b: u8) -> Result<bool> {
186 Ok(nibble(mem, addr)? & (1 << b) != 0)
187}
188
189pub fn read(mem: &dyn Memory, layout: &EditorLayout) -> Result<Editor> {
191 let active = bit(mem, layout.line, 1)?;
192 let mut text = Vec::new();
193 if active {
194 let end = field(mem, layout.text_end_ptr, 5)?;
195 let floor = field(mem, layout.stack_end_ptr, 5)?;
196 ensure!(
197 floor < end,
198 "the command line pointers (#{floor:05X}, #{end:05X}) are not the ROM's"
199 );
200 let mut a = end;
201 while a >= floor + 2 {
202 a -= 2;
203 let b = field(mem, a, 2)? as u8;
204 if b == 0 {
205 break;
206 }
207 ensure!(
208 text.len() < MAX_CHARS,
209 "the command line is longer than {MAX_CHARS} characters"
210 );
211 text.push(b);
212 }
213 }
214 let cursor = field(mem, layout.cursor, 5)? as usize;
215 let keys = nibble(mem, layout.keys)?;
216 let message = if layout.message_box {
217 !active && (nibble(mem, layout.message)? == 0xF || nibble(mem, layout.message + 1)? == 0xF)
218 } else {
219 bit(mem, layout.message, 0)?
220 };
221 Ok(Editor {
222 message,
223 active,
224 cursor: if active { cursor.min(text.len()) } else { 0 },
225 text,
226 insert: bit(mem, layout.entry, 1)?,
227 algebraic: bit(mem, layout.entry, 2)?,
228 program: bit(mem, layout.program, 0)?,
229 lowercase: bit(mem, layout.line, 2)?,
230 alpha: keys & 4 != 0,
231 alpha_lock: bit(mem, layout.alpha_lock, 0)?,
232 left_shift: keys & 1 != 0,
233 right_shift: keys & 2 != 0,
234 })
235}
236
237pub fn editor(machine: &Machine) -> Result<Editor> {
239 read(machine, &EditorLayout::require(machine.model())?)
240}
241
242pub fn command_line(machine: &Machine) -> Result<CommandLine> {
244 let e = editor(machine)?;
245 Ok(CommandLine {
246 active: e.active,
247 text: charset::decode(&e.text),
248 cursor: e.cursor,
249 })
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use std::collections::HashMap;
256
257 #[derive(Default)]
258 struct Ram(HashMap<u32, u8>);
259
260 impl Memory for Ram {
261 fn nibble(&self, addr: u32) -> Option<u8> {
262 Some(self.0.get(&addr).copied().unwrap_or(0))
263 }
264 }
265
266 impl Ram {
267 fn put(&mut self, addr: u32, value: u32, len: u32) {
268 for i in 0..len {
269 self.0.insert(addr + i, ((value >> (4 * i)) & 15) as u8);
270 }
271 }
272 }
273
274 #[test]
275 fn reads_the_reversed_text_and_the_flags() {
276 let l = EditorLayout::HP48SX;
277 let mut ram = Ram::default();
278 ram.put(l.text_end_ptr, 0x7FEE3, 5);
280 ram.put(l.stack_end_ptr, 0x7FEB3, 5);
281 for (i, b) in b"FEDCBA".iter().enumerate() {
282 ram.put(0x7FED7 + 2 * i as u32, u32::from(*b), 2);
283 }
284 ram.put(l.cursor, 4, 5);
285 ram.put(l.line, 3, 1);
286 ram.put(l.entry, 3, 1);
287 ram.put(l.keys, 4, 1);
288 let e = read(&ram, &l).unwrap();
289 assert!(e.active && e.insert && !e.algebraic && e.alpha && !e.alpha_lock);
290 assert_eq!(e.text, b"ABCDEF");
291 assert_eq!(e.cursor, 4);
292 ram.put(l.line, 1, 1);
294 let e = read(&ram, &l).unwrap();
295 assert!(!e.active && e.text.is_empty() && e.cursor == 0);
296 }
297
298 #[test]
299 fn the_text_stops_at_the_stack() {
300 let l = EditorLayout::HP48GX;
301 let mut ram = Ram::default();
302 ram.put(l.text_end_ptr, 0x90010, 5);
303 ram.put(l.stack_end_ptr, 0x9000C, 5);
304 ram.put(0x9000C, 0x4242, 4);
305 ram.put(0x9000E, 0x41, 2);
306 ram.put(l.line, 2, 1);
307 assert_eq!(read(&ram, &l).unwrap().text, b"AB");
308 ram.put(l.stack_end_ptr, 0x90020, 5);
309 assert!(read(&ram, &l).is_err());
310 }
311
312 #[test]
313 fn aplet_models_have_none() {
314 assert!(EditorLayout::of(Model::Hp38g).is_none());
315 let err = EditorLayout::require(Model::Hp42s).unwrap_err();
316 assert!(err.to_string().contains("42S has no RPL command line"));
317 }
318}