Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A

Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben).
Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature.

Vollstaendigkeits-Inventar
- docs/inventar.md mit 285 Eintraegen aus den Themenlisten von
  bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle
- crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide
  Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten

Bruchschritt (Puffer, Host, Korpus)
- Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui
  behaelt Farbabbildung, Widget und den neuen Terminal-Host
- Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus
  Ereignisse (Taste, Groessenaenderung, Abbruch)
- Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht
  bestehenden Sollausgaben blieben dabei unveraendert

Groessenunabhaengigkeit
- 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend
  der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor
  und VIEW PRINT geklemmt)
- Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener
  Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf

Bibliothek
- Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch)
- Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS
- Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON)
- PRINT USING, LPRINT USING, FORMAT$, SetFormatCC
- Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge)
- Datum und Zeit mit Serienwerten ab 1899
- Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#,
  IRR#, MIRR#, SLN#, SYD#, DDB#
- Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen),
  BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV
- System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung

Altlasten aus Phase 2
- ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar
  (prozeduruebergreifender Fixup im Codegenerator)
- DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum)
- Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest

Vom Inventar aufgedeckte Fehler
- Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht
  ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die
  String*-Routinen, LINE und OPEN "COMn:") -- geschlossen
- Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik,
  Record-Konvertierung, Rest der Dateisystemfunktionen
- bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar
  bildet die Vereinigung mit qb45advr.hlp

Neue Changes
- phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb)
- phase-3-ortszeit: zieht die UTC-Abweichung zurueck

Neue Abhaengigkeit: unicode-width.
Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei,
docs/bibliothek.md neu, PLAN.md fortgeschrieben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 06:54:19 +02:00
parent a6f005d023
commit 52ccbb5848
94 changed files with 10421 additions and 792 deletions

150
crates/tb-ui/src/host.rs Normal file
View File

@@ -0,0 +1,150 @@
//! Terminal-Host: stellt den Zellenpuffer über Ratatui dar und liefert
//! Tastatur- und Größenereignisse aus crossterm.
//!
//! Er lebt hier statt in `tb-runtime`, damit die Ausführungsschicht ohne
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).
use std::io::{self, Stdout};
use std::time::Duration;
use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
KeyModifiers,
};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use tb_runtime::host::{taste, Ereignis, Host};
use tb_runtime::screen::TextScreen;
use crate::screen::ScreenWidget;
/// Host für laufende Programme im Terminal.
///
/// Der Konstruktor schaltet in den Alternativschirm und den Rohmodus,
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
pub struct TerminalHost {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl TerminalHost {
pub fn new() -> io::Result<Self> {
enable_raw_mode()?;
let mut out = io::stdout();
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
let terminal = Terminal::new(CrosstermBackend::new(out))?;
Ok(TerminalHost { terminal })
}
/// Aktuelle Terminalgröße, damit der Bildschirm sie von Beginn an nutzt.
pub fn groesse(&self) -> io::Result<(usize, usize)> {
let s = self.terminal.size()?;
Ok((s.width as usize, s.height as usize))
}
}
impl Drop for TerminalHost {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
);
let _ = self.terminal.show_cursor();
}
}
impl Host for TerminalHost {
fn present(&mut self, screen: &TextScreen) {
let _ = self
.terminal
.draw(|f| f.render_widget(ScreenWidget(screen), f.area()));
}
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
loop {
if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) {
return None;
}
match event::read() {
Ok(Event::Key(k)) if k.kind == KeyEventKind::Press => {
if let Some(e) = taste_zu_ereignis(k) {
return Some(e);
}
}
Ok(Event::Resize(cols, rows)) => {
return Some(Ereignis::Groesse {
cols: cols as usize,
rows: rows as usize,
})
}
Ok(_) => {}
Err(_) => return Some(Ereignis::Ende),
}
}
}
}
/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
// Strg+Untbr bzw. Strg+C: Abbruchwunsch.
if k.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(k.code, KeyCode::Char('c') | KeyCode::Pause)
{
return Some(Ereignis::Abbruch);
}
let s = match k.code {
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => taste::ENTER.to_string(),
KeyCode::Backspace => taste::BACKSPACE.to_string(),
KeyCode::Esc => taste::ESC.to_string(),
KeyCode::Tab => taste::TAB.to_string(),
// Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
KeyCode::Home => taste::sonder(71),
KeyCode::Up => taste::sonder(72),
KeyCode::PageUp => taste::sonder(73),
KeyCode::Left => taste::sonder(75),
KeyCode::Right => taste::sonder(77),
KeyCode::End => taste::sonder(79),
KeyCode::Down => taste::sonder(80),
KeyCode::PageDown => taste::sonder(81),
KeyCode::Insert => taste::sonder(82),
KeyCode::Delete => taste::sonder(83),
_ => return None,
};
Some(Ereignis::Taste(s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zeichentaste_wird_zu_inkey_form() {
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into())));
}
#[test]
fn sondertaste_traegt_fuehrendes_nullzeichen() {
let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
let Some(Ereignis::Taste(s)) = taste_zu_ereignis(k) else {
panic!("keine Taste");
};
assert_eq!(s.chars().count(), 2);
assert_eq!(s.chars().next(), Some('\0'));
assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59
}
#[test]
fn strg_c_ist_abbruch() {
let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Abbruch));
}
}