Files
TerminalBasic/crates/tb-runtime/src/host.rs

409 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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<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 {
/// 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<Option<i32>, 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<Ereignis>;
/// 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<u64>) -> 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
/// vorbereitete Ereignisse ein. Grundlage des Korpus-Harness.
#[derive(Default)]
pub struct CaptureHost {
/// Zuletzt angezeigter Bildschirm.
pub screen: Option<TextScreen>,
/// Wie oft angezeigt wurde (Nachweis der Zustellpunkte).
pub presents: usize,
ereignisse: VecDeque<Ereignis>,
/// 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<Ereignis> {
self.ereignisse.pop_front()
}
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
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);
}
}