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>
151 lines
4.9 KiB
Rust
151 lines
4.9 KiB
Rust
//! 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));
|
|
}
|
|
}
|