Skip to main content

ironwork_rt/
terminal.rs

1//! A 3270 display: its buffer of fields, the outbound data stream that writes it, the inbound Read
2//! Modified stream that reports what the operator changed, and a text rendering. The byte values
3//! are those of the 3270 Data Stream Programmer's Reference (GA23-0059).
4
5use zarch::ebcdic::CodePage;
6
7/// The task's terminal: SEND writes a 3270 data stream to it, and RECEIVE reads the stream the
8/// operator's next AID key sends back.
9pub trait Terminal: std::fmt::Debug {
10    fn size(&self) -> (usize, usize);
11    fn send(&mut self, stream: &[u8]) -> Result<(), String>;
12    /// None when the operator has nothing more to send.
13    fn receive(&mut self) -> Result<Option<Vec<u8>>, String>;
14}
15
16pub const WRITE: u8 = 0xF1;
17pub const ERASE_WRITE: u8 = 0xF5;
18pub const ERASE_WRITE_ALTERNATE: u8 = 0x7E;
19pub const ERASE_ALL_UNPROTECTED: u8 = 0x6F;
20
21pub const WCC_ALARM: u8 = 0x04;
22pub const WCC_RESTORE: u8 = 0x02;
23pub const WCC_RESET_MDT: u8 = 0x01;
24
25pub const SF: u8 = 0x1D;
26pub const SFE: u8 = 0x29;
27pub const SBA: u8 = 0x11;
28pub const SA: u8 = 0x28;
29pub const MF: u8 = 0x2C;
30pub const IC: u8 = 0x13;
31pub const PT: u8 = 0x05;
32pub const RA: u8 = 0x3C;
33pub const EUA: u8 = 0x12;
34
35pub const PROTECTED: u8 = 0x20;
36pub const NUMERIC: u8 = 0x10;
37pub const DISPLAY: u8 = 0x0C;
38pub const INTENSIFIED: u8 = 0x08;
39pub const NON_DISPLAY: u8 = 0x0C;
40pub const MDT: u8 = 0x01;
41
42pub const AID_ENTER: u8 = 0x7D;
43pub const AID_CLEAR: u8 = 0x6D;
44pub const AID_PA1: u8 = 0x6C;
45pub const AID_PA2: u8 = 0x6E;
46pub const AID_PA3: u8 = 0x6B;
47pub const AID_NONE: u8 = 0x60;
48
49/// The 6-bit code table: a 12-bit address's halves, and an attribute byte's graphic form.
50const CODES: [u8; 64] = [
51    0x40, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5,
52    0xD6, 0xD7, 0xD8, 0xD9, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0x6A, 0x6B,
53    0x6C, 0x6D, 0x6E, 0x6F, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
54];
55
56/// A buffer address as the data stream carries it: 12-bit coded below 4096, else 14-bit binary.
57pub fn encode_address(address: usize) -> [u8; 2] {
58    if address < 4096 {
59        [CODES[address >> 6], CODES[address & 0x3F]]
60    } else {
61        [((address >> 8) & 0x3F) as u8, (address & 0xFF) as u8]
62    }
63}
64
65pub fn decode_address(bytes: [u8; 2]) -> usize {
66    if bytes[0] & 0xC0 == 0 {
67        (usize::from(bytes[0] & 0x3F) << 8) | usize::from(bytes[1])
68    } else {
69        (usize::from(bytes[0] & 0x3F) << 6) | usize::from(bytes[1] & 0x3F)
70    }
71}
72
73/// An attribute's graphic form: its low six bits through the code table.
74pub fn attribute_byte(bits: u8) -> u8 {
75    CODES[usize::from(bits & 0x3F)]
76}
77
78/// A field on the screen: where its attribute byte is, the attribute, and where its data runs.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct FieldView {
81    pub attribute_at: usize,
82    pub attribute: u8,
83    pub start: usize,
84    pub len: usize,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct Screen {
89    pub rows: usize,
90    pub columns: usize,
91    data: Vec<u8>,
92    attributes: Vec<Option<u8>>,
93    pub cursor: usize,
94    pub alarm: bool,
95    pub keyboard_locked: bool,
96}
97
98impl Screen {
99    pub fn new(rows: usize, columns: usize) -> Self {
100        let size = rows * columns;
101        Self { rows, columns, data: vec![0; size], attributes: vec![None; size], cursor: 0, alarm: false, keyboard_locked: true }
102    }
103
104    pub fn size(&self) -> usize {
105        self.rows * self.columns
106    }
107
108    /// Row and column (both from 1) of a buffer address.
109    pub fn position(&self, address: usize) -> (usize, usize) {
110        (address / self.columns + 1, address % self.columns + 1)
111    }
112
113    pub fn address(&self, row: usize, column: usize) -> usize {
114        ((row - 1) * self.columns + column - 1) % self.size()
115    }
116
117    fn erase(&mut self) {
118        self.data.fill(0);
119        self.attributes.fill(None);
120        self.cursor = 0;
121    }
122
123    /// Every field, in buffer order; empty when the screen is unformatted.
124    pub fn fields(&self) -> Vec<FieldView> {
125        let size = self.size();
126        let starts: Vec<usize> = (0..size).filter(|&a| self.attributes[a].is_some()).collect();
127        starts
128            .iter()
129            .enumerate()
130            .map(|(i, &at)| {
131                let next = starts[(i + 1) % starts.len()];
132                let len = (next + size - at - 1) % size;
133                FieldView { attribute_at: at, attribute: self.attributes[at].unwrap_or(0), start: (at + 1) % size, len: if starts.len() == 1 { size - 1 } else { len } }
134            })
135            .collect()
136    }
137
138    /// The field a buffer address falls in.
139    pub fn field_at(&self, address: usize) -> Option<FieldView> {
140        let size = self.size();
141        self.fields().into_iter().find(|f| f.attribute_at == address || (address + size - f.start) % size < f.len)
142    }
143
144    pub fn field_data(&self, field: &FieldView) -> Vec<u8> {
145        (0..field.len).map(|i| self.data[(field.start + i) % self.size()]).collect()
146    }
147
148    fn set_attribute(&mut self, address: usize, attribute: u8) {
149        self.attributes[address] = Some(attribute);
150        self.data[address] = 0;
151    }
152
153    fn put(&mut self, address: &mut usize, byte: u8) {
154        self.attributes[*address] = None;
155        self.data[*address] = byte;
156        *address = (*address + 1) % self.size();
157    }
158
159    fn next_unprotected(&self, from: usize) -> usize {
160        let size = self.size();
161        (1..=size)
162            .map(|i| (from + i) % size)
163            .find(|&a| self.attributes[a].is_some_and(|attr| attr & PROTECTED == 0))
164            .map_or(0, |a| (a + 1) % size)
165    }
166
167    /// The Home key: the cursor to the first position of the first unprotected field, or to
168    /// address 0 when the screen has none.
169    pub fn home(&mut self) {
170        self.cursor = self.next_unprotected(self.size() - 1);
171    }
172
173    /// The Tab key: the cursor to the first position of the next unprotected field after it,
174    /// wrapping, or to address 0 when the screen has none.
175    pub fn tab(&mut self) {
176        self.cursor = self.next_unprotected(self.cursor);
177    }
178
179    /// Applies an outbound data stream: a command, its WCC, then orders and data.
180    pub fn apply(&mut self, stream: &[u8]) -> Result<(), String> {
181        let (&command, rest) = stream.split_first().ok_or("an empty data stream")?;
182        match command {
183            ERASE_WRITE | ERASE_WRITE_ALTERNATE | 0x05 | 0x0D => self.erase(),
184            WRITE | 0x01 => {}
185            ERASE_ALL_UNPROTECTED | 0x0F => {
186                let size = self.size();
187                for f in self.fields().iter().filter(|f| f.attribute & PROTECTED == 0) {
188                    for i in 0..f.len {
189                        self.data[(f.start + i) % size] = 0;
190                    }
191                    self.attributes[f.attribute_at] = Some(f.attribute & !MDT);
192                }
193                self.keyboard_locked = false;
194                return Ok(());
195            }
196            other => return Err(format!("3270 command X'{other:02X}' is not supported")),
197        }
198        let Some((&wcc, mut orders)) = rest.split_first() else { return Ok(()) };
199        if wcc & WCC_RESET_MDT != 0 {
200            for a in 0..self.size() {
201                if let Some(attr) = self.attributes[a] {
202                    self.attributes[a] = Some(attr & !MDT);
203                }
204            }
205        }
206        let mut at = self.cursor;
207        let size = self.size();
208        let address = |bytes: &[u8]| -> Result<usize, String> {
209            let [a, b] = bytes.get(..2).and_then(|s| s.try_into().ok()).ok_or("a buffer address cut short")?;
210            let address = decode_address([a, b]);
211            if address >= size { Err(format!("buffer address {address} is past the screen ({size})")) } else { Ok(address) }
212        };
213        while let Some((&byte, rest)) = orders.split_first() {
214            orders = rest;
215            match byte {
216                SBA => {
217                    at = address(orders)?;
218                    orders = &orders[2..];
219                }
220                SF => {
221                    let (&attr, rest) = orders.split_first().ok_or("SF without an attribute")?;
222                    self.set_attribute(at, attr);
223                    at = (at + 1) % size;
224                    orders = rest;
225                }
226                SFE => {
227                    let (&pairs, rest) = orders.split_first().ok_or("SFE without a count")?;
228                    let pairs = usize::from(pairs);
229                    let list = rest.get(..pairs * 2).ok_or("SFE cut short")?;
230                    let attr = list.chunks(2).find(|p| p[0] == 0xC0).map_or(0x40, |p| p[1]);
231                    self.set_attribute(at, attr);
232                    at = (at + 1) % size;
233                    orders = &rest[pairs * 2..];
234                }
235                SA => orders = orders.get(2..).ok_or("SA cut short")?,
236                MF => {
237                    let (&pairs, rest) = orders.split_first().ok_or("MF without a count")?;
238                    orders = rest.get(usize::from(pairs) * 2..).ok_or("MF cut short")?;
239                }
240                IC => self.cursor = at,
241                PT => at = self.next_unprotected(at),
242                RA => {
243                    let stop = address(orders)?;
244                    let fill = *orders.get(2).ok_or("RA without a character")?;
245                    orders = &orders[3..];
246                    loop {
247                        self.put(&mut at, fill);
248                        if at == stop {
249                            break;
250                        }
251                    }
252                }
253                EUA => {
254                    let stop = address(orders)?;
255                    orders = &orders[2..];
256                    while at != stop {
257                        if self.field_at(at).is_some_and(|f| f.attribute & PROTECTED == 0) && self.attributes[at].is_none() {
258                            self.data[at] = 0;
259                        }
260                        at = (at + 1) % size;
261                    }
262                }
263                data => self.put(&mut at, data),
264            }
265        }
266        self.alarm = wcc & WCC_ALARM != 0;
267        if wcc & WCC_RESTORE != 0 {
268            self.keyboard_locked = false;
269        }
270        Ok(())
271    }
272
273    /// The operator typing `text` (EBCDIC) at an address: it must be in an unprotected field, and
274    /// it sets the field's modified bit. Typing stops at the end of the field. An unformatted screen
275    /// takes typing anywhere, wrapping at its end.
276    pub fn type_at(&mut self, address: usize, text: &[u8]) -> Result<(), String> {
277        let size = self.size();
278        if self.fields().is_empty() {
279            for (i, &b) in text.iter().take(size).enumerate() {
280                self.data[(address + i) % size] = b;
281            }
282            self.cursor = (address + text.len().min(size)) % size;
283            return Ok(());
284        }
285        let field = self.field_at(address).ok_or("the screen has no fields")?;
286        if field.attribute & PROTECTED != 0 || field.attribute_at == address {
287            return Err(format!("row {} column {} is protected", self.position(address).0, self.position(address).1));
288        }
289        let offset = (address + size - field.start) % size;
290        for (i, &b) in text.iter().take(field.len - offset).enumerate() {
291            self.data[(address + i) % size] = b;
292        }
293        self.attributes[field.attribute_at] = Some(field.attribute | MDT);
294        self.cursor = (address + text.len().min(field.len - offset)) % size;
295        Ok(())
296    }
297
298    /// ERASE EOF: clears the rest of the field at an address and sets its modified bit.
299    pub fn erase_eof(&mut self, address: usize) -> Result<(), String> {
300        let field = self.field_at(address).ok_or("the screen has no fields")?;
301        if field.attribute & PROTECTED != 0 {
302            return Err("ERASE EOF in a protected field".into());
303        }
304        let size = self.size();
305        let offset = (address + size - field.start) % size;
306        for i in offset..field.len {
307            self.data[(field.start + i) % size] = 0;
308        }
309        self.attributes[field.attribute_at] = Some(field.attribute | MDT);
310        Ok(())
311    }
312
313    /// The inbound stream an AID key sends: Read Modified, or the short read of CLEAR and the PA
314    /// keys (the AID alone). Nulls in a field's data are not sent.
315    pub fn read_modified(&self, aid: u8) -> Vec<u8> {
316        if matches!(aid, AID_CLEAR | AID_PA1 | AID_PA2 | AID_PA3) {
317            return vec![aid];
318        }
319        let mut out = vec![aid];
320        out.extend(encode_address(self.cursor));
321        let fields = self.fields();
322        // An unformatted screen sends its whole buffer, nulls suppressed, with no buffer addresses.
323        if fields.is_empty() {
324            out.extend(self.data.iter().copied().filter(|&b| b != 0));
325            return out;
326        }
327        for f in fields.iter().filter(|f| f.attribute & MDT != 0) {
328            out.push(SBA);
329            out.extend(encode_address(f.start));
330            out.extend(self.field_data(f).into_iter().filter(|&b| b != 0));
331        }
332        out
333    }
334
335    /// The screen as text: attribute positions and nulls as spaces, non-display fields blank.
336    pub fn render(&self, page: &CodePage) -> String {
337        let mut shown: Vec<char> = self.data.iter().map(|&b| if b == 0 { ' ' } else { page.decode_byte(b) }).collect();
338        for f in self.fields() {
339            shown[f.attribute_at] = ' ';
340            if f.attribute & DISPLAY == NON_DISPLAY {
341                for i in 0..f.len {
342                    shown[(f.start + i) % self.size()] = ' ';
343                }
344            }
345        }
346        shown.chunks(self.columns).map(|row| row.iter().collect::<String>().trim_end().to_owned()).collect::<Vec<_>>().join("\n")
347    }
348}
349
350/// What an AID key sent: the key, the cursor (absent in a short read), and each modified field's
351/// data with the address it starts at.
352#[derive(Clone, Debug, PartialEq, Eq)]
353pub struct Inbound {
354    pub aid: u8,
355    pub cursor: Option<usize>,
356    pub fields: Vec<(usize, Vec<u8>)>,
357}
358
359pub fn parse_inbound(stream: &[u8]) -> Result<Inbound, String> {
360    let (&aid, rest) = stream.split_first().ok_or("an empty inbound stream")?;
361    if rest.is_empty() {
362        return Ok(Inbound { aid, cursor: None, fields: Vec::new() });
363    }
364    let cursor = decode_address(rest.get(..2).and_then(|s| s.try_into().ok()).ok_or("the cursor address is cut short")?);
365    let mut fields = Vec::new();
366    let mut i = 2;
367    while i < rest.len() {
368        if rest[i] != SBA {
369            return Err(format!("expected SBA at byte {}", i + 1));
370        }
371        let at = decode_address(rest.get(i + 1..i + 3).and_then(|s| s.try_into().ok()).ok_or("an SBA address is cut short")?);
372        let end = rest[i + 3..].iter().position(|&b| b == SBA).map_or(rest.len(), |p| i + 3 + p);
373        fields.push((at, rest[i + 3..end].to_vec()));
374        i = end;
375    }
376    Ok(Inbound { aid, cursor: Some(cursor), fields })
377}
378
379/// The AID byte a key sends: ENTER, CLEAR, PA1-PA3, PF1-PF24.
380pub fn aid_of(key: &str) -> Option<u8> {
381    let key = key.to_ascii_uppercase();
382    let pf = |n: u8| -> u8 {
383        match n {
384            1..=9 => 0xF0 + n,
385            10..=12 => 0x7A + n - 10,
386            13..=21 => 0xC1 + n - 13,
387            _ => 0x4A + n - 22,
388        }
389    };
390    Some(match key.as_str() {
391        "ENTER" => AID_ENTER,
392        "CLEAR" => AID_CLEAR,
393        "PA1" => AID_PA1,
394        "PA2" => AID_PA2,
395        "PA3" => AID_PA3,
396        k => match k.strip_prefix("PF").and_then(|n| n.parse::<u8>().ok()) {
397            Some(n @ 1..=24) => pf(n),
398            _ => return None,
399        },
400    })
401}
402
403/// One thing the operator does at the keyboard.
404#[derive(Clone, Debug, PartialEq, Eq)]
405pub enum Action {
406    Type { row: usize, column: usize, text: String },
407    EraseEof { row: usize, column: usize },
408    Cursor { row: usize, column: usize },
409    Home,
410    Tab,
411    /// Text typed at the cursor.
412    Text(String),
413    Key(u8),
414}
415
416/// A scripted operator: `type ROW COL text`, `eof ROW COL`, `cursor ROW COL`, the `home` and `tab`
417/// keys, `string text` typed at the cursor, and an AID key (`ENTER`, `CLEAR`, `PA1`..`PA3`,
418/// `PF1`..`PF24`) that ends each turn. `#` starts a comment.
419pub fn parse_script(text: &str) -> Result<Vec<Action>, String> {
420    let mut actions = Vec::new();
421    for (n, raw) in text.lines().enumerate() {
422        let line = raw.trim();
423        if line.is_empty() || line.starts_with('#') {
424            continue;
425        }
426        let bad = |why: &str| format!("screen script line {}: {why}: {raw}", n + 1);
427        let mut words = line.splitn(4, ' ');
428        let verb = words.next().unwrap_or("").to_ascii_lowercase();
429        let at = |words: &mut std::str::SplitN<'_, char>| -> Result<(usize, usize), String> {
430            let row = words.next().and_then(|w| w.parse().ok()).filter(|&r: &usize| r >= 1).ok_or_else(|| bad("expected a row from 1"))?;
431            let column = words.next().and_then(|w| w.parse().ok()).filter(|&c: &usize| c >= 1).ok_or_else(|| bad("expected a column from 1"))?;
432            Ok((row, column))
433        };
434        actions.push(match verb.as_str() {
435            "type" => {
436                let (row, column) = at(&mut words)?;
437                Action::Type { row, column, text: words.next().unwrap_or("").to_owned() }
438            }
439            "eof" => {
440                let (row, column) = at(&mut words)?;
441                Action::EraseEof { row, column }
442            }
443            "cursor" => {
444                let (row, column) = at(&mut words)?;
445                Action::Cursor { row, column }
446            }
447            "home" => Action::Home,
448            "tab" => Action::Tab,
449            "string" => Action::Text(line.split_once(' ').map_or("", |(_, text)| text).to_owned()),
450            key => Action::Key(aid_of(key).ok_or_else(|| bad("not type, eof, cursor or an AID key"))?),
451        });
452    }
453    Ok(actions)
454}
455
456/// A terminal driven by a screen script: each SEND is applied to the screen and its rendering kept
457/// in `shown`; each RECEIVE plays the script up to its next AID key.
458pub struct Scripted {
459    pub screen: Screen,
460    actions: std::collections::VecDeque<Action>,
461    page: &'static CodePage,
462    pub shown: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
463    /// What an AID key sent between two tasks, which the next task's RECEIVE reads.
464    pending: Option<Vec<u8>>,
465}
466
467impl std::fmt::Debug for Scripted {
468    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
469        write!(f, "Scripted({}x{}, {} actions left)", self.screen.rows, self.screen.columns, self.actions.len())
470    }
471}
472
473impl Scripted {
474    pub fn new(rows: usize, columns: usize, actions: Vec<Action>, page: &'static CodePage) -> Self {
475        Self { screen: Screen::new(rows, columns), actions: actions.into(), page, shown: Default::default(), pending: None }
476    }
477
478    /// Keeps what an AID key sent for the next RECEIVE, as CICS keeps the input that starts a task.
479    pub fn push_back(&mut self, record: Vec<u8>) {
480        self.pending = Some(record);
481    }
482
483    /// Drops the input that started a task when the task never read it: a task's terminal input
484    /// ends with the task.
485    pub fn discard_pending(&mut self) {
486        self.pending = None;
487    }
488}
489
490/// A scripted terminal the caller keeps between the tasks of a pseudo-conversation.
491#[derive(Clone, Debug)]
492pub struct SharedScript(pub std::rc::Rc<std::cell::RefCell<Scripted>>);
493
494impl Terminal for SharedScript {
495    fn size(&self) -> (usize, usize) {
496        self.0.borrow().size()
497    }
498
499    fn send(&mut self, stream: &[u8]) -> Result<(), String> {
500        self.0.borrow_mut().send(stream)
501    }
502
503    fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
504        self.0.borrow_mut().receive()
505    }
506}
507
508impl Terminal for Scripted {
509    fn size(&self) -> (usize, usize) {
510        (self.screen.rows, self.screen.columns)
511    }
512
513    fn send(&mut self, stream: &[u8]) -> Result<(), String> {
514        self.screen.apply(stream)?;
515        self.shown.borrow_mut().push(self.screen.render(self.page));
516        Ok(())
517    }
518
519    fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
520        if let Some(record) = self.pending.take() {
521            return Ok(Some(record));
522        }
523        while let Some(action) = self.actions.pop_front() {
524            match action {
525                Action::Type { row, column, text } => {
526                    let bytes = self.page.encode_lossy(&text);
527                    self.screen.type_at(self.screen.address(row, column), &bytes)?;
528                }
529                Action::EraseEof { row, column } => self.screen.erase_eof(self.screen.address(row, column))?,
530                Action::Cursor { row, column } => self.screen.cursor = self.screen.address(row, column),
531                Action::Home => self.screen.home(),
532                Action::Tab => self.screen.tab(),
533                Action::Text(text) => {
534                    let bytes = self.page.encode_lossy(&text);
535                    self.screen.type_at(self.screen.cursor, &bytes)?;
536                }
537                Action::Key(aid) => return Ok(Some(self.screen.read_modified(aid))),
538            }
539        }
540        Ok(None)
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    fn page() -> &'static CodePage {
549        CodePage::by_ccsid(37).unwrap()
550    }
551
552    #[test]
553    fn addresses_round_trip_in_both_encodings() {
554        for a in [0, 1, 79, 80, 1919, 4095] {
555            assert_eq!(decode_address(encode_address(a)), a);
556        }
557        assert_eq!(encode_address(80), [0xC1, 0x50]);
558        assert_eq!(decode_address(encode_address(5000)), 5000);
559        assert_eq!(encode_address(5000)[0] & 0xC0, 0);
560    }
561
562    #[test]
563    fn a_write_builds_fields_and_read_modified_returns_what_was_typed() {
564        let mut s = Screen::new(24, 80);
565        let mut stream = vec![ERASE_WRITE, WCC_RESTORE, SBA];
566        stream.extend(encode_address(0));
567        stream.extend([SF, attribute_byte(PROTECTED | INTENSIFIED)]);
568        stream.extend(page().encode("NAME:").unwrap());
569        stream.extend([SF, attribute_byte(0), IC]);
570        stream.extend([SBA]);
571        stream.extend(encode_address(20));
572        stream.extend([SF, attribute_byte(PROTECTED)]);
573        s.apply(&stream).unwrap();
574        assert_eq!(s.fields().len(), 3);
575        assert_eq!(s.cursor, 7);
576        assert!(!s.keyboard_locked);
577        assert!(s.type_at(1, b"x").is_err());
578        s.type_at(7, &page().encode("SMITH").unwrap()).unwrap();
579        assert_eq!(s.render(page()).lines().next().unwrap(), " NAME: SMITH");
580        let inbound = s.read_modified(AID_ENTER);
581        let read = parse_inbound(&inbound).unwrap();
582        assert_eq!((read.aid, read.cursor), (AID_ENTER, Some(12)));
583        assert_eq!(read.fields, vec![(7, page().encode("SMITH").unwrap())]);
584        assert_eq!(s.read_modified(AID_CLEAR), vec![AID_CLEAR]);
585    }
586
587    #[test]
588    fn an_unformatted_screen_takes_typing_anywhere_and_sends_its_whole_buffer() {
589        let mut s = Screen::new(24, 80);
590        s.type_at(0, &page().encode("SSC1 HELLO").unwrap()).unwrap();
591        let inbound = s.read_modified(AID_ENTER);
592        assert_eq!(inbound[0], AID_ENTER);
593        assert_eq!(&inbound[3..], page().encode("SSC1 HELLO").unwrap().as_slice());
594    }
595
596    #[test]
597    fn wcc_resets_modified_bits_and_eau_clears_unprotected_data() {
598        let mut s = Screen::new(24, 80);
599        let mut stream = vec![ERASE_WRITE, WCC_RESTORE, SF, attribute_byte(0)];
600        stream.extend(page().encode("AB").unwrap());
601        s.apply(&stream).unwrap();
602        s.type_at(1, &page().encode("Z").unwrap()).unwrap();
603        assert_eq!(s.fields()[0].attribute & MDT, MDT);
604        s.apply(&[WRITE, WCC_RESET_MDT]).unwrap();
605        assert_eq!(s.fields()[0].attribute & MDT, 0);
606        s.apply(&[ERASE_ALL_UNPROTECTED]).unwrap();
607        assert!(s.field_data(&s.fields()[0]).iter().all(|&b| b == 0));
608    }
609
610    #[test]
611    fn non_display_fields_render_blank_and_bad_streams_are_refused() {
612        let mut s = Screen::new(2, 10);
613        let mut stream = vec![ERASE_WRITE, 0, SF, attribute_byte(NON_DISPLAY)];
614        stream.extend(page().encode("SECRET").unwrap());
615        s.apply(&stream).unwrap();
616        assert_eq!(s.render(page()), "\n");
617        assert!(s.apply(&[0x99]).is_err());
618        assert!(s.apply(&[WRITE, 0, SBA, 0x7F]).is_err());
619    }
620
621    #[test]
622    fn aid_keys_and_screen_scripts() {
623        assert_eq!(aid_of("enter"), Some(0x7D));
624        assert_eq!(aid_of("PF1"), Some(0xF1));
625        assert_eq!(aid_of("PF12"), Some(0x7C));
626        assert_eq!(aid_of("PF13"), Some(0xC1));
627        assert_eq!(aid_of("PF24"), Some(0x4C));
628        assert_eq!(aid_of("PF25"), None);
629        let script = parse_script("# order entry\ntype 5 20 SMITH & CO\neof 6 20\ncursor 7 1\nENTER\nPF3\n").unwrap();
630        assert_eq!(script[0], Action::Type { row: 5, column: 20, text: "SMITH & CO".into() });
631        assert_eq!(script[1], Action::EraseEof { row: 6, column: 20 });
632        assert_eq!(script[3], Action::Key(0x7D));
633        assert_eq!(script[4], Action::Key(0xF3));
634        assert!(parse_script("type 0 1 X").is_err());
635        assert!(parse_script("jump 1 1").is_err());
636    }
637
638    #[test]
639    fn home_and_tab_reach_each_unprotected_field_and_string_types_at_the_cursor() {
640        let mut stream = vec![ERASE_WRITE, WCC_RESTORE, SF, attribute_byte(PROTECTED)];
641        stream.extend(page().encode("ID").unwrap());
642        stream.extend([SF, attribute_byte(0), 0, 0, 0, SF, attribute_byte(PROTECTED), SF, attribute_byte(0), 0, 0, SF, attribute_byte(PROTECTED)]);
643        let script = parse_script("home\nstring ABCD\ntab\nstring  Z\ntab\nstring Q\nENTER\n").unwrap();
644        assert_eq!(script[3], Action::Text(" Z".into()));
645        let mut t = Scripted::new(1, 20, script, page());
646        t.send(&stream).unwrap();
647        let read = parse_inbound(&t.receive().unwrap().unwrap()).unwrap();
648        let typed = |s: &str| page().encode(s).unwrap();
649        assert_eq!(read.fields, vec![(4, typed("QBC")), (9, typed(" Z"))]);
650        let mut blank = Screen::new(2, 10);
651        blank.cursor = 7;
652        blank.home();
653        assert_eq!(blank.cursor, 0);
654        blank.tab();
655        assert_eq!(blank.cursor, 0);
656    }
657}