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

@@ -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)?;
// 1525 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()));
}
}