Phase 5: Ausführung und Output implementieren und archivieren

This commit is contained in:
2026-09-06 18:56:47 +02:00
parent 9c85349a4d
commit e3dc9028bf
37 changed files with 2404 additions and 450 deletions

View File

@@ -4,16 +4,12 @@
//! 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::io::{self, IsTerminal, Stdout};
use std::time::{Duration, Instant};
use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
MouseEventKind,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
@@ -29,6 +25,7 @@ use crate::signale::Signalquelle;
/// Der Konstruktor schaltet in den Alternativschirm und den Rohmodus,
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
pub struct TerminalHost {
guard: crate::terminal::TerminalGuard<Stdout>,
terminal: Terminal<CrosstermBackend<Stdout>>,
/// Nullpunkt der monotonen Zeit (`Host::jetzt_ms`).
start: Instant,
@@ -40,15 +37,18 @@ pub struct TerminalHost {
impl TerminalHost {
pub fn new() -> io::Result<Self> {
enable_raw_mode()?;
let mut out = io::stdout();
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
return Err(io::Error::other("Kein interaktives Terminal"));
}
let guard = crate::terminal::TerminalGuard::enter(io::stdout())?;
let out = io::stdout();
let terminal = Terminal::new(CrosstermBackend::new(out))?;
let flaeche = terminal
.size()
.map(|s| (s.width as usize, s.height as usize))
.unwrap_or((80, 25));
Ok(TerminalHost {
guard,
terminal,
start: Instant::now(),
signale: Signalquelle::neu(),
@@ -63,23 +63,25 @@ impl TerminalHost {
}
}
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 shell(&mut self, command: &str) -> Result<Option<i32>, tb_runtime::errors::RuntimeError> {
let result = self.guard.shell(command, &self.signale);
let _ = self.terminal.clear();
result
.map(Some)
.map_err(|_| tb_runtime::errors::RuntimeError(53))
}
fn present(&mut self, screen: &TextScreen) {
let _ = self
.terminal
.draw(|f| f.render_widget(ScreenWidget(screen), f.area()));
let _ = self.terminal.draw(|f| {
if f.area().width < 80 || f.area().height < 25 {
f.render_widget(
ratatui::widgets::Paragraph::new("Terminal zu klein: Minimum 80x25"),
f.area(),
);
} else {
f.render_widget(ScreenWidget(screen), f.area());
}
});
}
fn jetzt_ms(&mut self) -> u64 {
@@ -152,7 +154,7 @@ impl Host for TerminalHost {
/// crossterm zählt ab 0, der Dialekt ab 1 — umgerechnet wird hier, damit es
/// in der Laufzeit nur eine Zählweise gibt. Ereignisse außerhalb der
/// Darstellungsfläche und Ereignisse ohne Entsprechung liefern `None`.
fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis> {
pub fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis> {
let (cols, rows) = flaeche;
let spalte = m.column as usize + 1;
let zeile = m.row as usize + 1;
@@ -195,7 +197,7 @@ fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis>
}
/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
pub 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)