use crate::abend::Abend;
use crate::intrinsic::numval::{self, Form};
use crate::storage::{Kind, Loc, Val, literal_fixed};
use crate::store::{self, ProgramFacts};
use crate::terminal::{AID_ENTER, Action};
use crate::unit::{Loader, RunUnit};
use crate::vocab::Pos;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
pub const ROWS: usize = 24;
pub const COLUMNS: usize = 80;
pub fn crt_status(key: u8) -> u16 {
match key {
crate::terminal::AID_ENTER => 0,
crate::terminal::AID_CLEAR => 2005,
crate::terminal::AID_PA1 => 2001,
crate::terminal::AID_PA2 => 2002,
crate::terminal::AID_PA3 => 2006,
_ => crate::terminal::pf_number(key).map_or(9000, |n| 1000 + u16::from(n)),
}
}
pub fn line_column(at: u64) -> (usize, usize) {
if at > 9999 { ((at / 1000) as usize, (at % 1000) as usize) } else { ((at / 100) as usize, (at % 100) as usize) }
}
fn screen<H, L: Loader<H>>(unit: &mut RunUnit<'_, H, L>) -> Rc<RefCell<Crt>> {
unit.crt.get_or_insert_with(|| Rc::new(RefCell::new(Crt::new(ROWS, COLUMNS, Vec::new())))).clone()
}
pub fn display<H, L: Loader<H>>(unit: &mut RunUnit<'_, H, L>, at: Option<(usize, usize)>, text: &str, clearing: Clearing) {
screen(unit).borrow_mut().display(at, text, clearing);
}
#[derive(Clone, Copy, Debug)]
pub struct Input {
pub target: Loc,
pub field: Loc,
pub at: Option<(usize, usize)>,
pub update: bool,
pub secure: bool,
}
pub fn accept<H, L: Loader<H>>(facts: &dyn ProgramFacts, unit: &mut RunUnit<'_, H, L>, inputs: &[Input], pos: Pos) -> Result<bool, Abend> {
let crt = screen(unit);
let mut fields = Vec::with_capacity(inputs.len());
let mut shown = Vec::with_capacity(inputs.len());
for input in inputs {
let text = crate::display::place(facts, &unit.mem, input.field, pos, false)?;
let (row, column) = input.at.unwrap_or_else(|| crt.borrow().cursor_position());
fields.push(Field { row, column, len: text.chars().count().max(1), secure: input.secure });
shown.push(text);
}
let current: Vec<Option<&str>> = inputs.iter().zip(&shown).map(|(i, t)| i.update.then_some(t.as_str())).collect();
let Some(entry) = crt.borrow_mut().accept(&fields, ¤t) else {
return Err(Abend::ironwork("ACCEPT: the screen has no more operator input", pos));
};
for ((input, field), text) in inputs.iter().zip(&fields).zip(&entry.texts) {
let val = if is_numeric(input.field.kind) || is_numeric(input.target.kind) {
let typed = text.trim();
let number = numval::parse(typed, Form::Numval, 31, facts.decimal_point() == ',').ok().and_then(|n| {
let digits = n.digits.to_string();
let (int, frac) = digits.split_at(digits.len().saturating_sub(n.decimals as usize));
literal_fixed(&format!("{}{}.{frac}", if n.negative { "-" } else { "" }, if int.is_empty() { "0" } else { int }))
});
Val::Num(number.unwrap_or_else(|| literal_fixed("0").expect("zero")))
} else {
let page = facts.page();
let unknown = page.encode_char('?').unwrap_or(0x6F);
Val::Bytes(text.chars().map(|c| page.encode_char(c).unwrap_or(unknown)).collect())
};
store::assign(facts, unit, input.target, val, None, pos)?;
unit.mark_input(input.target.offset, input.target.len, true);
if input.field.offset != input.target.offset || input.field.item != input.target.item {
let moved = store::read_stored(facts, &unit.mem, input.target, pos)?;
store::assign(facts, unit, input.field, moved, Some(input.target), pos)?;
}
let stored = crate::display::place(facts, &unit.mem, input.field, pos, false)?;
crt.borrow_mut().show(*field, &stored);
}
Ok(entry.key != AID_ENTER)
}
fn is_numeric(kind: Kind) -> bool {
matches!(kind, Kind::Zoned { .. } | Kind::Packed { .. } | Kind::Binary { .. } | Kind::NumericEdited { .. } | Kind::Float(_))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Clearing {
pub screen: bool,
pub line: bool,
pub to_line_end: bool,
pub to_screen_end: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Field {
pub row: usize,
pub column: usize,
pub len: usize,
pub secure: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
pub texts: Vec<String>,
pub key: u8,
}
pub struct Crt {
pub rows: usize,
pub columns: usize,
cells: Vec<char>,
cursor: usize,
actions: VecDeque<Action>,
pub shown: Vec<String>,
pub used: bool,
pub last_key: Option<u8>,
}
impl std::fmt::Debug for Crt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Crt({}x{}, {} actions left)", self.rows, self.columns, self.actions.len())
}
}
impl Crt {
pub fn new(rows: usize, columns: usize, actions: Vec<Action>) -> Self {
Self { rows, columns, cells: vec![' '; rows * columns], cursor: 0, actions: actions.into(), shown: Vec::new(), used: false, last_key: None }
}
pub fn cursor_position(&self) -> (usize, usize) {
(self.cursor / self.columns + 1, self.cursor % self.columns + 1)
}
fn address(&self, at: Option<(usize, usize)>) -> usize {
match at {
Some((row, column)) => ((row.max(1) - 1) * self.columns + column.max(1) - 1).min(self.cells.len() - 1),
None => self.cursor,
}
}
pub fn display(&mut self, at: Option<(usize, usize)>, text: &str, clearing: Clearing) {
self.used = true;
let start = self.address(at);
let line = start / self.columns * self.columns;
if clearing.screen {
self.cells.fill(' ');
}
if clearing.line {
self.cells[line..line + self.columns].fill(' ');
}
if clearing.to_screen_end {
self.cells[start..].fill(' ');
} else if clearing.to_line_end {
self.cells[start..line + self.columns].fill(' ');
}
let mut at = start;
for c in text.chars() {
if at >= self.cells.len() {
break;
}
self.cells[at] = c;
at += 1;
}
self.cursor = at.min(self.cells.len() - 1);
}
pub fn accept(&mut self, fields: &[Field], current: &[Option<&str>]) -> Option<Entry> {
self.used = true;
let spans: Vec<(usize, usize)> = fields
.iter()
.map(|f| {
let start = self.address(Some((f.row, f.column)));
(start, (start + f.len).min(self.cells.len()))
})
.collect();
let mut values: Vec<Vec<char>> = spans.iter().zip(current).map(|(&(start, end), c)| c.unwrap_or("").chars().chain(std::iter::repeat(' ')).take(end - start).collect()).collect();
for (k, f) in fields.iter().enumerate() {
self.paint(*f, spans[k], &values[k]);
}
self.shown.push(self.render());
let field_at = |at: usize| spans.iter().position(|&(start, end)| (start..end).contains(&at));
let mut cursor = spans.first().map_or(self.cursor, |s| s.0);
let typed = |cursor: &mut usize, text: &str, values: &mut Vec<Vec<char>>| {
let Some(k) = field_at(*cursor) else { return };
let (start, end) = spans[k];
values[k][*cursor - start..].fill(' ');
for c in text.chars() {
if *cursor < end {
values[k][*cursor - start] = c;
}
*cursor += 1;
}
};
while let Some(action) = self.actions.pop_front() {
match action {
Action::Text(text) => typed(&mut cursor, &text, &mut values),
Action::Type { row, column, text } => {
cursor = self.address(Some((row, column)));
typed(&mut cursor, &text, &mut values);
}
Action::EraseEof { row, column } => {
let at = self.address(Some((row, column)));
if let Some(k) = field_at(at) {
values[k][at - spans[k].0..].fill(' ');
}
cursor = at;
}
Action::Cursor { row, column } => cursor = self.address(Some((row, column))),
Action::Home => cursor = spans.first().map_or(cursor, |s| s.0),
Action::Tab => {
let next = spans.iter().position(|&(start, _)| start > cursor).unwrap_or(0);
cursor = spans.get(next).map_or(cursor, |s| s.0);
}
Action::Key(key) => {
self.last_key = Some(key);
for (k, f) in fields.iter().enumerate() {
self.paint(*f, spans[k], &values[k]);
}
self.cursor = spans.last().map_or(self.cursor, |s| s.1.min(self.cells.len() - 1));
return Some(Entry { texts: values.into_iter().map(|v| v.into_iter().collect()).collect(), key });
}
}
}
None
}
fn paint(&mut self, field: Field, (start, end): (usize, usize), value: &[char]) {
for (cell, &c) in self.cells[start..end].iter_mut().zip(value) {
*cell = if field.secure && c != ' ' { '*' } else { c };
}
}
pub fn show(&mut self, field: Field, text: &str) {
let start = self.address(Some((field.row, field.column)));
let end = (start + field.len).min(self.cells.len());
for (cell, c) in self.cells[start..end].iter_mut().zip(text.chars().chain(std::iter::repeat(' '))) {
*cell = if field.secure && c != ' ' { '*' } else { c };
}
}
pub fn render(&self) -> String {
let mut lines: Vec<String> = self.cells.chunks(self.columns).map(|row| row.iter().collect::<String>().trim_end().to_owned()).collect();
while lines.last().is_some_and(String::is_empty) {
lines.pop();
}
lines.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal::parse_script;
fn crt(script: &str) -> Crt {
Crt::new(24, 80, parse_script(script).unwrap())
}
#[test]
fn display_writes_at_its_position_and_leaves_the_cursor_after_the_text() {
let mut c = crt("");
c.display(Some((1, 1)), "ROW1COL1", Clearing::default());
c.display(Some((2, 1)), "XXXXXXXXXX", Clearing::default());
c.display(Some((2, 5)), "AB", Clearing { to_line_end: true, ..Clearing::default() });
c.display(None, "CD", Clearing::default());
assert_eq!(c.render(), "ROW1COL1\nXXXXABCD");
c.display(Some((1, 79)), "WRAP", Clearing::default());
assert_eq!(c.render(), "ROW1COL1 WR\nAPXXABCD");
c.display(Some((1, 5)), "Z", Clearing { screen: true, ..Clearing::default() });
assert_eq!(c.render(), " Z");
}
#[test]
fn erase_eos_clears_to_the_end_of_the_screen_and_text_past_it_is_lost() {
let mut c = crt("");
c.display(Some((3, 1)), "KEEP", Clearing::default());
c.display(Some((5, 1)), "GONE", Clearing::default());
c.display(Some((4, 1)), "NEW", Clearing { to_screen_end: true, ..Clearing::default() });
assert_eq!(c.render(), "\n\nKEEP\nNEW");
c.display(Some((24, 79)), "XYZ", Clearing::default());
assert!(c.render().ends_with(&format!("{}XY", " ".repeat(78))));
}
#[test]
fn accept_shows_the_screen_then_takes_what_is_typed_into_the_field_up_to_the_key() {
let mut c = crt("string BOBBY-TOO-LONG\nENTER\ntype 11 3 9\nENTER\n");
c.display(Some((1, 1)), "NAME:", Clearing::default());
let name = c.accept(&[Field { row: 1, column: 7, len: 5, secure: false }], &[None]).unwrap();
assert_eq!(name, Entry { texts: vec!["BOBBY".into()], key: AID_ENTER });
assert_eq!(c.shown, ["NAME:"]);
let qty = c.accept(&[Field { row: 11, column: 1, len: 3, secure: false }], &[Some("007")]).unwrap();
assert_eq!(qty.texts, ["009"]);
assert_eq!(c.shown[1], "NAME: BOBBY\n\n\n\n\n\n\n\n\n\n007");
assert_eq!(c.render(), "NAME: BOBBY\n\n\n\n\n\n\n\n\n\n009");
assert_eq!(c.accept(&[Field { row: 2, column: 1, len: 1, secure: false }], &[None]), None);
}
#[test]
fn a_secure_field_shows_stars_and_eof_clears_the_rest_of_the_field() {
let mut c = crt("string PW\nENTER\neof 3 3\nstring Z\nENTER\n");
let pw = c.accept(&[Field { row: 2, column: 1, len: 4, secure: true }], &[None]).unwrap();
assert_eq!((pw.texts[0].as_str(), c.render()), ("PW ", "\n**".into()));
let rest = c.accept(&[Field { row: 3, column: 1, len: 5, secure: false }], &[Some("ABCDE")]).unwrap();
assert_eq!(rest.texts, ["ABZ "]);
let mut c = crt("string 42\nENTER\n");
assert_eq!(c.accept(&[Field { row: 1, column: 1, len: 3, secure: false }], &[Some("007")]).unwrap().texts, ["42 "]);
}
#[test]
fn tab_moves_between_the_fields_of_one_accept() {
let mut c = crt("string AL\ntab\nstring 9\ntype 1 1 Z\nPF3\n");
let fields = [Field { row: 1, column: 1, len: 3, secure: false }, Field { row: 2, column: 5, len: 2, secure: false }];
let e = c.accept(&fields, &[Some("XYZ"), None]).unwrap();
assert_eq!((e.texts, e.key), (vec!["Z ".to_owned(), "9 ".to_owned()], crate::terminal::aid_of("PF3").unwrap()));
}
}