Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -16,17 +16,56 @@ use crate::screen::TextScreen;
|
||||
/// Was vom Terminal hereinkommt.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ereignis {
|
||||
/// Tastendruck in `INKEY$`-Form: ein Zeichen für Zeichentasten,
|
||||
/// Nullzeichen + Kennung für Sondertasten.
|
||||
Taste(String),
|
||||
/// Tastendruck in `INKEY$`-Form (ein Zeichen für Zeichentasten,
|
||||
/// Nullzeichen + Kennung für Sondertasten) samt Umschaltzustand.
|
||||
///
|
||||
/// Der Umschaltzustand ist ein Bitfeld wie bei der Maus: 1 Umschalt,
|
||||
/// 2 Strg, 4 Alt. Er wird für `ON KEY(15)`–`ON KEY(25)` gebraucht —
|
||||
/// benutzerdefinierte Tasten sind über Umschaltzustand und Scancode
|
||||
/// erklärt, nicht über das erzeugte Zeichen.
|
||||
Taste(String, u8),
|
||||
/// Die Darstellungsfläche hat ihre Größe geändert.
|
||||
Groesse { cols: usize, rows: usize },
|
||||
/// Mausereignis in Zellenkoordinaten, 1-basiert wie `LOCATE`.
|
||||
Maus(MausEreignis),
|
||||
/// Betriebssystemsignal: 1 = SIGINT, 2 = SIGTERM (Sprachreferenz,
|
||||
/// Abweichung 8). Andere Signale werden nicht abgebildet.
|
||||
Signal(u8),
|
||||
/// Abbruchwunsch (Strg+Untbr).
|
||||
Abbruch,
|
||||
/// Eingabeende — es kommt nichts mehr.
|
||||
Ende,
|
||||
}
|
||||
|
||||
/// Was die Maus getan hat. Die Position ist 1-basiert und in Zellen des
|
||||
/// Textbildschirms — dieselbe Zählweise wie `LOCATE`, `CSRLIN` und `POS`;
|
||||
/// die Umrechnung aus der 0-basierten Zählung des Terminals macht die
|
||||
/// Darstellungsschicht (design.md, D7).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MausEreignis {
|
||||
pub art: MausArt,
|
||||
/// Gedrückte Taste: 1 links, 2 rechts, 4 Mitte; 0 bei reiner Bewegung.
|
||||
pub taste: u8,
|
||||
/// Umschaltzustand als Bitfeld: 1 Umschalt, 2 Strg, 4 Alt.
|
||||
pub shift: u8,
|
||||
pub zeile: usize,
|
||||
pub spalte: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MausArt {
|
||||
Druck,
|
||||
Loslassen,
|
||||
Bewegung,
|
||||
}
|
||||
|
||||
/// Umschaltzustand: Bitfeld 1 Umschalt, 2 Strg, 4 Alt.
|
||||
pub mod umschalt {
|
||||
pub const SHIFT: u8 = 1;
|
||||
pub const CTRL: u8 = 2;
|
||||
pub const ALT: u8 = 4;
|
||||
}
|
||||
|
||||
/// Steuerzeichen in `INKEY$`-Form (Kodierung des Vorbilds).
|
||||
pub mod taste {
|
||||
pub const ENTER: &str = "\r";
|
||||
@@ -38,6 +77,84 @@ pub mod taste {
|
||||
pub fn sonder(code: u8) -> String {
|
||||
format!("\0{}", code as char)
|
||||
}
|
||||
|
||||
/// Zeichen → Scancode der Tastatur (Tabelle aus `qb45advr.hlp`,
|
||||
/// „Keyboard Scan Codes"). Gebraucht für die benutzerdefinierten
|
||||
/// Trap-Tasten `KEY 15`–`KEY 25`, die über Scancode erklärt sind.
|
||||
///
|
||||
/// ponytail: nur die Tasten der Original-Tabelle, keine Ziffernblock-
|
||||
/// Doppelbelegungen. Ceiling: ein hier nicht geführtes Zeichen kann
|
||||
/// keinen benutzerdefinierten Trap auslösen; die Tabelle wächst, wenn
|
||||
/// jemand eine solche Taste braucht.
|
||||
pub fn scancode(c: char) -> Option<u8> {
|
||||
let c = c.to_ascii_uppercase();
|
||||
Some(match c {
|
||||
'\u{1b}' => 0x01,
|
||||
'1' | '!' => 0x02,
|
||||
'2' | '@' => 0x03,
|
||||
'3' | '#' => 0x04,
|
||||
'4' | '$' => 0x05,
|
||||
'5' | '%' => 0x06,
|
||||
'6' | '^' => 0x07,
|
||||
'7' | '&' => 0x08,
|
||||
'8' | '*' => 0x09,
|
||||
'9' | '(' => 0x0A,
|
||||
'0' | ')' => 0x0B,
|
||||
'-' | '_' => 0x0C,
|
||||
'=' | '+' => 0x0D,
|
||||
'\u{8}' => 0x0E,
|
||||
'\t' => 0x0F,
|
||||
'Q' => 0x10,
|
||||
'W' => 0x11,
|
||||
'E' => 0x12,
|
||||
'R' => 0x13,
|
||||
'T' => 0x14,
|
||||
'Y' => 0x15,
|
||||
'U' => 0x16,
|
||||
'I' => 0x17,
|
||||
'O' => 0x18,
|
||||
'P' => 0x19,
|
||||
'[' | '{' => 0x1A,
|
||||
']' | '}' => 0x1B,
|
||||
'\r' => 0x1C,
|
||||
'A' => 0x1E,
|
||||
'S' => 0x1F,
|
||||
'D' => 0x20,
|
||||
'F' => 0x21,
|
||||
'G' => 0x22,
|
||||
'H' => 0x23,
|
||||
'J' => 0x24,
|
||||
'K' => 0x25,
|
||||
'L' => 0x26,
|
||||
';' | ':' => 0x27,
|
||||
'\'' | '"' => 0x28,
|
||||
'`' | '~' => 0x29,
|
||||
'\\' | '|' => 0x2B,
|
||||
'Z' => 0x2C,
|
||||
'X' => 0x2D,
|
||||
'C' => 0x2E,
|
||||
'V' => 0x2F,
|
||||
'B' => 0x30,
|
||||
'N' => 0x31,
|
||||
'M' => 0x32,
|
||||
',' | '<' => 0x33,
|
||||
'.' | '>' => 0x34,
|
||||
'/' | '?' => 0x35,
|
||||
' ' => 0x39,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Scancode eines Tastendrucks in `INKEY$`-Form: bei Sondertasten die
|
||||
/// Kennung hinter dem Nullzeichen, sonst aus der Zeichentabelle.
|
||||
pub fn scancode_von(s: &str) -> Option<u8> {
|
||||
let mut it = s.chars();
|
||||
match (it.next(), it.next()) {
|
||||
(Some('\0'), Some(k)) => Some(k as u8),
|
||||
(Some(c), None) => scancode(c),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Host {
|
||||
@@ -47,6 +164,16 @@ pub trait Host {
|
||||
/// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`,
|
||||
/// wenn nichts anliegt (Grundlage von `INKEY$`).
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
|
||||
|
||||
/// Monotone Zeit in Millisekunden seit Programmstart.
|
||||
///
|
||||
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
|
||||
/// greift dafür **nicht** selbst auf die Uhr zu (design.md, D3). Nur so
|
||||
/// kann ein Host ohne Terminal die Zeit stellen und ein zeitgesteuertes
|
||||
/// Programm ohne Wartezeit und reproduzierbar prüfen. Von der Ortszeit
|
||||
/// hinter `TIME$`/`TIMER` ist das unabhängig — hier zählt Dauer, nicht
|
||||
/// Uhrzeit.
|
||||
fn jetzt_ms(&mut self) -> u64;
|
||||
}
|
||||
|
||||
/// Host ohne Terminal: hält den Bildschirmzustand fest und speist
|
||||
@@ -61,6 +188,9 @@ pub struct CaptureHost {
|
||||
/// Ereignisse, die erst nach der n-ten Anzeige eingereiht werden —
|
||||
/// so lässt sich eine Größenänderung mitten im Lauf nachstellen.
|
||||
verzoegert: Vec<(usize, Ereignis)>,
|
||||
/// Virtuelle Uhr in Millisekunden. Sie läuft nur, wenn der Test sie
|
||||
/// stellt — von allein vergeht hier keine Zeit.
|
||||
uhr_ms: u64,
|
||||
}
|
||||
|
||||
impl CaptureHost {
|
||||
@@ -77,10 +207,16 @@ impl CaptureHost {
|
||||
/// Text als Folge von Tastendrücken anhängen, abgeschlossen mit Enter.
|
||||
pub fn tippe(&mut self, text: &str) {
|
||||
for c in text.chars() {
|
||||
self.ereignisse.push_back(Ereignis::Taste(c.to_string()));
|
||||
self.ereignisse.push_back(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
self.ereignisse
|
||||
.push_back(Ereignis::Taste(taste::ENTER.to_string()));
|
||||
.push_back(Ereignis::Taste(taste::ENTER.to_string(), 0));
|
||||
}
|
||||
|
||||
/// Tastendruck mit Umschaltzustand einreihen (Strg+A, Alt+F1 …).
|
||||
pub fn taste_mit(&mut self, s: &str, shift: u8) {
|
||||
self.ereignisse
|
||||
.push_back(Ereignis::Taste(s.to_string(), shift));
|
||||
}
|
||||
|
||||
/// Beliebiges Ereignis einreihen (Größenänderung, Abbruch, Sondertaste).
|
||||
@@ -92,6 +228,16 @@ impl CaptureHost {
|
||||
pub fn ereignis_nach(&mut self, nach: usize, e: Ereignis) {
|
||||
self.verzoegert.push((nach, e));
|
||||
}
|
||||
|
||||
/// Virtuelle Uhr vorrücken.
|
||||
pub fn uhr_vorruecken(&mut self, ms: u64) {
|
||||
self.uhr_ms += ms;
|
||||
}
|
||||
|
||||
/// Virtuelle Uhr auf einen Wert setzen.
|
||||
pub fn uhr_setzen(&mut self, ms: u64) {
|
||||
self.uhr_ms = ms;
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for CaptureHost {
|
||||
@@ -113,6 +259,10 @@ impl Host for CaptureHost {
|
||||
fn next_event(&mut self, _blockierend: bool) -> Option<Ereignis> {
|
||||
self.ereignisse.pop_front()
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.uhr_ms
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -137,37 +287,84 @@ mod tests {
|
||||
#[test]
|
||||
fn verzoegertes_ereignis_kommt_nach_der_anzeige() {
|
||||
let mut h = CaptureHost::default();
|
||||
h.ereignis_nach(2, Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
h.ereignis_nach(
|
||||
2,
|
||||
Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
},
|
||||
);
|
||||
let s = TextScreen::new();
|
||||
h.present(&s);
|
||||
assert_eq!(h.next_event(false), None);
|
||||
h.present(&s);
|
||||
assert_eq!(
|
||||
h.next_event(false),
|
||||
Some(Ereignis::Groesse { cols: 120, rows: 40 })
|
||||
Some(Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groessenaenderung_und_abbruch_als_ereignis() {
|
||||
let mut h = CaptureHost::default();
|
||||
h.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
h.ereignis(Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
h.ereignis(Ereignis::Abbruch);
|
||||
assert_eq!(
|
||||
h.next_event(true),
|
||||
Some(Ereignis::Groesse { cols: 120, rows: 40 })
|
||||
Some(Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40
|
||||
})
|
||||
);
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Abbruch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maus_und_tasten_behalten_ihre_reihenfolge() {
|
||||
let mut h = CaptureHost::default();
|
||||
let klick = Ereignis::Maus(MausEreignis {
|
||||
art: MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 0,
|
||||
zeile: 3,
|
||||
spalte: 7,
|
||||
});
|
||||
h.ereignis(Ereignis::Taste("a".into(), 0));
|
||||
h.ereignis(klick.clone());
|
||||
h.ereignis(Ereignis::Taste("b".into(), 0));
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into(), 0)));
|
||||
assert_eq!(h.next_event(true), Some(klick));
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtuelle_uhr_laeuft_nur_wenn_gestellt() {
|
||||
let mut h = CaptureHost::default();
|
||||
assert_eq!(h.jetzt_ms(), 0);
|
||||
h.uhr_vorruecken(250);
|
||||
assert_eq!(h.jetzt_ms(), 250);
|
||||
h.uhr_vorruecken(250);
|
||||
assert_eq!(h.jetzt_ms(), 500);
|
||||
h.uhr_setzen(42);
|
||||
assert_eq!(h.jetzt_ms(), 42);
|
||||
// Ohne Zutun vergeht keine Zeit — das traegt die Reproduzierbarkeit.
|
||||
assert_eq!(h.jetzt_ms(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vorbereitete_zeile_wird_zu_tasten() {
|
||||
let mut h = CaptureHost::with_input(&["ab"]);
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into())));
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into())));
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into(), 0)));
|
||||
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0)));
|
||||
assert_eq!(
|
||||
h.next_event(true),
|
||||
Some(Ereignis::Taste(taste::ENTER.into()))
|
||||
Some(Ereignis::Taste(taste::ENTER.into(), 0))
|
||||
);
|
||||
assert_eq!(h.next_event(false), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user