use crate::error::Error;
use saturnus_core::{Machine, Model};
use serde::Serialize;
use crate::charset;
use crate::object::Memory;
type Result<T, E = Error> = std::result::Result<T, E>;
pub const MAX_CHARS: usize = 65_536;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EditorLayout {
pub text_end_ptr: u32,
pub stack_end_ptr: u32,
pub cursor: u32,
pub entry: u32,
pub line: u32,
pub keys: u32,
pub alpha_lock: u32,
pub program: u32,
pub message: u32,
pub message_box: bool,
pub chars_position: Option<u32>,
}
impl EditorLayout {
pub const HP48SX: EditorLayout = EditorLayout {
text_end_ptr: 0x70583,
stack_end_ptr: 0x7057E,
cursor: 0x70704,
entry: 0x70685,
line: 0x70687,
keys: 0x706C4,
alpha_lock: 0x70793,
program: 0x70794,
message: 0x7068B,
message_box: false,
chars_position: None,
};
pub const HP48GX: EditorLayout = EditorLayout {
text_end_ptr: 0x80702,
stack_end_ptr: 0x806FD,
cursor: 0x80882,
entry: 0x80803,
line: 0x80805,
keys: 0x80842,
alpha_lock: 0x80911,
program: 0x80912,
message: 0x80809,
message_box: false,
chars_position: None,
};
pub const HP49G: EditorLayout = EditorLayout {
text_end_ptr: 0x80702,
stack_end_ptr: 0x806FD,
cursor: 0x80F61,
entry: 0x80EC2,
line: 0x80EC4,
keys: 0x80F01,
alpha_lock: 0x80FF0,
program: 0x80FF1,
message: 0x80EC8,
message_box: true,
chars_position: Some(0x818CF),
};
pub fn of(model: Model) -> Option<EditorLayout> {
match model {
Model::Hp48sx => Some(Self::HP48SX),
Model::Hp48gx => Some(Self::HP48GX),
Model::Hp49g => Some(Self::HP49G),
Model::Hp38g
| Model::Hp39g
| Model::Hp40g
| Model::Hp42s
| Model::Hp27s
| Model::Hp17bii
| Model::Hp17b
| Model::Hp28s
| Model::Hp19bii => None,
}
}
pub fn require(model: Model) -> Result<EditorLayout> {
Self::of(model).ok_or(Error::NoCommandLine(model))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Editor {
pub active: bool,
pub text: Vec<u8>,
pub cursor: usize,
pub insert: bool,
pub algebraic: bool,
pub program: bool,
pub lowercase: bool,
pub alpha: bool,
pub alpha_lock: bool,
pub left_shift: bool,
pub right_shift: bool,
pub message: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct CommandLine {
pub active: bool,
pub text: String,
pub cursor: usize,
}
fn nibble(mem: &dyn Memory, addr: u32) -> Result<u8> {
mem.nibble(addr).ok_or(Error::Unreadable(addr))
}
fn field(mem: &dyn Memory, addr: u32, len: u32) -> Result<u32> {
let mut v = 0;
for i in (0..len).rev() {
v = (v << 4) | u32::from(nibble(mem, addr + i)?);
}
Ok(v)
}
fn bit(mem: &dyn Memory, addr: u32, b: u8) -> Result<bool> {
Ok(nibble(mem, addr)? & (1 << b) != 0)
}
pub fn read(mem: &dyn Memory, layout: &EditorLayout) -> Result<Editor> {
let active = bit(mem, layout.line, 1)?;
let mut text = Vec::new();
if active {
let end = field(mem, layout.text_end_ptr, 5)?;
let floor = field(mem, layout.stack_end_ptr, 5)?;
if floor >= end {
return Err(Error::Memory(format!(
"the command line pointers (#{floor:05X}, #{end:05X}) are not the ROM's"
)));
}
let mut a = end;
while a >= floor + 2 {
a -= 2;
let b = field(mem, a, 2)? as u8;
if b == 0 {
break;
}
if text.len() >= MAX_CHARS {
return Err(Error::Memory(format!(
"the command line is longer than {MAX_CHARS} characters"
)));
}
text.push(b);
}
}
let cursor = field(mem, layout.cursor, 5)? as usize;
let keys = nibble(mem, layout.keys)?;
let message = if layout.message_box {
!active && (nibble(mem, layout.message)? == 0xF || nibble(mem, layout.message + 1)? == 0xF)
} else {
bit(mem, layout.message, 0)?
};
Ok(Editor {
message,
active,
cursor: if active { cursor.min(text.len()) } else { 0 },
text,
insert: bit(mem, layout.entry, 1)?,
algebraic: bit(mem, layout.entry, 2)?,
program: bit(mem, layout.program, 0)?,
lowercase: bit(mem, layout.line, 2)?,
alpha: keys & 4 != 0,
alpha_lock: bit(mem, layout.alpha_lock, 0)?,
left_shift: keys & 1 != 0,
right_shift: keys & 2 != 0,
})
}
pub fn editor(machine: &Machine) -> Result<Editor> {
read(machine, &EditorLayout::require(machine.model())?)
}
pub fn command_line(machine: &Machine) -> Result<CommandLine> {
let e = editor(machine)?;
Ok(CommandLine {
active: e.active,
text: charset::decode(&e.text),
cursor: e.cursor,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[derive(Default)]
struct Ram(HashMap<u32, u8>);
impl Memory for Ram {
fn nibble(&self, addr: u32) -> Option<u8> {
Some(self.0.get(&addr).copied().unwrap_or(0))
}
}
impl Ram {
fn put(&mut self, addr: u32, value: u32, len: u32) {
for i in 0..len {
self.0.insert(addr + i, ((value >> (4 * i)) & 15) as u8);
}
}
}
#[test]
fn reads_the_reversed_text_and_the_flags() {
let l = EditorLayout::HP48SX;
let mut ram = Ram::default();
ram.put(l.text_end_ptr, 0x7FEE3, 5);
ram.put(l.stack_end_ptr, 0x7FEB3, 5);
for (i, b) in b"FEDCBA".iter().enumerate() {
ram.put(0x7FED7 + 2 * i as u32, u32::from(*b), 2);
}
ram.put(l.cursor, 4, 5);
ram.put(l.line, 3, 1);
ram.put(l.entry, 3, 1);
ram.put(l.keys, 4, 1);
let e = read(&ram, &l).unwrap();
assert!(e.active && e.insert && !e.algebraic && e.alpha && !e.alpha_lock);
assert_eq!(e.text, b"ABCDEF");
assert_eq!(e.cursor, 4);
ram.put(l.line, 1, 1);
let e = read(&ram, &l).unwrap();
assert!(!e.active && e.text.is_empty() && e.cursor == 0);
}
#[test]
fn the_text_stops_at_the_stack() {
let l = EditorLayout::HP48GX;
let mut ram = Ram::default();
ram.put(l.text_end_ptr, 0x90010, 5);
ram.put(l.stack_end_ptr, 0x9000C, 5);
ram.put(0x9000C, 0x4242, 4);
ram.put(0x9000E, 0x41, 2);
ram.put(l.line, 2, 1);
assert_eq!(read(&ram, &l).unwrap().text, b"AB");
ram.put(l.stack_end_ptr, 0x90020, 5);
assert!(read(&ram, &l).is_err());
}
#[test]
fn aplet_models_have_none() {
assert!(EditorLayout::of(Model::Hp38g).is_none());
let err = EditorLayout::require(Model::Hp42s).unwrap_err();
assert!(err.to_string().contains("42S has no RPL command line"));
}
}