Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
|
||||
use crate::console::PrintState;
|
||||
use crate::datetime as dt;
|
||||
use crate::finanz as fin;
|
||||
use crate::fileio as fio;
|
||||
use crate::value;
|
||||
use crate::errors::RuntimeError;
|
||||
use crate::fileio as fio;
|
||||
use crate::finanz as fin;
|
||||
use crate::format;
|
||||
use crate::host::{Ereignis, Host};
|
||||
use crate::screen::TextScreen;
|
||||
use crate::value;
|
||||
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
@@ -28,8 +28,11 @@ pub struct RtState {
|
||||
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
|
||||
pub isam: crate::isam::Isam,
|
||||
pub print: PrintState,
|
||||
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form.
|
||||
pub tasten: VecDeque<String>,
|
||||
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
|
||||
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand
|
||||
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
|
||||
/// `INKEY$` und `INPUT` sehen ihn nicht.
|
||||
pub tasten: VecDeque<(String, u8)>,
|
||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||
pub abbruch: bool,
|
||||
/// Eingabeende wurde gemeldet.
|
||||
@@ -42,6 +45,8 @@ pub struct RtState {
|
||||
pub waehrung: crate::using::Waehrung,
|
||||
/// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet?
|
||||
pub trace: bool,
|
||||
/// Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
|
||||
pub traps: crate::traps::Traps,
|
||||
/// Trefferliste und Zeiger für `DIR$`.
|
||||
pub dir_treffer: Vec<String>,
|
||||
pub dir_index: usize,
|
||||
@@ -73,6 +78,7 @@ impl Default for RtState {
|
||||
key_zeile: false,
|
||||
waehrung: Default::default(),
|
||||
trace: false,
|
||||
traps: crate::traps::Traps::neu(),
|
||||
dir_treffer: Vec::new(),
|
||||
dir_index: 0,
|
||||
uhr_offset: 0,
|
||||
@@ -100,10 +106,19 @@ impl RtState {
|
||||
return;
|
||||
};
|
||||
match e {
|
||||
Ereignis::Taste(t) => self.tasten.push_back(t),
|
||||
Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)),
|
||||
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
|
||||
Ereignis::Abbruch => self.abbruch = true,
|
||||
Ereignis::Ende => self.ende = true,
|
||||
Ereignis::Signal(n) => {
|
||||
if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) {
|
||||
self.abbruch = true;
|
||||
}
|
||||
}
|
||||
// ponytail: Mausereignisse bekommen ihren Verbraucher erst
|
||||
// mit dem Forms-Objektmodell. Bis dahin werden sie hier
|
||||
// verworfen, damit die Warteschlange nicht wächst.
|
||||
Ereignis::Maus(_) => {}
|
||||
}
|
||||
if blockierend {
|
||||
return;
|
||||
@@ -116,12 +131,46 @@ impl RtState {
|
||||
pub fn tick(&mut self, host: &mut dyn Host) {
|
||||
host.present(&self.screen);
|
||||
self.pump(host, false);
|
||||
self.traps_pruefen(host);
|
||||
}
|
||||
|
||||
/// Was am Zustellpunkt vor der eigentlichen Zustellung zu tun ist:
|
||||
/// Zeit prüfen und getrappte Tasten aus der Schlange nehmen.
|
||||
///
|
||||
/// Die Tasten werden **hier** aussortiert, nicht beim Einsortieren
|
||||
/// (design.md, D5): so wirkt ein `KEY(n) OFF` sofort und auch auf
|
||||
/// Tasten, die schon in der Schlange liegen.
|
||||
pub fn traps_pruefen(&mut self, host: &mut dyn Host) {
|
||||
if !self.traps.aktiv() {
|
||||
return;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.traps.zeit_pruefen(jetzt);
|
||||
self.tasten_traps_pruefen();
|
||||
}
|
||||
|
||||
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
|
||||
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
|
||||
pub fn tasten_traps_pruefen(&mut self) {
|
||||
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt
|
||||
// erhalten, und eine getrappte Taste hinter einer ungetrappten
|
||||
// wartet, bis diese gelesen ist.
|
||||
while let Some((s, shift)) = self.tasten.front() {
|
||||
let Some(code) = crate::host::taste::scancode_von(s) else {
|
||||
break;
|
||||
};
|
||||
let Some(n) = self.traps.taste_faengt(code, *shift) else {
|
||||
break;
|
||||
};
|
||||
self.tasten.pop_front();
|
||||
self.traps.melden(crate::traps::Quelle::Key(n));
|
||||
}
|
||||
}
|
||||
|
||||
/// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`.
|
||||
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
|
||||
loop {
|
||||
if let Some(t) = self.tasten.pop_front() {
|
||||
if let Some((t, _)) = self.tasten.pop_front() {
|
||||
return Some(t);
|
||||
}
|
||||
if self.ende {
|
||||
@@ -386,7 +435,9 @@ pub mod ids {
|
||||
pub const ISAM_SAVEPOINT: u16 = 145;
|
||||
pub const ISAM_SETMEM: u16 = 146;
|
||||
pub const ISAM_BOF: u16 = 147;
|
||||
pub const COUNT: u16 = 148;
|
||||
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
|
||||
pub const SETUEVENT: u16 = 148;
|
||||
pub const COUNT: u16 = 149;
|
||||
}
|
||||
|
||||
/// Dispatch-Tabelle in Index-Reihenfolge.
|
||||
@@ -541,6 +592,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
bi_isam_savepoint,
|
||||
bi_isam_setmem,
|
||||
bi_isam_bof,
|
||||
bi_setuevent,
|
||||
];
|
||||
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
|
||||
TABLE
|
||||
@@ -1439,9 +1491,26 @@ fn key_index(n: i32) -> Option<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
fn bi_key_assign(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
fn bi_key_assign(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let n = arg_i32(a, 0)?;
|
||||
let text = arg_str(a, 1)?;
|
||||
// 15–25 weisen kein Makro zu, sondern erklären eine Trap-Taste über
|
||||
// Tastaturflagbyte und Scancode (Sprachreferenz §8).
|
||||
if (15..=25).contains(&n) {
|
||||
let mut it = text.chars();
|
||||
let (Some(flag), Some(scan)) = (it.next(), it.next()) else {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
};
|
||||
if it.next().is_some() || flag as u32 > 255 || scan as u32 > 255 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
st.traps.benutzertaste(n as u8, flag as u8, scan as u8);
|
||||
return Ok(None);
|
||||
}
|
||||
let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
// Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild).
|
||||
st.key_makros[i] = text.chars().take(15).collect();
|
||||
@@ -1651,7 +1720,21 @@ fn bi_sleep(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Op
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_beep(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
|
||||
fn bi_setuevent(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
// „sets a flag that BASIC checks before executing the next statement"
|
||||
st.traps.melden(crate::traps::Quelle::UEvent);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_beep(
|
||||
_: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
// ponytail: Der Signalton geht direkt an das Terminal, nicht durch den
|
||||
// Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host
|
||||
// mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf.
|
||||
@@ -1885,4 +1968,155 @@ mod tests {
|
||||
};
|
||||
assert_eq!(n, 3);
|
||||
}
|
||||
#[test]
|
||||
fn setuevent_meldet_das_benutzerereignis() {
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
st.traps.definieren(Quelle::UEvent, Some(11));
|
||||
st.traps.setzen(Quelle::UEvent, Zustand::An, 0);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
bi_setuevent(&mut st, &mut h, &mut []).unwrap();
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::UEvent, 11)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_aus_dem_host_meldet_den_trap() {
|
||||
use crate::host::{Ereignis, Host};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Signal(1), Some(22));
|
||||
st.traps.setzen(Quelle::Signal(1), Zustand::An, 0);
|
||||
h.ereignis(Ereignis::Signal(1));
|
||||
st.pump(&mut h, false);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22)));
|
||||
assert!(matches!(h.next_event(false), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
|
||||
use crate::host::Ereignis;
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
h.ereignis(Ereignis::Signal(1));
|
||||
st.pump(&mut h, false);
|
||||
assert!(st.abbruch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mausereignisse_haeufen_sich_nicht_an() {
|
||||
use crate::host::{Ereignis, MausArt, MausEreignis};
|
||||
let mut st = RtState::default();
|
||||
let mut h = crate::host::CaptureHost::default();
|
||||
for i in 0..500 {
|
||||
h.ereignis(Ereignis::Maus(MausEreignis {
|
||||
art: MausArt::Bewegung,
|
||||
taste: 0,
|
||||
shift: 0,
|
||||
zeile: 1 + i % 20,
|
||||
spalte: 1 + i % 70,
|
||||
}));
|
||||
}
|
||||
st.pump(&mut h, false);
|
||||
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getrappte_taste_verlaesst_den_eingabestrom() {
|
||||
use crate::host::{taste, CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(1), Some(77));
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); // F1
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(1), 77)));
|
||||
assert_eq!(st.naechste_taste(&mut h, false), None, "F1 ist weg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nicht_getrappte_taste_bleibt_im_strom() {
|
||||
use crate::host::{taste, CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(1), Some(77));
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
|
||||
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
assert_eq!(
|
||||
st.naechste_taste(&mut h, false),
|
||||
Some(taste::sonder(0x3B)),
|
||||
"bei OFF bleibt F1 lesbar"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_wirkt_auf_bereits_wartende_tasten() {
|
||||
use crate::host::{taste, CaptureHost, Ereignis};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(1), Some(77));
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
|
||||
st.pump(&mut h, false); // Taste liegt in der Schlange
|
||||
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(
|
||||
st.naechste_taste(&mut h, false),
|
||||
Some(taste::sonder(0x3B)),
|
||||
"das OFF wirkt auch auf schon Wartendes (design.md D5)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benutzertaste_ueber_key_anweisung() {
|
||||
use crate::host::{umschalt, CaptureHost};
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(15), Some(88));
|
||||
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
|
||||
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
|
||||
bi_key_assign(
|
||||
&mut st,
|
||||
&mut h,
|
||||
&mut [Value::Int(15), Value::Str(def.into())],
|
||||
)
|
||||
.unwrap();
|
||||
h.taste_mit("a", umschalt::CTRL);
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 88)));
|
||||
assert_eq!(st.naechste_taste(&mut h, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benutzertaste_ohne_umschalt_bleibt_im_strom() {
|
||||
use crate::host::CaptureHost;
|
||||
use crate::traps::{Quelle, Zustand};
|
||||
let mut st = RtState::default();
|
||||
let mut h = CaptureHost::default();
|
||||
st.traps.definieren(Quelle::Key(15), Some(88));
|
||||
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
|
||||
bi_key_assign(
|
||||
&mut st,
|
||||
&mut h,
|
||||
&mut [Value::Int(15), Value::Str(def.into())],
|
||||
)
|
||||
.unwrap();
|
||||
h.taste_mit("a", 0); // ohne Strg
|
||||
st.pump(&mut h, false);
|
||||
st.traps_pruefen(&mut h);
|
||||
assert_eq!(st.traps.naechstes(), None);
|
||||
assert_eq!(st.naechste_taste(&mut h, false), Some("a".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
//! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive
|
||||
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
|
||||
|
||||
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
|
||||
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
|
||||
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
|
||||
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
|
||||
pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb)
|
||||
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
|
||||
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
|
||||
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
|
||||
pub mod datetime; // DATE$, TIME$, TIMER
|
||||
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
|
||||
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
|
||||
pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …)
|
||||
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
|
||||
pub mod host;
|
||||
pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb)
|
||||
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
|
||||
pub mod screen;
|
||||
pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …)
|
||||
pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten
|
||||
pub mod snapshot; // Bildschirmzustand als vergleichbarer Text (Korpus) // Zellenpuffer des Textbildschirms (ohne Terminalbezug) // Host-Trait (Konsole, Capture) — Entscheidung D3
|
||||
pub mod datetime; // DATE$, TIME$, TIMER
|
||||
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
|
||||
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
|
||||
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix
|
||||
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
|
||||
pub mod traps; // Zustaende der klassischen Ereignis-Traps (Sprachreferenz §8)
|
||||
pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten
|
||||
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix
|
||||
|
||||
520
crates/tb-runtime/src/traps.rs
Normal file
520
crates/tb-runtime/src/traps.rs
Normal file
@@ -0,0 +1,520 @@
|
||||
//! Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
|
||||
//!
|
||||
//! Die Fachlogik liegt hier, ohne VM und ohne Host: welche Quelle in
|
||||
//! welchem Zustand ist, was ansteht und was als Nächstes zuzustellen wäre.
|
||||
//! Wer das Sprungziel anspringt, entscheidet die VM.
|
||||
//!
|
||||
//! Belege und Festlegungen: `umfang-und-form.md` des Changes
|
||||
//! `phase-4-ereignisschleife`.
|
||||
|
||||
/// Eine Ereignisquelle. Die Reihenfolge der Varianten ist zugleich die
|
||||
/// Zustellreihenfolge bei gleichzeitiger Fälligkeit (Festlegung, weil das
|
||||
/// Vorbild dazu nichts hergibt): Tasten zuerst, weil sie an einer
|
||||
/// Benutzereingabe hängen.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Quelle {
|
||||
/// `ON KEY(n)` — n aus 0, 1–25, 30–31.
|
||||
Key(u8),
|
||||
/// `ON TIMER(n)`.
|
||||
Timer,
|
||||
/// `ON UEVENT`.
|
||||
UEvent,
|
||||
/// `ON SIGNAL(n)` — n aus 1–2.
|
||||
Signal(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Zustand {
|
||||
/// Zustellen.
|
||||
An,
|
||||
/// Verwerfen — auch das bereits Gemerkte.
|
||||
#[default]
|
||||
Aus,
|
||||
/// Höchstens eines merken und beim nächsten `ON` zustellen.
|
||||
Gestoppt,
|
||||
}
|
||||
|
||||
/// Ein definierter Trap: Ziel, Zustand, ob etwas ansteht.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Trap {
|
||||
quelle: Quelle,
|
||||
/// Sprungziel im Modulrumpf. `None` = `GOSUB 0`, also abgeschaltet.
|
||||
ziel: Option<u32>,
|
||||
zustand: Zustand,
|
||||
/// Höchstens ein gemerktes Ereignis (Vorbild: „remembered").
|
||||
anstehend: bool,
|
||||
/// Läuft der Handler dieser Quelle gerade?
|
||||
laeuft: bool,
|
||||
/// Wurde im laufenden Handler ausdrücklich `OFF` ausgeführt? Dann
|
||||
/// unterbleibt das selbsttätige `ON` beim `RETURN`.
|
||||
off_im_handler: bool,
|
||||
}
|
||||
|
||||
/// Alle Traps eines Moduls.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Traps {
|
||||
traps: Vec<Trap>,
|
||||
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
|
||||
/// dritter Zustand — den kennt das Vorbild nicht.
|
||||
event_an: bool,
|
||||
/// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
|
||||
/// das nächste Mal fällig wird (beides in Millisekunden).
|
||||
timer_intervall_ms: u64,
|
||||
timer_faellig_ms: u64,
|
||||
/// Benutzerdefinierte Tasten 15–25: Umschaltzustand und Scancode aus
|
||||
/// `KEY n, CHR$(flag) + CHR$(scancode)`.
|
||||
benutzertasten: [Option<(u8, u8)>; 11],
|
||||
/// Gibt es überhaupt einen nicht abgeschalteten Trap? Gepflegt statt
|
||||
/// gerechnet: die VM fragt das an jeder Anweisungsgrenze (design.md, D4).
|
||||
aktiv: bool,
|
||||
}
|
||||
|
||||
impl Traps {
|
||||
pub fn neu() -> Self {
|
||||
Traps {
|
||||
event_an: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn finde(&mut self, q: Quelle) -> Option<&mut Trap> {
|
||||
self.traps.iter_mut().find(|t| t.quelle == q)
|
||||
}
|
||||
|
||||
/// `ON <quelle> GOSUB ziel`. `ziel = None` schaltet den Trap ab
|
||||
/// (`GOSUB 0`). Der Zustand bleibt, was er war — das Einschalten ist
|
||||
/// Sache der Steueranweisung.
|
||||
pub fn definieren(&mut self, q: Quelle, ziel: Option<u32>) {
|
||||
if let Some(t) = self.finde(q) {
|
||||
t.ziel = ziel;
|
||||
if ziel.is_none() {
|
||||
t.anstehend = false;
|
||||
}
|
||||
self.aktiv = self.traps.iter().any(|t| t.ziel.is_some());
|
||||
return;
|
||||
}
|
||||
self.aktiv = self.aktiv || ziel.is_some();
|
||||
self.traps.push(Trap {
|
||||
quelle: q,
|
||||
ziel,
|
||||
zustand: Zustand::Aus,
|
||||
anstehend: false,
|
||||
laeuft: false,
|
||||
off_im_handler: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Intervall des Zeit-Traps setzen (Sekunden, 1–86 400) und die erste
|
||||
/// Fälligkeit von `jetzt_ms` aus rechnen.
|
||||
pub fn timer_intervall(&mut self, sekunden: u64, jetzt_ms: u64) {
|
||||
self.timer_intervall_ms = sekunden.saturating_mul(1000);
|
||||
self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms);
|
||||
}
|
||||
|
||||
/// Steueranweisung `<quelle> ON|OFF|STOP`. Ohne vorangehende
|
||||
/// Trap-Definition bleibt sie wirkungslos, aber fehlerfrei.
|
||||
pub fn setzen(&mut self, q: Quelle, z: Zustand, jetzt_ms: u64) {
|
||||
let neu_faellig = matches!(q, Quelle::Timer) && z == Zustand::An;
|
||||
let intervall = self.timer_intervall_ms;
|
||||
if let Some(t) = self.finde(q) {
|
||||
if z == Zustand::Aus {
|
||||
// „OFF … Events occurring while trapping is off are
|
||||
// ignored" und verwirft auch das unter STOP Gemerkte.
|
||||
t.anstehend = false;
|
||||
if t.laeuft {
|
||||
t.off_im_handler = true;
|
||||
}
|
||||
}
|
||||
t.zustand = z;
|
||||
}
|
||||
// Ein neu eingeschalteter Zeit-Trap zählt ab jetzt.
|
||||
if neu_faellig && intervall > 0 {
|
||||
self.timer_faellig_ms = jetzt_ms.saturating_add(intervall);
|
||||
}
|
||||
}
|
||||
|
||||
/// `EVENT ON` / `EVENT OFF`.
|
||||
pub fn event(&mut self, an: bool) {
|
||||
if !an {
|
||||
for trap in &mut self.traps {
|
||||
trap.anstehend = false;
|
||||
}
|
||||
}
|
||||
self.event_an = an;
|
||||
}
|
||||
|
||||
/// Ein Ereignis ist eingetreten. Es wird nur gemerkt, wenn die Quelle
|
||||
/// einen Trap hat und nicht auf `OFF` steht.
|
||||
pub fn melden(&mut self, q: Quelle) -> bool {
|
||||
if !self.event_an {
|
||||
return false;
|
||||
}
|
||||
if let Some(t) = self.finde(q) {
|
||||
if t.ziel.is_some() && t.zustand != Zustand::Aus {
|
||||
t.anstehend = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Zeit prüfen und den Zeit-Trap melden, wenn die Frist verstrichen ist.
|
||||
/// Übersprungene Fristen zählen als eine — das Vorbild merkt sich nur
|
||||
/// eines.
|
||||
pub fn zeit_pruefen(&mut self, jetzt_ms: u64) {
|
||||
if self.timer_intervall_ms == 0 || jetzt_ms < self.timer_faellig_ms {
|
||||
return;
|
||||
}
|
||||
// Nächste Fälligkeit vom jetzigen Zeitpunkt aus, nicht kumulativ:
|
||||
// sonst holt ein langer Handler die verpassten Fristen nach.
|
||||
self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms);
|
||||
self.melden(Quelle::Timer);
|
||||
}
|
||||
|
||||
/// Gibt es einen definierten, nicht abgeschalteten Trap? Solange nicht,
|
||||
/// kostet die Prüfung an der Anweisungsgrenze nichts (design.md, D4).
|
||||
#[inline]
|
||||
pub fn aktiv(&self) -> bool {
|
||||
self.aktiv
|
||||
}
|
||||
|
||||
/// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
|
||||
/// n = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds.
|
||||
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
|
||||
if (15..=25).contains(&n) {
|
||||
self.benutzertasten[n as usize - 15] = Some((flag_zu_umschalt(flag), scancode));
|
||||
}
|
||||
}
|
||||
|
||||
/// Welcher `KEY`-Trap fängt diesen Tastendruck? Geprüft wird gegen den
|
||||
/// Scancode und — bei den benutzerdefinierten Tasten — zusätzlich gegen
|
||||
/// den Umschaltzustand. `KEY(0)` fängt alle Tasten der Tabelle.
|
||||
///
|
||||
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
|
||||
/// wissen, welche Quelle er melden soll.
|
||||
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
|
||||
if !self.event_an {
|
||||
return None;
|
||||
}
|
||||
let treffer = |k: u8| -> bool {
|
||||
match k {
|
||||
0 => vordefinierte_taste(scancode).is_some(),
|
||||
1..=14 | 30 | 31 => vordefinierte_taste(scancode) == Some(k),
|
||||
15..=25 => self.benutzertasten[k as usize - 15] == Some((shift, scancode)),
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
self.traps
|
||||
.iter()
|
||||
.filter(|t| t.ziel.is_some() && t.zustand != Zustand::Aus && !t.laeuft)
|
||||
.filter_map(|t| match t.quelle {
|
||||
Quelle::Key(k) if treffer(k) => Some(k),
|
||||
_ => None,
|
||||
})
|
||||
// Ein genauer Trap geht dem Sammeltrap `KEY(0)` vor.
|
||||
.min_by_key(|&k| if k == 0 { u8::MAX } else { k })
|
||||
}
|
||||
|
||||
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
|
||||
/// höchstens eines je Aufruf; die übrigen bleiben anstehend.
|
||||
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
|
||||
if !self.event_an {
|
||||
return None;
|
||||
}
|
||||
let mut kandidaten: Vec<usize> = (0..self.traps.len())
|
||||
.filter(|&i| {
|
||||
let t = &self.traps[i];
|
||||
t.anstehend && t.zustand == Zustand::An && !t.laeuft && t.ziel.is_some()
|
||||
})
|
||||
.collect();
|
||||
kandidaten.sort_by_key(|&i| self.traps[i].quelle);
|
||||
let i = *kandidaten.first()?;
|
||||
let t = &mut self.traps[i];
|
||||
t.anstehend = false;
|
||||
// „an automatic TIMER STOP is executed so that recursive traps
|
||||
// cannot take place" — gilt für jede Quelle.
|
||||
t.laeuft = true;
|
||||
t.off_im_handler = false;
|
||||
Some((t.quelle, t.ziel.unwrap()))
|
||||
}
|
||||
|
||||
/// `RETURN` aus dem Handler: „automatically performs a TIMER ON
|
||||
/// statement unless an explicit TIMER OFF was performed inside".
|
||||
pub fn handler_beendet(&mut self, q: Quelle) {
|
||||
if let Some(t) = self.finde(q) {
|
||||
t.laeuft = false;
|
||||
if t.off_im_handler {
|
||||
t.zustand = Zustand::Aus;
|
||||
t.anstehend = false;
|
||||
} else {
|
||||
t.zustand = Zustand::An;
|
||||
}
|
||||
t.off_im_handler = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tastaturflagbyte des Vorbilds → unser Bitfeld (1 Umschalt, 2 Strg,
|
||||
/// 4 Alt). Links und rechts sind im Terminal nicht unterscheidbar, beide
|
||||
/// Umschaltbits gelten deshalb gleich; NumLock und CapsLock spielen für
|
||||
/// einen Trap keine Rolle (Festlegung, siehe Sprachreferenz §8).
|
||||
fn flag_zu_umschalt(flag: u8) -> u8 {
|
||||
let mut u = 0;
|
||||
if flag & 0b11 != 0 {
|
||||
u |= 1;
|
||||
}
|
||||
if flag & 0b100 != 0 {
|
||||
u |= 2;
|
||||
}
|
||||
if flag & 0b1000 != 0 {
|
||||
u |= 4;
|
||||
}
|
||||
u
|
||||
}
|
||||
|
||||
/// Scancode → Nummer des vordefinierten Traps (`ON KEY(1)`–`ON KEY(14)`,
|
||||
/// `ON KEY(30)`/`ON KEY(31)`), Tabelle aus der Original-Hilfe.
|
||||
fn vordefinierte_taste(scancode: u8) -> Option<u8> {
|
||||
Some(match scancode {
|
||||
0x3B..=0x44 => scancode - 0x3A, // F1–F10 → 1–10
|
||||
0x48 => 11, // Pfeil hoch
|
||||
0x4B => 12, // Pfeil links
|
||||
0x4D => 13, // Pfeil rechts
|
||||
0x50 => 14, // Pfeil runter
|
||||
0x85 => 30, // F11
|
||||
0x86 => 31, // F12
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn mit_timer() -> Traps {
|
||||
let mut t = Traps::neu();
|
||||
t.definieren(Quelle::Timer, Some(100));
|
||||
t.timer_intervall(1, 0);
|
||||
t
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_trap_kostet_nichts() {
|
||||
let mut t = Traps::neu();
|
||||
assert!(!t.aktiv());
|
||||
assert_eq!(t.naechstes(), None);
|
||||
t.definieren(Quelle::Timer, Some(7));
|
||||
assert!(t.aktiv());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aus_verwirft_auch_das_gemerkte() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::Gestoppt, 0);
|
||||
t.zeit_pruefen(1000);
|
||||
t.setzen(Quelle::Timer, Zustand::Aus, 1000);
|
||||
t.setzen(Quelle::Timer, Zustand::An, 1000);
|
||||
assert_eq!(t.naechstes(), None, "OFF verwirft das unter STOP Gemerkte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gestoppt_merkt_genau_eines() {
|
||||
let mut t = Traps::neu();
|
||||
t.definieren(Quelle::Key(1), Some(42));
|
||||
t.setzen(Quelle::Key(1), Zustand::Gestoppt, 0);
|
||||
t.melden(Quelle::Key(1));
|
||||
t.melden(Quelle::Key(1));
|
||||
t.melden(Quelle::Key(1));
|
||||
t.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Key(1), 42)));
|
||||
t.handler_beendet(Quelle::Key(1));
|
||||
assert_eq!(t.naechstes(), None, "gemerkt wird hoechstens eines");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aus_verwirft_kuenftige() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::Aus, 0);
|
||||
t.zeit_pruefen(5000);
|
||||
t.setzen(Quelle::Timer, Zustand::An, 5000);
|
||||
assert_eq!(t.naechstes(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeit_wird_erst_nach_der_frist_faellig() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.zeit_pruefen(999);
|
||||
assert_eq!(t.naechstes(), None);
|
||||
t.zeit_pruefen(1000);
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uebersprungene_fristen_zaehlen_als_eine() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.zeit_pruefen(10_000); // zehn Fristen auf einmal
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||
t.handler_beendet(Quelle::Timer);
|
||||
t.zeit_pruefen(10_000);
|
||||
assert_eq!(t.naechstes(), None, "nicht nachgeholt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kein_wiedereintritt_waehrend_der_handler_laeuft() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.zeit_pruefen(1000);
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||
t.zeit_pruefen(2000);
|
||||
assert_eq!(t.naechstes(), None, "kein Wiedereintritt");
|
||||
t.handler_beendet(Quelle::Timer);
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)), "danach einmal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_im_handler_ueberlebt_das_return() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.zeit_pruefen(1000);
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||
t.setzen(Quelle::Timer, Zustand::Aus, 1000); // OFF im Handler
|
||||
t.handler_beendet(Quelle::Timer);
|
||||
t.zeit_pruefen(9000);
|
||||
assert_eq!(t.naechstes(), None, "RETURN schaltet nicht wieder ein");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_schaltet_sonst_selbsttaetig_ein() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.zeit_pruefen(1000);
|
||||
t.naechstes().unwrap();
|
||||
t.handler_beendet(Quelle::Timer);
|
||||
t.zeit_pruefen(2000);
|
||||
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_off_verwirft_ereignisse_und_laesst_die_zustaende() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.event(false);
|
||||
t.zeit_pruefen(1000);
|
||||
assert_eq!(t.naechstes(), None);
|
||||
t.event(true);
|
||||
assert_eq!(
|
||||
t.naechstes(),
|
||||
None,
|
||||
"EVENT OFF muss Eingetretenes verwerfen"
|
||||
);
|
||||
t.zeit_pruefen(2000);
|
||||
assert_eq!(
|
||||
t.naechstes(),
|
||||
Some((Quelle::Timer, 100)),
|
||||
"TIMER ON blieb erhalten"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reihenfolge_key_vor_timer_vor_uevent_vor_signal() {
|
||||
let mut t = Traps::neu();
|
||||
for (q, ziel) in [
|
||||
(Quelle::Signal(1), 4),
|
||||
(Quelle::UEvent, 3),
|
||||
(Quelle::Timer, 2),
|
||||
(Quelle::Key(5), 1),
|
||||
] {
|
||||
t.definieren(q, Some(ziel));
|
||||
t.setzen(q, Zustand::An, 0);
|
||||
t.melden(q);
|
||||
}
|
||||
let mut reihe = Vec::new();
|
||||
while let Some((q, _)) = t.naechstes() {
|
||||
reihe.push(q);
|
||||
t.handler_beendet(q);
|
||||
}
|
||||
assert_eq!(
|
||||
reihe,
|
||||
vec![
|
||||
Quelle::Key(5),
|
||||
Quelle::Timer,
|
||||
Quelle::UEvent,
|
||||
Quelle::Signal(1)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_null_faengt_alle_tasten_der_tabelle() {
|
||||
let mut t = Traps::neu();
|
||||
t.definieren(Quelle::Key(0), Some(9));
|
||||
t.setzen(Quelle::Key(0), Zustand::An, 0);
|
||||
assert_eq!(t.taste_faengt(0x3B, 0), Some(0), "F1");
|
||||
assert_eq!(t.taste_faengt(0x48, 0), Some(0), "Pfeil hoch");
|
||||
assert_eq!(t.taste_faengt(0x1E, 0), None, "A ist keine Trap-Taste");
|
||||
t.setzen(Quelle::Key(0), Zustand::Aus, 0);
|
||||
assert_eq!(t.taste_faengt(0x3B, 0), None, "OFF gibt die Taste frei");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vordefinierte_taste_wird_ueber_den_scancode_erkannt() {
|
||||
let mut t = Traps::neu();
|
||||
t.definieren(Quelle::Key(1), Some(1));
|
||||
t.setzen(Quelle::Key(1), Zustand::An, 0);
|
||||
assert_eq!(t.taste_faengt(0x3B, 0), Some(1), "F1");
|
||||
assert_eq!(t.taste_faengt(0x3C, 0), None, "F2 hat keinen Trap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benutzertaste_braucht_umschaltzustand_und_scancode() {
|
||||
let mut t = Traps::neu();
|
||||
t.definieren(Quelle::Key(15), Some(5));
|
||||
t.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
|
||||
t.benutzertaste(15, 4, 0x1E);
|
||||
assert_eq!(t.taste_faengt(0x1E, 2), Some(15), "Strg+A");
|
||||
assert_eq!(t.taste_faengt(0x1E, 0), None, "A ohne Strg nicht");
|
||||
assert_eq!(t.taste_faengt(0x1F, 2), None, "Strg+S nicht");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beide_umschaltbits_gelten_gleich() {
|
||||
let mut t = Traps::neu();
|
||||
t.definieren(Quelle::Key(16), Some(6));
|
||||
t.setzen(Quelle::Key(16), Zustand::An, 0);
|
||||
t.benutzertaste(16, 1, 0x1E); // rechte Umschalttaste
|
||||
assert_eq!(t.taste_faengt(0x1E, 1), Some(16));
|
||||
let mut t2 = Traps::neu();
|
||||
t2.definieren(Quelle::Key(16), Some(6));
|
||||
t2.setzen(Quelle::Key(16), Zustand::An, 0);
|
||||
t2.benutzertaste(16, 2, 0x1E); // linke Umschalttaste
|
||||
assert_eq!(t2.taste_faengt(0x1E, 1), Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genauer_trap_geht_dem_sammeltrap_vor() {
|
||||
let mut t = Traps::neu();
|
||||
for (q, z) in [(Quelle::Key(0), 9), (Quelle::Key(1), 1)] {
|
||||
t.definieren(q, Some(z));
|
||||
t.setzen(q, Zustand::An, 0);
|
||||
}
|
||||
assert_eq!(
|
||||
t.taste_faengt(0x3B, 0),
|
||||
Some(1),
|
||||
"F1 hat einen eigenen Trap"
|
||||
);
|
||||
assert_eq!(t.taste_faengt(0x3C, 0), Some(0), "F2 faellt an KEY(0)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gosub_null_schaltet_den_trap_ab() {
|
||||
let mut t = mit_timer();
|
||||
t.setzen(Quelle::Timer, Zustand::An, 0);
|
||||
t.definieren(Quelle::Timer, None);
|
||||
t.zeit_pruefen(5000);
|
||||
assert_eq!(t.naechstes(), None);
|
||||
assert!(!t.aktiv());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user