Phase 5: Ausführung und Output implementieren und archivieren
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user