Phase 4: Ereignisschleife und klassische Traps

This commit is contained in:
2026-09-04 15:56:47 +02:00
parent 060674b466
commit 5cf5a6582c
40 changed files with 4024 additions and 176 deletions

View File

@@ -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);
}