//! `Host`-Abstraktion für den Textbildschirm. //! //! Der Host ist bewusst dumm: er zeigt den Zellenpuffer an und liefert //! Ereignisse. Alle Bildschirmwirkungen entstehen in der Laufzeitbibliothek //! auf [`TextScreen`] — ein Zeichenstrom könnte `LOCATE`, `COLOR`, `CSRLIN` //! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren //! (Entscheidung 2026-09-03, D2). //! //! Der Host besitzt Zeit und Warten; die VM liefert nur an kooperativen //! Zustellpunkten aus. use std::collections::VecDeque; 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) 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"; pub const BACKSPACE: &str = "\u{8}"; pub const ESC: &str = "\u{1b}"; pub const TAB: &str = "\t"; /// Sondertaste: Nullzeichen + Kennung (F1 = 59, F2 = 60 …). 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 { 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 { 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 { /// None means the frontend has scheduled a terminal handoff; retry the /// same request after it supplies the child result. fn shell(&mut self, command: &str) -> Result, crate::errors::RuntimeError> { shell_command(command) .status() .map(|s| Some(s.code().unwrap_or(0))) .map_err(|_| crate::errors::RuntimeError(53)) } /// Aktuellen Bildschirmzustand anzeigen. fn present(&mut self, screen: &TextScreen); /// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`, /// wenn nichts anliegt (Grundlage von `INKEY$`). fn next_event(&mut self, blockierend: bool) -> Option; /// Wartet auf Eingabe oder eine absolute Deadline in `jetzt_ms`-Zeit. /// `None` bedeutet: Deadline erreicht. Ohne Deadline wird bis zu einem /// Ereignis (gegebenenfalls explizitem Eingabeende) gewartet. Virtuelle /// Hosts steuern den Zeitfortschritt selbst; keine reale Rückfalluhr. fn warten(&mut self, deadline_ms: Option) -> Option; /// 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 /// vorbereitete Ereignisse ein. Grundlage des Korpus-Harness. #[derive(Default)] pub struct CaptureHost { /// Zuletzt angezeigter Bildschirm. pub screen: Option, /// Wie oft angezeigt wurde (Nachweis der Zustellpunkte). pub presents: usize, ereignisse: VecDeque, /// 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 { /// Host mit vorbereiteten Eingabezeilen: jede Zeile wird zu ihren /// Zeichen plus Enter. pub fn with_input(lines: &[&str]) -> Self { let mut h = CaptureHost::default(); for l in lines { h.tippe(l); } h } /// 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(), 0)); } self.ereignisse .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). pub fn ereignis(&mut self, e: Ereignis) { self.ereignisse.push_back(e); } /// Ereignis erst nach der `nach`-ten Anzeige einreihen. 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 { fn present(&mut self, screen: &TextScreen) { self.screen = Some(screen.clone()); self.presents += 1; let faellig = self.presents; let mut i = 0; while i < self.verzoegert.len() { if self.verzoegert[i].0 <= faellig { let (_, e) = self.verzoegert.remove(i); self.ereignisse.push_back(e); } else { i += 1; } } } fn next_event(&mut self, _blockierend: bool) -> Option { self.ereignisse.pop_front() } fn warten(&mut self, deadline_ms: Option) -> Option { if let Some(event) = self.next_event(false) { return Some(event); } match deadline_ms { Some(deadline) => { self.uhr_ms = self.uhr_ms.max(deadline); None } None => Some(Ereignis::Ende), } } fn jetzt_ms(&mut self) -> u64 { self.uhr_ms } } /// Shared child command, inheriting the foreground terminal and stdio. pub fn shell_command(command: &str) -> std::process::Command { let mut child = std::process::Command::new(if cfg!(windows) { "cmd" } else { "sh" }); if !command.is_empty() { child.args([if cfg!(windows) { "/C" } else { "-c" }, command]); } child } #[cfg(test)] mod tests { use super::*; #[test] fn capture_host_haelt_zeichen_und_attribut() { let mut h = CaptureHost::default(); let mut s = TextScreen::new(); s.locate(5, 10).unwrap(); s.set_color(14, 1); s.print("x"); h.present(&s); let gehalten = h.screen.as_ref().unwrap(); let z = gehalten.cell(5, 10); assert_eq!(z.ch, 'x'); assert_eq!((z.fg, z.bg), (14, 1)); assert_eq!(h.presents, 1); } #[test] fn verzoegertes_ereignis_kommt_nach_der_anzeige() { let mut h = CaptureHost::default(); 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 }) ); } #[test] fn groessenaenderung_und_abbruch_als_ereignis() { let mut h = CaptureHost::default(); 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 }) ); 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(), 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(), 0)) ); assert_eq!(h.next_event(false), None); } }