Files
rustkeep/.scratch/rustkeep-1.0/research/06-ratatui-architektur.md

1195 lines
65 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06 — Research: ratatui-App-Architektur
Recherchiert am **2026-08-11**. Alle Versions- und Empfehlungsangaben sind gegen Live-Quellen
geprüft (crates.io, docs.rs, ratatui.rs, GitHub-Rohdateien), nicht aus Trainingswissen.
Ergebnis ist eine **begründete Empfehlung**, keine Entscheidung.
---
## 0. Kurzfassung
| Frage | Antwort in einem Satz |
|---|---|
| App-Muster | ratatui empfiehlt **nichts** — es dokumentiert drei Muster (Elm, Component, Flux) neutral. Für rustkeep passt ein **Screen-Stack (`Vec<Screen>`) über einem zentralen `Game`-State**, kein Elm, kein Component-Trait. |
| Popups | Kein Widget-Stack. Der Standard ist: Screen rendern, dann `Clear` auf ein zentriertes Sub-`Rect`, dann Popup. |
| Logik/Rendering | Rendering ist reine Funktion `(&Game, &Ui) -> Buffer`. Testbar ohne Terminal über `TestBackend` — besser noch: Spiellogik komplett ohne ratatui-Typen. |
| Event-Loop | Blockierendes `event::read()`. Ein Tick ist für ein rundenbasiertes Spiel **nicht** nötig. |
| Dungeon-Grid | Muss selbst gebaut werden. Es gibt kein Grid-/Tilemap-Widget, weder in ratatui noch als Drittanbieter-Crate. `impl Widget` mit direktem Buffer-Zugriff. |
| Panic | `ratatui::init()` bzw. `ratatui::run()` installieren den Panic-Hook **selbst**. Die Rezeptseite dazu ist veraltet. |
| Versionen | ratatui **0.30.2** (2026-06-19, MSRV 1.88), crossterm **0.29.0** (2025-04-05). |
| Windows | Ein Fallstrick, der zwingend adressiert werden muss: doppelte Key-Events. Lösung ist eine Zeile. Dazu ein paar Dinge, die auf Windows schlicht nicht gehen (Kitty-Protokoll, `Event::Paste`) und Spaltenversatz bei doppelbreiten Zeichen in conhost. |
---
## 1. Versionsstand
| Crate | Version | Datum | Quelle |
|---|---|---|---|
| `ratatui` | **0.30.2** | 2026-06-19 | <https://crates.io/api/v1/crates/ratatui>, <https://raw.githubusercontent.com/ratatui/ratatui/main/CHANGELOG.md> |
| `crossterm` | **0.29.0** | 2025-04-05 | <https://crates.io/api/v1/crates/crossterm>, <https://github.com/crossterm-rs/crossterm/releases/tag/0.29> |
Vorgängerversionen zur Einordnung: ratatui 0.30.1 (2026-06-05), 0.30.0 (2025-12-26), 0.29.0 (2024-10-21)
— Quelle wie oben.
**MSRV:** ratatui 0.30.2 verlangt Rust **1.88.0** (`rust-version = "1.88.0"` in
<https://github.com/ratatui/ratatui/blob/main/Cargo.toml>; 0.30.0 lag noch bei 1.86.0). ratatui selbst
steht auf Edition 2024 — rustkeep also auch, passt.
crossterm 0.29.0 verlangt nur 1.63.0 (crates.io `rust_version`).
**Hinweis für später:** Auf crossterms `master` liegt ein unveröffentlichter `Unreleased`-Abschnitt mit
Breaking Changes — MSRV 1.63 → 1.85, Entfernung des `IsTty`-Traits zugunsten von
`std::io::IsTerminal`, Umstellung auf Edition 2024
(<https://raw.githubusercontent.com/crossterm-rs/crossterm/master/CHANGELOG.md>). Ein 0.30 ist noch
nicht veröffentlicht. Nebenbei: crossterm **0.28.0 ist yanked**, nutzbar ist 0.28.1.
### 1.1 ratatui 0.30 ist aufgeteilt
Seit 0.30.0 ist `ratatui` ein Facade-Crate über mehrere Teilcrates
(Quellen: <https://docs.rs/crate/ratatui/latest/features>, <https://docs.rs/crate/ratatui/latest>):
| Crate | Version | Rolle |
|---|---|---|
| `ratatui` | 0.30.2 | Umbrella-Crate — das nimmt man in Anwendungen |
| `ratatui-core` | 0.1.2 | Buffer, Rect, Style, Widget-Traits (für Widget-Autoren) |
| `ratatui-widgets` | 0.3.2 | die mitgelieferten Widgets |
| `ratatui-crossterm` | 0.1.2 | Crossterm-Backend (Default) |
| `ratatui-termion` | 0.1.2 | Termion-Backend |
| `ratatui-termwiz` | 0.1.2 | Termwiz-Backend |
| `ratatui-termina` | 0.1.0 | Termina-Backend, **neu in 0.30.2** |
| `ratatui-macros` | 0.7.2 | Makros |
Default-Features: `crossterm`, `all-widgets`, `layout-cache`, `macros`, `underline-color`, `std`,
`widget-calendar` (ebd.).
**Windows-relevant:** ratatuis eigene `Cargo.toml` schließt das Termion-Backend aus den
`default-members` aus, wörtlich kommentiert:
```
# this is not included as it doesn't compile on windows
# "ratatui-termion",
```
Quelle: <https://github.com/ratatui/ratatui/blob/main/Cargo.toml>
Die Backend-Wahl ist damit für rustkeep entschieden, bevor sie gestellt wird — und deckt sich mit
map.md. ratatui selbst rät: *"Choose Crossterm for most tasks"*, mit einem ausdrücklichen
Entscheidungszweig *"Is Windows compatibility important?"*
(<https://ratatui.rs/concepts/backends/comparison/>).
Für rustkeep heißt das: `ratatui = "0.30"` mit Defaults genügt. Man muss `crossterm` **nicht**
separat als Dependency führen, solange man nur die von ratatui re-exportierten Teile nutzt —
und sollte es auch nicht, siehe 1.2.
### 1.2 Fallstrick: zwei crossterm-Versionen im Graph
ratatui bindet `crossterm = "0.29"` in `[workspace.dependencies]`
(<https://github.com/ratatui/ratatui/blob/main/Cargo.toml>). `ratatui-crossterm` 0.1.2 unterstützt
zwei crossterm-Versionen über sich gegenseitig ausschließende Features, durchgereicht bis zum
Umbrella-Crate: **`crossterm_0_29` ist Default**, `crossterm_0_28` ist opt-in
(<https://docs.rs/crate/ratatui-crossterm/latest/features>, <https://docs.rs/crate/ratatui/latest/features>).
Zieht man crossterm zusätzlich direkt in `Cargo.toml` und trifft eine andere Major-Version als die von
ratatui kompilierte, gibt es Typ-Mismatch-Fehler, die schwer zu lesen sind. Die ratatui-FAQ
dokumentiert das explizit:
> "You can resolve these conflicts by selecting the Crossterm major version used by Ratatui via feature flags."
Quelle: <https://ratatui.rs/faq/> (Eintrag *"How do I fix errors when multiple Crossterm versions are used?"*)
**Empfehlung:** crossterm nur über den ratatui-Re-Export benutzen, keine eigene crossterm-Zeile in
`Cargo.toml`. Falls doch nötig: exakt die Version pinnen, die ratatui zieht.
---
## 2. Welches App-Muster — was ratatui sagt
### 2.1 ratatui dokumentiert drei Muster und empfiehlt keines
Die Übersichtsseite <https://ratatui.rs/concepts/application-patterns/> listet:
**The Elm Architecture (TEA)** — <https://ratatui.rs/concepts/application-patterns/the-elm-architecture/>
Model (aller State), Message (Enum aller Eingaben/Aktionen), Update (`Model + Message -> Model`),
View (`Model -> UI`). Die `update`-Funktion darf `Option<Message>` zurückgeben, wodurch sich Updates
verketten und Zustandsautomaten bauen lassen. Die Seite nennt zwei Reibungspunkte selbst:
Reine Immutabilität ist in Rust unpraktisch (*"Rust developers can choose the most suitable approach
based on performance and their application's needs"*), und `StatefulWidget`s brauchen `&mut`, was die
View-Reinheit bricht. Dazu die Immediate-Mode-Einschränkung: *"the `view` function is only aware of the
area available to draw in at render time"* — wer die Größe zwischenspeichert, handelt sich einen
Frame Verzögerung und Flackern ein.
**Component Architecture** — <https://ratatui.rs/concepts/application-patterns/component-architecture/>
Objektorientierter: ein `Component`-Trait mit `init`, `handle_events` / `handle_key_events` /
`handle_mouse_events`, `update`, `render(&mut self, f: &mut Frame, rect: Rect)`. State ist
komponentenprivat statt global. Genannter Vorteil, wörtlich:
> "One advantage of this approach is that it incentivizes co-locating the `handle_events`, `update` and `render` functions on a component level."
Nachteile nennt die Seite **keine**. Verwiesen wird auf `TaKO8Ki/gobang`, `nomadiz/edma` und das
offizielle `component`-Template.
**Flux Architecture** — <https://ratatui.rs/concepts/application-patterns/flux-architecture/>
Dispatcher / Stores / Actions / Views, unidirektionaler Datenfluss. Begründung auf der Seite:
*"Terminal applications often have to deal with complex user interactions, multiple views, and dynamic
data sources."* Auch hier: keine Nachteile genannt.
**Es gibt keine Präferenz.** Die Übersichtsseite ist wörtlich neutral:
> "This page covers several patterns one can use for their application and acts as a top-level page for the following articles where these patterns are explored more in-depth."
Bestätigt durch:
- Tutorial-Abschluss: *"this is only *one* way to create a `ratatui` application. Because `ratatui` is relatively low level compared to other UI frameworks, almost any application model can be implemented."* — <https://ratatui.rs/tutorials/json-editor/closing-thoughts/>
- Template-Seite: *"This is an opinionated view on how to organize a `ratatui` project."* … *"pick and choose the pieces of this template to suit your needs and sensibilities."* — <https://ratatui.rs/templates/component/>
- Die Template-README benennt kein Default-Template — <https://github.com/ratatui/templates/blob/main/README.md>
### 2.2 Zu Multi-Screen gibt es keine offizielle Doku
**Verifiziert negativ:** Auf ratatui.rs existiert keine Seite über Screen-Wechsel, Routing oder
Multi-Screen-Struktur. Die nächstgelegenen offiziellen Artefakte sind die drei Musterseiten und die
Templates.
Was es gibt, ist eine Aussage des ratatui-Maintainers **joshka** im offiziellen Forum
(<https://forum.ratatui.rs/t/multiple-screens-in-ratatui/82>, Beitrag vom 27. Juli):
> "I'd still implement this as an enum with all the screens included and have logic to choose which screen is rendered and when to change between them."
Das ist Maintainer-Meinung im Forum, keine dokumentierte Empfehlung — entsprechend gewichten.
Im selben Thread rät er außerdem zu *"rendering on a single thread"*, weil *"stdio is a global lock"*.
### 2.3 Modulstruktur der offiziellen Templates
Repo: <https://github.com/ratatui/templates>, Branch `main`. Sechs Templates:
`hello-world`, `simple`, `simple-async`, `event-driven`, `event-driven-async`, `component`.
- `hello-world` / `simple` / `simple-async` — nur `src/main.rs`.
- `event-driven` / `event-driven-async` — `main.rs`, `app.rs`, `event.rs`, `ui.rs`.
Rollen laut <https://github.com/ratatui/templates/blob/main/event-driven/README.md>:
`app.rs` = `App`-Struct mit State und Kernlogik; `event.rs` = `Event`/`AppEvent`-Enums plus
`EventHandler`, der *"spawns a new thread and listens for the events using mpsc channels"*;
`ui.rs` = das Rendering (die `Widget`-Impl für `App` liegt bewusst **nicht** in `app.rs`).
- `component` — das größte Template
(<https://ratatui.rs/templates/component/project-structure/>):
```
src/
├── main.rs Einstiegspunkt
├── app.rs App-State + Loop, hält Vec<Box<dyn Component>>
├── action.rs Action-Enum
├── components.rs das Component-Trait
├── components/
│ ├── home.rs
│ └── fps.rs
├── tui.rs Terminal-Setup/-Teardown + Event-Stream
├── cli.rs clap
├── config.rs
├── errors.rs Panic-/Error-Hooks
└── logging.rs tracing
```
Regel des Templates, wörtlich:
> "Once you have set up the project, you shouldn't need to change the contents of anything outside the `components` folder."
Es zieht tokio, tracing, clap, color-eyre, better-panic und human-panic mit. Das ist für rustkeep
deutlich zu viel Apparat — und es hat von Haus aus **kein** Popup-/Overlay-Konzept.
Randnotiz: Die ältere Tutorial-Aufteilung `app.rs / event.rs / tui.rs / ui.rs / update.rs` liegt unter
<https://ratatui.rs/tutorials/counter-app/_multiple-files/>. Der führende Unterstrich schließt die Seite
aus der Navigation aus — als Alt-Bestand behandeln, nicht als aktuelle Empfehlung.
---
## 3. Was echte Projekte tun
Untersucht wurden neun Projekte anhand ihres tatsächlichen Quellcodes. Das Bild ist eindeutig:
**niemand implementiert reines Elm, und praktisch alle nutzen ein Enum oder einen Stack für Screens.**
### 3.1 chess-tui — das nächste Vorbild für rustkeep
<https://github.com/thomas-mauran/chess-tui> — rundenbasiertes Spiel, Menü + Spielfeld + mehrere
Popups, in ratatuis offizieller Showcase-Liste (<https://ratatui.rs/showcase/apps/>).
`src/state/ui_state.rs` (<https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/state/ui_state.rs>):
```rust
pub struct UIState {
pub current_page: Pages,
pub current_popup: Option<Popups>,
pub popup_message: Option<String>,
pub end_screen_dismissed: bool,
pub menu_cursor: u8,
}
```
`src/constants.rs` (<https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/constants.rs>):
```rust
pub enum Pages { Home, Solo, Multiplayer, Lichess, LichessMenu,
OngoingGames, Bot, Credit, GameModeMenu, PgnViewer }
pub enum Popups { EnterHostIP, WaitingForOpponentToJoin, EnginePathError, Help,
EndScreen, PuzzleEndScreen, Error, Success, SeekingLichessGame,
EnterGameCode, EnterLichessToken, ResignConfirmation,
MoveInputSelection, LoadPgnPath, Loading }
```
Dispatch in `src/ui/main_ui.rs`
(<https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/ui/main_ui.rs>):
```rust
match app.ui_state.current_page {
Pages::Solo => render_game_ui(frame, app, main_area),
Pages::Bot => render_bot_page(frame, app, main_area),
// ...
Pages::Home | Pages::Credit => render_menu_ui(frame, app, main_area),
}
// danach ein zweites match auf app.ui_state.current_popup
```
Genau das Muster, das rustkeep braucht — nur dass `Option<Popups>` bei uns ein Stack werden sollte
(siehe 4).
### 3.2 sxtetris — der sauberste Screen-Stack
<https://github.com/shixinhuang99/sxtetris>, `src/state/focus.rs` (vollständig, ~50 Zeilen):
```rust
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum Scene { StartMenu, Game, PauseMenu, GameOverMenu,
SettingMenu, Scores, Help, About }
pub struct Focus { history: Vec<Scene> }
impl Focus {
pub fn new() -> Self { Self { history: vec![Scene::StartMenu] } }
pub fn to(&mut self, focus: Scene) { self.history.clear(); self.history.push(focus); }
pub fn push(&mut self, focus: Scene) { self.history.push(focus); }
pub fn back(&mut self) { self.history.pop(); }
pub fn current(&self) -> &Scene { self.history.last().expect("should be a least one") }
pub fn contains(&self, scene: Scene) -> bool { self.history.contains(&scene) }
}
```
Quelle: <https://raw.githubusercontent.com/shixinhuang99/sxtetris/main/src/state/focus.rs>
Der Clou ist `contains()`: das Rendering fragt nicht nur nach dem obersten Scene, sondern zeichnet
jede Szene, die im Stack liegt. Dadurch bleibt das Spielfeld unter dem Pausenmenü sichtbar, ohne
Sonderfall. `push(Scene::PauseMenu)` lässt den Spielzustand unangetastet.
Für rustkeep: Inventar über dem Dungeon-Grid, Bestätigungsdialog über dem Kampf — genau diese Semantik.
### 3.3 gitui — Component-Trait und Event-Verbrauch
<https://github.com/gitui-org/gitui>, `src/components/mod.rs`:
```rust
pub trait DrawableComponent { fn draw(&self, f: &mut Frame, rect: Rect) -> Result<()>; }
pub trait Component {
fn commands(&self, out: &mut Vec<CommandInfo>, force_all: bool) -> CommandBlocking;
fn event(&mut self, ev: &Event) -> Result<EventState>;
fn focused(&self) -> bool { false }
fn is_visible(&self) -> bool { true }
fn hide(&mut self) {}
fn show(&mut self) -> Result<()> { Ok(()) }
// ...
}
pub enum EventState { Consumed, NotConsumed }
```
Quelle: <https://raw.githubusercontent.com/gitui-org/gitui/master/src/components/mod.rs>
`src/app.rs`:
```rust
if event_pump(&ev, self.components_mut().as_mut_slice())?.is_consumed() {
flags.insert(NeedsUpdate::COMMANDS);
} else if let Event::Key(k) = &ev { /* globale Shortcuts */ }
```
Das ist die eine Idee aus gitui, die sich lohnt: **`Consumed`/`NotConsumed` als Rückgabewert der
Eingabebehandlung.** Oberster Screen bekommt das Event zuerst; was er nicht verbraucht, fällt auf
globale Tasten durch.
Wichtige Korrektur eines verbreiteten Missverständnisses: gitui hat **keinen**
`Vec<Box<dyn Component>>`-Popup-Stack. Die Popups sind ~25 benannte konkrete Felder auf `App`
(`help_popup`, `msg_popup`, `confirm_popup`, …), generiert über `accessors!`/`setup_popups!`-Makros;
`is_visible()` entscheidet, ob gezeichnet wird. `src/popup_stack.rs` ist 298 Byte groß und speichert
nur, **welches Popup wieder zu öffnen ist** (Breadcrumb für „Zurück"), nicht Render-Objekte:
```rust
pub struct PopupStack { stack: Vec<StackablePopupOpen> }
```
Quelle: <https://raw.githubusercontent.com/gitui-org/gitui/master/src/popup_stack.rs>
### 3.4 ratatuis eigenes Flaggschiff-Beispiel: demo2
<https://ratatui.rs/examples/apps/demo2/> — Tab-Enum, ein Modul pro Tab, `impl Widget for &App`:
```rust
enum Tab { About, Recipe, Email, Traceroute, Weather }
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
match self.tab {
Tab::About => self.about_tab.render(area, buf),
Tab::Recipe => self.recipe_tab.render(area, buf),
// ...
};
}
```
Module: `app.rs`, `tabs.rs` + `tabs/{about,email,recipe,traceroute,weather}.rs`, `theme.rs`,
`colors.rs`, `destroy.rs`.
Das ist offizieller ratatui-Code und deckt sich mit joshkas Forum-Aussage.
### 3.5 Weitere, kurz
| Projekt | Muster | Bemerkung |
|---|---|---|
| **Rebels in the Sky** (<https://github.com/ricott1/rebels-in-the-sky>) | `UiState`-Enum + `UiTab`-Enum + `Screen`-Trait pro Panel + `Vec<PopupMessage>` + `UiCallback`-Enum (faktisch Elm-`Msg`) | Größtes Spiel-Vorbild. Explizite Layer-Nummer (`set_active_layer(1)` bei Popup). Nur `popup_messages[0]` wird gezeichnet und bekommt alle Eingaben. |
| **taskwarrior-tui** (<https://github.com/kdheepak/taskwarrior-tui>) | verschachteltes Enum: `Mode::Tasks(Action::HelpPopup)` | **Warnendes Beispiel**: `app.rs` ist 234 KB. Popup im State-Enum zu kodieren skaliert schlecht. |
| **yazi** (<https://github.com/sxyazi/yazi>) | Maler-Algorithmus ohne `Clear`; jedes Overlay ein Modul mit `active`-Flag, feste Renderreihenfolge in `root.rs` | Overlays malen ihren eigenen opaken Hintergrund. |
| **television** (<https://github.com/alexpasmantier/television>) | `Action`-Enum + zentraler State + `draw.rs` mit fester Reihenfolge | Nur das echte Popup nutzt `Clear`; Hilfe/Picker übernehmen bewusst ein bestehendes Panel. |
| **atuin** (<https://github.com/atuinsh/atuin>) | ein großer `App`-Struct, `popup_mode: bool` → `Clear` über den **ganzen** Frame | `interactive.rs` ist 105 KB. |
| **bottom** (<https://github.com/ClementTsang/bottom>) | handgebauter Widget-Layout-Baum, Message-Loop | Kein Screen-Stack (Single-Dashboard). |
**Verifiziert negativ:** Ich habe kein prominentes ratatui-basiertes Roguelike gefunden. Die
bekannten Rust-Roguelikes (rusty-rogue u. a.) nutzen bracket-lib/rltk, nicht ratatui. In ratatuis
Showcase stehen als Spiele nur `chess-tui`, `minesweep-rs` und `twozero48`
(<https://ratatui.rs/showcase/apps/>). rustkeep betritt hier also kein ausgetretenes Terrain,
aber chess-tui und Rebels in the Sky sind nahe genug.
---
## 4. Empfehlung für rustkeep
### 4.1 Muster: Screen-Stack über zentralem `Game`-State
Nicht Elm, nicht Component-Trait, nicht Flux. Begründung:
- **Gegen Elm/TEA:** Der Charter legt bereits einen `Game`-Ownership-Baum mit ID-Verweisen fest
(map.md). Ein zusätzliches `Message`/`update`-Sandwich über jede Zustandsänderung zu legen,
verdoppelt die Schreibarbeit für jede Aktion, ohne dass ein Nutzen dagegensteht — es gibt kein
Undo, kein Time-Travel-Debugging, keine Netzwerkreplikation. Und ratatui selbst räumt ein, dass
die View-Reinheit in Rust an `StatefulWidget` scheitert
(<https://ratatui.rs/concepts/application-patterns/the-elm-architecture/>).
- **Gegen Component-Trait:** Der Vorteil des Traits ist Polymorphie über eine offene Menge von
Komponenten. rustkeep hat eine **geschlossene, bekannte** Menge von sieben Bildschirmen. Ein Enum
bildet das exakt ab, der Compiler prüft Vollständigkeit im `match`, und es fällt kein
`Box<dyn>`/Downcasting an. Das offizielle `component`-Template zieht außerdem tokio, clap, tracing,
color-eyre mit — Apparat, den rustkeep nicht braucht.
- **Gegen Flux:** Dispatcher und Stores sind eine Antwort auf verteilten State in großen
Client-Apps. rustkeep hat einen Ownership-Baum und einen einzigen Thread.
- **Für den Stack statt eines einzelnen `current_screen`:** Inventar über Kampf, Bestätigung über
Inventar, „Zurück" muss überall funktionieren. Ein `Vec<Screen>` löst das in ~30 Zeilen
(sxtetris), ein einzelnes Feld erzwingt ein `previous_screen`-Flickwerk.
**Nicht** aus dem Blick verlieren: Die Verwandtschaft zu Elm bleibt bestehen — der Rückgabewert der
Eingabebehandlung ist eine `Action`, die der Kern verarbeitet. Das ist die eine Elm-Idee, die trägt,
ohne dass man das ganze Muster übernimmt.
### 4.2 Skizze der Modulstruktur
```
src/
├── main.rs Terminal init/restore, ruft ui::run(&mut game)
│
├── game/ ← kennt ratatui NICHT. Testbar ohne Terminal.
│ ├── mod.rs struct Game — der Ownership-Baum aus map.md
│ ├── stats.rs (bestehend)
│ ├── player.rs (bestehend, ohne print_box)
│ ├── dungeon.rs Grid, Etagen, Räume
│ ├── combat.rs Kampfzustand als Daten
│ ├── inventory.rs
│ └── action.rs enum Action — was der Spieler tun kann.
│ Genau EIN Eintrittspunkt: Game::apply(Action) -> Vec<GameEvent>
│
├── data/ ← TOML-Laden, Mod-Registry (Tickets 02/03)
│ ├── mod.rs
│ ├── items.rs (bestehend, umgezogen)
│ └── registry.rs
│
└── ui/ ← kennt Game nur lesend, ändert ihn nie direkt
├── mod.rs run(): Event-Loop. terminal.draw(..) + event::read()
├── screen.rs enum Screen { MainMenu, Dungeon, Combat, Inventory,
│ Shop, Questlog, CharSheet }
│ struct ScreenStack { stack: Vec<Screen> }
│ push/pop/replace/current/contains
├── input.rs Screen + KeyEvent -> Option<UiCommand>
│ enum UiCommand { Game(Action), Push(Screen), Pop, Quit }
│ enum Consumed { Yes, No } ← gitui-Idee
├── theme.rs Farben und Symbole an EINER Stelle
├── widgets/
│ ├── mod.rs
│ ├── grid.rs ← Eigenbau: impl Widget für die Dungeon-Karte
│ ├── statusbar.rs HP/Stamina via Gauge
│ └── popup.rs Popup { width, height, title } -> render() gibt inneres Rect zurück
└── screens/
├── mod.rs
├── main_menu.rs
├── dungeon.rs fn render(game: &Game, area: Rect, buf: &mut Buffer)
├── combat.rs
├── inventory.rs
├── shop.rs
├── questlog.rs
└── char_sheet.rs
```
Die harte Regel dahinter: **`game/` darf `ratatui` nicht importieren.** Das ist mechanisch prüfbar
(`grep -r ratatui src/game/` muss leer sein) und garantiert Testbarkeit ohne Terminal.
Skizze des Loops:
```rust
// ui/mod.rs
pub fn run(game: &mut Game) -> io::Result<()> {
ratatui::run(|terminal| {
let mut ui = UiState::new(); // ScreenStack + Listen-States
while !ui.quit {
terminal.draw(|f| render(f, game, &mut ui))?;
if let Some(key) = event::read()?.as_key_press_event() {
match input::map(&ui, key) { // Screen entscheidet
Some(UiCommand::Game(a)) => { let ev = game.apply(a); ui.react(ev); }
Some(UiCommand::Push(s)) => ui.stack.push(s),
Some(UiCommand::Pop) => ui.stack.pop(),
Some(UiCommand::Quit) => ui.quit = true,
None => {}
}
}
}
Ok(())
})
}
```
`ratatui::run` gibt es seit 0.30
(<https://docs.rs/ratatui/latest/ratatui/fn.run.html>): *"creates a new `DefaultTerminal` with `init`
and then runs the given closure … After the closure completes, the terminal is restored to its
original state with `restore`."*
### 4.3 Was ich bewusst NICHT empfehle
- **Kein `Box<dyn Screen>`.** Keines der untersuchten Projekte macht das für Screens oder Popups,
und die geschlossene Menge macht es überflüssig.
- **Kein tokio.** Siehe 6.
- **Kein `Mode::Combat(Popup::Confirm)`** (taskwarrior-tui-Muster). Es hat dort zu einer
234-KB-`app.rs` geführt. Popup-Stack getrennt vom Screen-Stack halten — oder beides im selben
`Vec<Screen>`, wenn Popups auch nur Screens sind. Letzteres ist der sxtetris-Weg und der
einfachere.
---
## 5. Popups und Overlay-Stacking
### 5.1 Das Verfahren
ratatui hat kein Popup-Widget und keinen Layer-Mechanismus. Es gibt nur einen `Buffer`, in den
nacheinander geschrieben wird — später gezeichnet gewinnt. Das offizielle Rezept:
> "Widgets do not always clear every cell in the area that they are rendering to. This may cause symbols and styles that were previously rendered to the buffer to 'bleed' through into the cells that are rendered on top of those cells."
>
> "Use the `Clear` widget to clear areas of the screen to avoid style and symbols from leaking from previously rendered widgets."
Quelle: <https://ratatui.rs/recipes/render/overwrite-regions/>
Offizielles Beispiel
(<https://github.com/ratatui/ratatui/blob/main/examples/apps/popup/src/main.rs>):
```rust
let centered_area = area.centered(Constraint::Percentage(60), Constraint::Percentage(20));
frame.render_widget(Clear, centered_area);
frame.render_widget(popup_block, centered_area);
```
`Rect::centered(h, v)` ist neu in 0.30, dazu `centered_horizontally`, `centered_vertically`, `outer`,
`resize`, `clamp`, `union`, `intersection`
(<https://docs.rs/ratatui/latest/ratatui/layout/struct.Rect.html>). Vorher musste man sich das aus
verschachtelten `Layout`s bauen.
### 5.2 Die fünf in freier Wildbahn belegten Strategien
| Strategie | Wer | Beleg |
|---|---|---|
| **A. `Clear` auf zentriertem Sub-Rect, nach dem Screen gezeichnet** — der Normalfall | ratatui-Beispiel, gitui, taskwarrior-tui, television, sxtetris, chess-tui | s. o. |
| **B. Maler-Algorithmus ohne `Clear`; Overlays malen eigenen opaken Hintergrund** | yazi `root.rs`, sxtetris (Spielfeld bleibt unter dem Pausenmenü sichtbar) | <https://raw.githubusercontent.com/sxyazi/yazi/main/yazi-fm/src/root.rs> |
| **C. `Vec<Popup>`-Warteschlange, nur Index 0 gezeichnet, verbraucht alle Eingaben** | Rebels in the Sky (`popup_messages`, Kappe 8, Dedup) | `src/ui/ui_screen.rs` |
| **D. Popup im State-Enum kodiert** | taskwarrior-tui (`Mode::Tasks(Action::HelpPopup)`), sxtetris (`Scene::PauseMenu` auf dem Stack) | s. 3.2/3.5 |
| **E. Separater „welches Popup wieder öffnen"-Breadcrumb, getrennt vom Rendering** | gitui `PopupStack` | <https://raw.githubusercontent.com/gitui-org/gitui/master/src/popup_stack.rs> |
| **F. `Clear` über den ganzen Frame (Vollbild-Modal)** | atuin (`if popup_mode { f.render_widget(Clear, f.area()); }`) | `.../search/interactive.rs` |
**Wichtiger Negativbefund:** Ich habe **kein** produktives Projekt gefunden, das
`Vec<Box<dyn Component>>` als Popup-Stack benutzt. Das ist eine Forum-Idee, keine gelebte Praxis.
### 5.3 Empfehlung für rustkeep
Ein wiederverwendbarer Helfer nach sxtetris-Vorbild (`src/ui/utils.rs` dort): `Popup { width, height,
title, padding }`, dessen `render()` das zentrierte `Rect` berechnet, `Clear` darauf rendert, den
Rahmen zeichnet und **das innere `Rect` zurückgibt**, das der Aufrufer füllt. Das ist die
Popup-Abstraktion in ~20 Zeilen, ohne Trait.
Für „Inventar über Kampf" ist Strategie B/D die richtige: das Inventar liegt als weiterer `Screen` auf
dem Stack, der Kampfbildschirm wird trotzdem gerendert (`stack.contains(Screen::Combat)`), und das
Inventar bekommt ein `Clear`-Fenster darauf. Der Kampfzustand bleibt unangetastet.
Optional: `widgets::Shadow` und `widgets::Dimmed` sind neu in 0.30 und geben Popups Tiefe bzw. dimmen
den Hintergrund (<https://docs.rs/ratatui/latest/ratatui/widgets/index.html>).
---
## 6. Event-Loop
### 6.1 Braucht ein rundenbasiertes Spiel einen Tick? Nein.
ratatui liefert selbst keine Eingabebehandlung:
> "Ratatui does not include any input handling. Instead event handling can be implemented by calling backend library methods directly."
Quelle: <https://docs.rs/ratatui/latest/ratatui/>
Der dokumentierte Standardfall ist **blockierendes** `event::read()`. Das Counter-Tutorial
(<https://ratatui.rs/tutorials/counter-app/basic-app/>):
```rust
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
```
mit dem Kommentar *"The `event::read` function blocks until there is an event."*
Das `simple`-Template sagt dazu, wann man **stattdessen** pollen sollte:
> "If your application needs to perform work in between handling events, you can use the `event::poll` function to check if there are any events available with a timeout."
Quelle: <https://raw.githubusercontent.com/ratatui/templates/main/simple/template/src/main.rs>
Für rustkeep heißt das: Solange nichts von allein passiert (keine Animation, keine Echtzeit-Gegner,
kein Netzwerk), ist der Tick **überflüssig**. Redraw nur nach Eingabe. CPU-Last im Leerlauf: null.
Das Konzept dahinter, wörtlich:
> "In Immediate mode rendering, the onus of triggering rendering lies on the programmer. Every visual update necessitates a call to `Backend.draw()`."
Quelle: <https://ratatui.rs/concepts/rendering/>
**Einschränkung, explizit:** ratatui dokumentiert **nirgends** direkt den Vergleich
`read()` vs. `poll()` vs. `EventStream`. `<https://ratatui.rs/concepts/event-handling/>` behandelt nur
die Architektur (zentrales `match` / zentral mit Message-Passing / verteilte Sub-Loops), nicht die
Loop-Mechanik. Die Aussage „blocking read ist der dokumentierte Default" leite ich aus Crate-Doku,
Tutorial und Template ab, nicht aus einem Satz, der das so sagt.
### 6.2 Was echte Projekte tun (Übersicht)
| Projekt | Mechanik |
|---|---|
| ratatui `simple`-Template | blockierendes `event::read()` |
| gitui | eigener OS-Thread, `poll(dur)` + `read()`, crossbeam-Channel; **adaptives Polling: 100 ms direkt nach Eingabe, 10 s im Leerlauf** |
| bottom | Input-Thread mit `poll(20ms)`, Main-Loop blockiert auf `receiver.recv()` |
| atuin | `tokio::select!` über `spawn_blocking(poll(250ms))`, dann Drain-Loop `while poll(ZERO)` |
| Rebels in the Sky | tokio, aber intern `poll(no_wait)` + `read()` + `sleep`, **kein** `EventStream` |
| taskwarrior-tui, sxtetris | `crossterm::event::EventStream` + `tokio::select!` mit Tick-Task |
Das offizielle Rezept <https://ratatui.rs/recipes/apps/terminal-and-event-handler/> geht voll auf
async: `EventStream` + `tokio::select!` + getrennte `tick_rate(4.0)` / `frame_rate(30.0)` +
`CancellationToken`. **Für rustkeep ist das Overkill** und würde tokio als Abhängigkeit einschleppen.
Falls später doch ein Tick nötig wird (Animationen, Autosave, Statuseffekt-Timer): gituis adaptives
Polling ist der billigste Weg — ein Thread, `poll` mit kurzem Intervall nach Aktivität, langem im
Leerlauf, kein async.
### 6.3 Eine Sache, die man früh richtig machen sollte
joshka im Forum: *"rendering on a single thread"*, weil *"stdio is a global lock"*
(<https://forum.ratatui.rs/t/multiple-screens-in-ratatui/82>). Also: nie aus zwei Stellen zeichnen.
Für rustkeep automatisch erfüllt, solange es einen Thread gibt.
---
## 7. Trennung von Spiellogik und Rendering, Testbarkeit
### 7.1 Was ratatui dazu sagt
Kein eigenes Doktrin-Kapitel, aber verstreut:
- Flux-Seite: Widgets *"don't hold or manage the application state, but they display it."*
(<https://ratatui.rs/concepts/application-patterns/flux-architecture/>)
- Rendering-Konzept: *"Without a persistent widget state, your UI logic becomes a direct reflection of
your application state."* (<https://ratatui.rs/concepts/rendering/>)
- Das `event-driven`-Template legt `App` in `app.rs` und dessen `Widget`-Impl bewusst in `ui.rs`
(<https://github.com/ratatui/templates/blob/main/event-driven/README.md>).
Der Immediate-Mode selbst hilft: Da jeder Frame neu aus dem State gezeichnet wird, gibt es keinen
UI-State, der aus dem Tritt geraten kann. Ausnahme sind die drei `StatefulWidget`s
(`ListState`, `TableState`, `ScrollbarState`) — die gehören in `ui/`, nicht in `game/`.
### 7.2 Testen ohne Terminal
Zwei Ebenen, und die wichtigere ist die erste:
**(a) Spiellogik ohne ratatui.** Wenn `game/` kein ratatui importiert, sind Kampfformeln,
Bewegungslogik, Inventar und Questbedingungen ganz normale Unit-Tests. Das ist der eigentliche
Testbarkeitsgewinn und kostet nur Disziplin. `src/stats.rs` macht das heute schon richtig.
**(b) Rendering mit `TestBackend`.**
```rust
let mut terminal = Terminal::new(TestBackend::new(80, 24))?;
terminal.draw(|f| render(f, &game, &mut ui))?;
terminal.backend().assert_buffer_lines([...]);
```
API auf `TestBackend` (<https://docs.rs/ratatui/latest/ratatui/backend/struct.TestBackend.html>):
`buffer()`, `assert_buffer()`, `assert_buffer_lines()`, `assert_scrollback()`,
`assert_scrollback_lines()`, `assert_scrollback_empty()`, `assert_cursor_position()`.
Die Doku selbst rät allerdings, das nicht zu übertreiben:
> "it is preferable to write unit tests for widgets directly against the buffer rather than using this backend."
Also: für ein Eigenbau-Widget wie `grid.rs` direkt `Widget::render(area, &mut buf)` aufrufen und den
`Buffer` prüfen — dafür braucht man gar kein `Terminal`.
**Snapshot-Tests** mit `insta` sind das einzige offizielle Test-Rezept
(<https://ratatui.rs/recipes/testing/snapshots/>): feste Terminalgröße (80x20) für Reproduzierbarkeit,
`cargo insta review` zum Abnehmen. Einschränkung auf der Seite: *"Asserting with color is not supported
as of now"* (ratatui-Issue #1402).
**Korrektur zu einer verbreiteten Annahme:** `Terminal::with_backend` existiert in 0.30.2 **nicht**.
Die Konstruktoren heißen `Terminal::new` und `Terminal::with_options`
(<https://docs.rs/ratatui/0.30.2/ratatui/struct.Terminal.html>).
---
## 8. Mitgelieferte Widgets vs. Eigenbau
### 8.1 Was ratatui 0.30.2 mitbringt
Quelle: <https://docs.rs/ratatui/latest/ratatui/widgets/index.html>
**Structs (31):** `Axis`, `Bar`, `BarChart`, `BarGroup`, `Block`, `Borders`, `Cell`, `Chart`,
`Clear`, `Dataset`, `Dimmed`, `Fill`, `Gauge`, `LineGauge`, `List`, `ListItem`, `ListState`,
`Padding`, `Paragraph`, `RatatuiLogo`, `RatatuiMascot`, `Row`, `Scrollbar`, `ScrollbarState`,
`Shadow`, `Sparkline`, `SparklineBar`, `Table`, `TableState`, `Tabs`, `Wrap`.
**Enums (11):** `BorderType`, `GraphType`, `HighlightSpacing`, `LegendPosition`, `ListDirection`,
`MascotEyeColor`, `RatatuiLogoSize`, `RenderDirection`, `ScrollDirection`, `ScrollbarOrientation`,
`TitlePosition`.
**Traits:** `Widget`, `StatefulWidget`, `BlockExt`, `CellEffect`; hinter dem Feature
`unstable-widget-ref` zusätzlich `WidgetRef`, `StatefulWidgetRef`, `FrameExt`.
**Stateful sind genau drei:** `List`/`ListState`, `Table`/`TableState`, `Scrollbar`/`ScrollbarState`.
Rendern über `frame.render_stateful_widget(w, area, &mut state)`.
**Feature-gated:** `widgets::calendar` (`Monthly`) braucht das Feature `widget-calendar`.
Neu seit 0.29 und für rustkeep interessant: `Fill` (Fläche mit wiederholtem Symbol füllen),
`Shadow` + `Dimmed` + Trait `CellEffect` (Schatten unter Popups).
**Es gibt kein Popup-Widget.** Popup = `Clear` + `Block`, siehe 5.
### 8.2 Was rustkeep direkt nutzen kann
| Bildschirm | Mitgeliefert |
|---|---|
| Hauptmenü | `List` + `ListState`, oder `Paragraph` mit `tui-big-text` für den Titel |
| Inventar | `Table` + `TableState`, `Scrollbar` |
| Shop | `Table` (zwei nebeneinander: Ware / eigenes Inventar) |
| Questlog | `List` + `Paragraph` für Details |
| Charakterbogen | `Paragraph` / `Table`, `Gauge` oder `LineGauge` für HP/Stamina/EXP |
| Kampf | `Gauge` für HP-Leisten, `List` für das Aktionsmenü, `Paragraph` für das Kampflog |
| Rahmen/Titel überall | `Block` mit `Borders` |
| Popup | `Clear` + `Block` (+ optional `Shadow`) |
Damit ist alles außer dem Dungeon-Grid abgedeckt.
### 8.3 Dungeon-Grid: Eigenbau, und zwar so
**Verifiziert negativ:** Es gibt kein Grid-/Tilemap-Widget — weder in `ratatui::widgets` (vollständige
Liste oben), noch in der offiziellen Drittanbieter-Liste
(<https://ratatui.rs/showcase/third-party-widgets/>: `ratatui-image`, `ratatui-textarea`,
`throbber-widgets-tui`, `tui-big-text`, `tui-checkbox`, `tui-logger`, `tui-menu`, `tui-nodes`,
`tui-piechart`, `tui-scrollview`, `tui-term`, `tui-tree-widget`, `tui-widget-list` — kein Tilemap).
Drei Optionen, in dieser Reihenfolge zu bewerten:
1. **Custom `impl Widget` mit direktem Buffer-Zugriff — die Empfehlung.**
```rust
pub trait Widget { fn render(self, area: Rect, buf: &mut Buffer); }
```
(<https://ratatui.rs/recipes/widgets/custom/>)
Im `render` pro Kachel `buf[(x, y)].set_char(sym).set_style(style)`. Eine Terminalzelle = eine
Kachel. Keine Allokation pro Frame, volle Kontrolle über Vorder-/Hintergrundfarbe und Modifier
(nützlich für Sichtfeld: gesehen aber nicht sichtbar = `DIM`). Ideal für ein `@`-und-`#`-Grid.
Camera-/Viewport-Logik (welcher Ausschnitt bei großer Karte) gehört ebenfalls hierhin.
2. **`Paragraph` aus `Line`s von `Span`s.** Eine `Line` pro Kartenzeile, `Span` pro Farbwechsel.
Einfacher zu schreiben, alloziert aber pro Frame. Gut als erster Wurf, später auf (1) umstellen.
3. **`Canvas` — nicht für Kacheln geeignet.** `Canvas` ist eine *Vektor*-Zeichenfläche mit
Float-Koordinatenraum (`x_bounds`/`y_bounds`), die auf Sub-Zell-Marker rastert. Mitgelieferte
Shapes: `Circle`, `Line`, `FilledLine`, `Rectangle`, `Map`, `Points`, `Label`
(<https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html>). Man kann kein `@` auf eine
Sub-Zell-Position setzen; `Label` ist der einzige Text-Ausweg. **Sinnvoll für eine Minimap,
Sichtfeldkegel oder Sichtlinien-Strahlen — nicht für die Hauptkarte.**
Die verfügbaren `symbols::Marker`
(<https://docs.rs/ratatui/latest/ratatui/symbols/enum.Marker.html>), falls doch Canvas:
| Variante | Auflösung pro Zelle | Anmerkung |
|---|---|---|
| `Dot` (`•`), `Block` (`█`), `Bar` (`▄`), `Custom(char)` | 1×1 | |
| `HalfBlock` | 2×2 | portabelste Wahl |
| `Quadrant` | 2×2 | wie HalfBlock, aber ohne sichtbare Bänder zwischen Zellen |
| `Braille` | 2×4 | höchste klassische Dichte, sichtbare Lücken |
| `Sextant` | 2×3 | braucht Font-Unterstützung |
| `Octant` | 2×4 | 2×4 ohne Bänder, neueste Variante, braucht Font-Unterstützung |
### 8.4 Drittanbieter-Crates, Versionen geprüft (2026-08-11)
| Crate | Version | Datum | Zweck |
|---|---|---|---|
| `ratatui-image` | 11.0.6 | 2026-06-25 | Bilder via sixel/kitty/iTerm2/Halbblöcke |
| `ratatui-textarea` | 0.9.2 | 2026-06-12 | Texteingabe-Widget (Nachfolger von `tui-textarea`) |
| `tui-popup` | 0.7.6 | 2026-06-14 | Popup-Helfer, Repo `ratatui/tui-widgets` |
| `tui-widgets` | 0.7.10 | 2026-06-18 | Sammel-Crate: Big Text, Box Text, Cards, **Popup**, Prompts, Scrollview u. a. |
| `tui-widget-list` | 0.15.3 | 2026-07-18 | Liste aus beliebigen Widgets |
Weitere Crates aus der Showcase (`tui-scrollview`, `tui-tree-widget`, `tui-term`, `tui-logger`,
`tui-big-text`, `throbber-widgets-tui`, `tui-menu`, `tui-nodes`, `tui-piechart`, `tui-checkbox`) —
**Versionen nicht einzeln geprüft**.
Meine Einschätzung: rustkeep braucht davon zunächst **keines**. `tui-popup` ersetzt ~20 Zeilen
Eigenbau, `ratatui-textarea` wird erst relevant, wenn Freitext-Eingabe dazukommt (Charaktername).
Jede Abhängigkeit hier ist eine, die bei ratatui-Major-Bumps nachziehen muss.
---
## 9. Terminal-Wiederherstellung bei Panic
### 9.1 ratatui macht das inzwischen selbst
Die API liegt im Modul `ratatui::init` und ist im Crate-Root re-exportiert
(<https://docs.rs/ratatui/latest/ratatui/init/index.html>):
| Funktion | Alt-Screen | Raw-Mode | Fehler | Zweck |
|---|---|---|---|---|
| `run(f)` | ✓ | ✓ | räumt automatisch auf | **die empfohlene Standardform** |
| `init()` | ✓ | ✓ | paniced | normale Vollbild-Apps |
| `try_init()` | ✓ | ✓ | `Result` | dito, mit Fehlerbehandlung |
| `init_with_options()` | ✗ | ✓ | paniced | eigener Viewport |
| `try_init_with_options()` | ✗ | ✓ | `Result` | dito |
| `restore()` | — | — | schreibt nach stderr, ignoriert | Teardown |
| `try_restore()` | — | — | `Result` | Teardown mit Fehlerbehandlung |
`DefaultTerminal = Terminal<CrosstermBackend<Stdout>>`
(<https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.html>).
`pub fn run<F, R>(f: F) -> R where F: FnOnce(&mut DefaultTerminal) -> R`
(<https://docs.rs/ratatui/latest/ratatui/fn.run.html>).
**Alle Init-Funktionen installieren einen Panic-Hook.** Modul-Doku: *"All initialization functions
install a panic hook that automatically restores the terminal state before panicking."* Die
Implementierung (<https://github.com/ratatui/ratatui/blob/main/ratatui/src/init.rs>):
```rust
fn set_panic_hook() {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore();
hook(info);
}));
}
```
**Für rustkeep genügt also `ratatui::run(...)`.** Kein eigener Hook nötig.
### 9.1a Drei Fallen dabei
**(1) Reihenfolge.** ratatui wickelt den *aktuellen* Hook ein. Wer `color_eyre`, `better-panic` oder
`human-panic` **nach** `init()` installiert, wirft ratatuis Restore weg. Doku wörtlich:
> "Ensure that this method is called **after** your app installs any other panic hooks to ensure the terminal is restored before the other hooks are called."
(<https://docs.rs/ratatui/latest/ratatui/fn.init.html>). Also: `color_eyre::install()?` zuerst, dann
`ratatui::run(...)`.
**(2) `Drop` stellt das Terminal NICHT wieder her.** Der gesamte `Drop`-Body von `Terminal<B>` zeigt
nur den Cursor wieder an (<https://github.com/ratatui/ratatui/blob/main/ratatui-core/src/terminal.rs>):
```rust
fn drop(&mut self) {
if self.hidden_cursor {
if let Err(err) = self.show_cursor() { /* eprintln */ }
}
}
```
Die Struct-Doku bestätigt: *"`Terminal` itself focuses on rendering and does not implicitly change
those modes."* Ohne `restore()` (oder `run()`) bleibt das Terminal im Raw-Mode zurück.
**(3) `Terminal::new()` direkt zu benutzen umgeht den Hook.** Doku: *"unlike `ratatui::init`, this does
not install a panic hook … the terminal may be left in an unusable state."*
### 9.2 Die Rezeptseiten sind veraltet — Warnung
Gleich drei Seiten stammen aus der Zeit vor `ratatui::init` und erwähnen den eingebauten Hook **nicht**:
- <https://ratatui.rs/recipes/apps/panic-hooks/> — handgebautes `take_hook`/`set_hook`
- <https://ratatui.rs/recipes/apps/color-eyre/> — `color_eyre::install()?` plus dasselbe Wrap-Muster
- <https://ratatui.rs/recipes/apps/better-panic/> — benutzt eine überholte `Tui`-Struct und `libc::EXIT_FAILURE`
Der Kern der ersten Seite:
```rust
pub fn init_panic_hook() {
let original_hook = take_hook();
set_hook(Box::new(move |panic_info| {
let _ = restore_tui();
original_hook(panic_info);
}));
}
```
mit *"you want to take the original panic hook and execute it after cleaning up the terminal"* und
*"intentionally ignore errors here since we're already in a panic"*.
Das ist nicht falsch, nur überflüssig, wenn man `ratatui::init()`/`run()` benutzt. Wer eigene
Hooks (color-eyre, better-panic, human-panic) installieren will, muss das **vor** `ratatui::init()`
tun — siehe Zitat oben.
### 9.3 Bezug zur Fehlerstrategie aus map.md
map.md legt fest: „`Result` statt `panic!` — kaputte Mod-TOMLs sind der Normalfall". Das passt
zusammen: Panics sollen echte Bugs sein, und für die reicht der eingebaute Hook. Fürs Laden gilt
weiter `Result`. Konkret bedeutet das aber auch, dass `src/items.rs` heute mit
`unwrap_or_else(|| panic!(...))` gegen die eigene Regel verstößt (Ticket 04).
---
## 10. Windows-Fallstricke
rustkeep wird auf Windows 11 entwickelt. Das ist gut — die meisten dieser Fallen fallen dann sofort
auf statt erst beim ersten Windows-Nutzer. Ein paar sind trotzdem still.
### 10.1 Doppelte Key-Events — der wichtigste Stolperstein
Auf Windows liefert crossterm sowohl Press- als auch Release-Events; auf Linux und macOS nur Press.
Wer das nicht filtert, bewegt sich zwei Felder pro Tastendruck.
**Ursache belegt:** crossterm-PR <https://github.com/crossterm-rs/crossterm/pull/745>, ausgeliefert in
crossterm **0.26.0** (CHANGELOG: *"Add key release event for windows. (#745)"*).
ratatui-FAQ, Eintrag *"Why am I getting duplicate key events on Windows?"*
(<https://ratatui.rs/faq/>):
> "on Windows, when using Crossterm, this will send the same `Event::Key(e)` twice; one for … `KeyEventKind::Press` and one for … `KeyEventKind::Release`. On MacOS and Linux only `KeyEventKind::Press` … is generated."
und ratatui-Issue #347 (geschlossen, <https://github.com/ratatui/ratatui/issues/347>):
> "To avoid registering an event twice, ensure that you check the `kind` field of the `KeyEvent`."
Weitere offene Issues dazu: crossterm
[#772](https://github.com/crossterm-rs/crossterm/issues/772) (reproduziert in CMD, PowerShell **und**
Windows Terminal), [#797](https://github.com/crossterm-rs/crossterm/issues/797),
[#778](https://github.com/crossterm-rs/crossterm/pull/778) (Vorschlag, das opt-in zu machen),
[#752](https://github.com/crossterm-rs/crossterm/issues/752) (spurioses `Enter`-Release beim Start);
ratatui [#1810](https://github.com/ratatui/ratatui/issues/1810),
[#1062](https://github.com/ratatui/ratatui/issues/1062).
**Lösung, eine Zeile.** Klassisch:
```rust
if key.kind == KeyEventKind::Press { /* ... */ }
```
Seit crossterm 0.29 eleganter über die Helfer auf `Event`
(<https://docs.rs/crossterm/latest/crossterm/event/enum.Event.html>):
`is_key_press()`, `is_key_release()`, `is_key_repeat()`, `as_key_press_event()`,
`as_key_event()`, `as_mouse_event()`, `as_paste_event()`, `as_resize_event()`.
```rust
if let Some(key) = event::read()?.as_key_press_event() { ... }
```
Das ist auch die Form, die ratatuis eigene 0.30-Doku im `run()`-Beispiel benutzt.
**Achtung, feiner Punkt:** Strikt auf `Press` zu filtern verwirft auch `KeyEventKind::Repeat`. Wer
gehaltene Tasten mit Autorepeat will (Laufen im Dungeon-Grid durch Halten der Pfeiltaste!), braucht
`matches!(kind, KeyEventKind::Press | KeyEventKind::Repeat)`. Für rustkeep ist das eine echte
Design-Frage, kein Detail: rundenbasiert heißt eigentlich „ein Zug pro Tastendruck", also
wahrscheinlich nur `Press`.
gitui filtert Key-Release im Input-Thread aus demselben Grund heraus (`src/input.rs`).
### 10.2 ANSI vs. WinAPI — und die Asymmetrie zwischen Aus- und Eingabe
crossterm 0.29 (<https://docs.rs/crossterm/latest/crossterm/>):
> "supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested see Tested Terminals for more info)"
> "Linux and Windows 10 systems support ANSI escape codes."
Für ältere Systeme fällt crossterm auf WinAPI-Aufrufe zurück (ebd.). Die Weiche liegt in
`Command::is_ansi_code_supported()`
(<https://github.com/crossterm-rs/crossterm/blob/master/src/command.rs>), die Erkennung in
<https://github.com/crossterm-rs/crossterm/blob/master/src/ansi_support.rs> (setzt
`ENABLE_VIRTUAL_TERMINAL_PROCESSING`, cached das Ergebnis). 256-Farben und RGB gibt es **erst ab
Windows 10** (crossterm-README).
**Die Asymmetrie, die man kennen muss:** Die *Ausgabe* läuft über ANSI, die *Eingabe* aber immer über
die Console-API. crossterm-PR <https://github.com/crossterm-rs/crossterm/pull/1030> (offen):
> "The console strips VT escape sequences (including bracketed paste markers) from these events."
Daraus folgt fast alles in 10.5.
### 10.3 `enable_raw_mode` verhält sich auf Windows anders
Die Doku (<https://docs.rs/crossterm/latest/crossterm/terminal/fn.enable_raw_mode.html>) schweigt zu
Plattformunterschieden, der Quellcode nicht:
- **Unix:** sichert `termios` in einem prozessglobalen `Mutex<Option<Termios>>` und stellt exakt
diesen Zustand wieder her. Idempotent.
- **Windows:** speichert **keinen** Zustand. `enable_raw_mode` löscht
`ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT` am Console-Input-Handle;
`disable_raw_mode()` ODERt diese drei blind wieder hinein. Das ist **keine echte
Wiederherstellung** — das Handle ist über die ganze Console-Session geteilt und wird an
Kindprozesse vererbt. `is_raw_mode_enabled()` fragt unter Windows den Live-Zustand der Console ab,
ein Kindprozess kann das Ergebnis also verändern.
- Windows-Raw-Mode setzt `ENABLE_VIRTUAL_TERMINAL_INPUT` **nicht**.
Praktisch für rustkeep: `ratatui::run()` benutzen und nicht darauf bauen, dass die Console nach dem
Beenden exakt so ist wie vorher.
### 10.4 Zeichen, Schriftart und Doppelbreite
ratatui-FAQ, Eintrag *"Some characters appear to be missing / look weird"* (<https://ratatui.rs/faq/>):
Empfohlen wird eine Nerd Font; *"If your font doesn't support such features, it may display a white
square (□) or a replacement character (<28>)."* Betroffen sind Box-Drawing-Zeichen, Braille-Muster und
Icons. Die Schriftgröße kann ratatui nicht steuern — das macht der Terminalemulator; die App soll
stattdessen sauber auf Resize reagieren (ebd.).
Belegte Windows-Probleme:
- **Spaltenversatz bei doppelbreiten Glyphen, am schlimmsten in conhost.**
ratatui-Issue [#2651](https://github.com/ratatui/ratatui/issues/2651) (offen), wörtlich:
*"After printing a double-width grapheme at `x`, the terminal cursor sits at `x + 2`, but the backend
believes it is at `x + 1`"* — mit ausdrücklicher Nennung von **Windows conhost / Command Prompt**.
Symptom: *"Panel borders shift outside their intended boundaries"*. Verwandt:
[#2652](https://github.com/ratatui/ratatui/issues/2652),
[#2526](https://github.com/ratatui/ratatui/issues/2526),
[#2431](https://github.com/ratatui/ratatui/issues/2431),
[#1271](https://github.com/ratatui/ratatui/issues/1271).
- **Emoji-Eingabe kaputt:** crossterm-Issue
[#1072](https://github.com/crossterm-rs/crossterm/issues/1072) (offen) — Surrogate-Paare aus dem
Windows-Emoji-Panel werden zerstört, weil seit #745 auch Key-Up-Records weitergereicht werden.
Auch [#1008](https://github.com/crossterm-rs/crossterm/issues/1008).
- **Codepage:** crossterm-Issue [#607](https://github.com/crossterm-rs/crossterm/issues/607)
(offen) — `▄` wird im Windows-Terminal von VS Code verstümmelt; Abhilfe ist
`SetConsoleOutputCP(CP_UTF8)`. **crossterm setzt die Codepage nicht für einen.**
- **`unicode-width`-Unruhe:** ratatui hat die Crate wiederholt gepinnt und wieder entpinnt, weil
Minor-Bumps Zeichenbreiten ändern ([#1403](https://github.com/ratatui/ratatui/pull/1403) pinnte auf
0.2.0, [#2482](https://github.com/ratatui/ratatui/issues/2482) hob die Schranke auf). Aktuell:
`unicode-width = ">=0.2.0"`, ohne Feature-Flag.
**Konsequenz für das Dungeon-Grid:**
- Bei ASCII bleiben, wo es geht (`#`, `.`, `@`, `+`, `<`, `>`), Farbe statt exotischer Glyphen.
- Keine Emoji in der Karte, in Rahmen oder in Item-Namen. Eine Kachel muss genau eine Zelle breit sein,
sonst verschiebt sich die ganze Zeile.
- `Quadrant`/`Sextant`/`Octant`-Marker (8.3) brauchen Font-Unterstützung; `HalfBlock` ist die sichere
Wahl.
- Falls doch Unicode-Rahmen: Codepage auf UTF-8 setzen.
### 10.5 Was auf Windows schlicht nicht funktioniert
Quelle für die Tabelle: <https://github.com/crossterm-rs/crossterm/blob/master/src/event.rs> und
`src/terminal/sys/windows.rs`.
| Fähigkeit | conhost (Legacy) | Windows Terminal | Mechanismus |
|---|---|---|---|
| Mouse Capture | ✅ | ✅ | WinAPI, erzwungen (`is_ansi_code_supported → false`) |
| Bracketed Paste (Kommando) | ❌ `Unsupported` | ✅ liefert `Ok` | ANSI, wenn VT verfügbar |
| `Event::Paste` feuert wirklich | ❌ | ❌ | `ReadConsoleInput` entfernt die Marker |
| Kitty-Keyboard-Protokoll | ❌ | ❌ | fest verdrahtet `Err(Unsupported)` / `Ok(false)` |
| `window_size()` in Pixeln | ❌ | ❌ | *"not implemented for the Windows API"* |
- **Kitty-Protokoll:** `supports_keyboard_enhancement()` ist unter Windows dokumentiert als
*"This always returns Ok(false) on Windows."*, und `PushKeyboardEnhancementFlags` liefert **immer**
`Err(Unsupported)` — auch im Windows Terminal. Folgen in freier Wildbahn:
crossterm-Issue [#842](https://github.com/crossterm-rs/crossterm/issues/842), das mitgelieferte
Beispiel bricht mit Exit-Code 1 ab. **Immer mit
`terminal::supports_keyboard_enhancement().unwrap_or(false)` absichern.** Relevant für rustkeep,
sobald man Tastenkombis wie `Shift+Pfeil` oder Key-Release unterscheiden will — geht auf Windows
nicht.
- **Bracketed Paste:** [#737](https://github.com/crossterm-rs/crossterm/issues/737) (offen) —
*"just yields a series of Event::char"*; [#962](https://github.com/crossterm-rs/crossterm/issues/962)
(offen) — Einfügen mit Zeilenumbrüchen hängt `Enter` ein falsches `CONTROL`-Modifier an.
- **Maus-Teardown-Reihenfolge (undokumentiert):** `enable_mouse_capture` setzt den Console-Mode
**absolut** und schaltet dabei nebenbei Line-Input/Echo aus. `disable_mouse_capture` stellt einen
einmal pro Prozess genommenen Snapshot wieder her — der typischerweise Line-Input/Echo wieder
einschaltet und damit **`enable_raw_mode()` still rückgängig macht**. Also: erst
`DisableMouseCapture`, dann `disable_raw_mode()`. Verwandt:
[#825](https://github.com/crossterm-rs/crossterm/issues/825).
Für rustkeep irrelevant, solange keine Maus benutzt wird — und das spricht dafür, es dabei zu
belassen.
### 10.6 Windows 7 und `underline-color`
Der einzige Windows-Vorbehalt in ratatuis Feature-Doku
(<https://docs.rs/ratatui/latest/ratatui/#crate-features>): das Default-Feature `underline-color`
*"is not supported on Windows 7"*. Ursprung: ratatui-Issue
[#555](https://github.com/ratatui/ratatui/issues/555) — ein bedingungsloses `SetUnderlineColor`
erzeugte `"SetUnderlineColor not supported by winapi."` und blockierte **das gesamte Rendering**.
Wer Windows 7 unterstützen will, muss das Feature abschalten. Die Website-Seite
<https://ratatui.rs/installation/feature-flags/> erwähnt das nicht — docs.rs ist maßgeblich.
Für rustkeep: irrelevant, solange Windows 7 kein Ziel ist. Falls doch, ist es eine Zeile in
`Cargo.toml`.
### 10.7 Sonstiges
- **Mehrere crossterm-Versionen im Graph** — plattformunabhängig, aber siehe 1.2.
- **Buffer-Panics:** Die FAQ hat einen Eintrag *"How do I avoid panics due to out of range calls on the
Buffer?"* (<https://ratatui.rs/faq/>). Relevant für das Eigenbau-Grid-Widget: beim direkten
Buffer-Schreiben immer gegen `area` clippen, sonst paniced ein zu kleines Terminal die App.
- **stdout vs. stderr:** Eigener FAQ-Eintrag. Für rustkeep irrelevant (kein Pipe-Betrieb), aber gut zu
wissen, falls Logging dazukommt.
### 10.8 Windows-Checkliste in Kurzform
1. `ratatui::run(...)` benutzen; falls `color_eyre`, dann `install()?` **davor**.
2. Auf jedem Key-Event `KeyEventKind::Press` filtern (ggf. `| Repeat`). Nicht verhandelbar.
3. `PushKeyboardEnhancementFlags` nie ungeprüft aufrufen.
4. Sich nicht auf `Event::Paste` verlassen.
5. Teardown-Reihenfolge: `DisableMouseCapture` → `disable_raw_mode()`. Besser: gar keine Maus.
6. Bei Unicode-Rahmen die Console-Codepage selbst auf UTF-8 setzen.
7. Im Grid ASCII/BMP statt Emoji — conhost verschiebt sonst Spalten.
8. Nicht auf `Drop` von `Terminal` bauen; das zeigt nur den Cursor wieder an.
---
## 11. Was ich NICHT verifizieren konnte
Explizit als ungeprüft markiert, statt geraten:
1. **Versionen von 10 der 15 Drittanbieter-Widget-Crates** (8.4) — nur `ratatui-image`,
`ratatui-textarea`, `tui-popup`, `tui-widgets` und `tui-widget-list` sind geprüft.
2. **Ob ratatui irgendwo doch `read()` vs. `poll()` vergleicht.** Geprüft wurden Crate-Root,
`/concepts/rendering/`, `/concepts/event-handling/` und der `/recipes/apps/`-Index — kein
vollständiger Site-Crawl.
3. **Ob television/yazi in nicht geöffneten Untermodulen doch `Clear` benutzen.**
4. **Ein ratatui-basiertes Roguelike als direktes Vorbild** existiert nach meiner Suche nicht;
ich kann nicht ausschließen, dass es eines gibt, das schlecht auffindbar ist.
5. **Es gibt keine pauschale Regel „Windows Terminal zeichnet Box-Drawing korrekt, conhost nicht".**
Keine Primärquelle sagt das so. Die conhost-Korruption in 10.4 ist eine Folge der
Breitenberechnung, kein eigener Defekt von conhost.
6. **crossterm hat keinen zusammenfassenden „Known Limitations"-Abschnitt.** Die `Unsupported`-Fehler
in 10.5 sind nur im Quellcode auffindbar, nicht in der Doku — was heißt, dass diese Liste
unvollständig sein kann.
7. **Die crossterm-README widerspricht der `Cargo.toml`** bei der Feature-Tabelle. Maßgeblich sind
`Cargo.toml` und docs.rs.
---
## 12. Quellen
### Offizielle ratatui-Dokumentation
- Application Patterns (Übersicht) — <https://ratatui.rs/concepts/application-patterns/>
- The Elm Architecture — <https://ratatui.rs/concepts/application-patterns/the-elm-architecture/>
- Component Architecture — <https://ratatui.rs/concepts/application-patterns/component-architecture/>
- Flux Architecture — <https://ratatui.rs/concepts/application-patterns/flux-architecture/>
- Rendering (Immediate Mode) — <https://ratatui.rs/concepts/rendering/>
- Event Handling (Konzept) — <https://ratatui.rs/concepts/event-handling/>
- Popups / Overwrite Regions — <https://ratatui.rs/recipes/render/overwrite-regions/>
- Custom Widgets — <https://ratatui.rs/recipes/widgets/custom/>
- Terminal and Event Handler (async) — <https://ratatui.rs/recipes/apps/terminal-and-event-handler/>
- Setup Panic Hooks (veraltet, s. 9.2) — <https://ratatui.rs/recipes/apps/panic-hooks/>
- color_eyre Error Hooks (veraltet) — <https://ratatui.rs/recipes/apps/color-eyre/>
- Better Panic Hooks (veraltet) — <https://ratatui.rs/recipes/apps/better-panic/>
- Backend-Vergleich (Windows-Zweig) — <https://ratatui.rs/concepts/backends/comparison/>
- Testing / Snapshots — <https://ratatui.rs/recipes/testing/snapshots/>
- Counter-App-Tutorial — <https://ratatui.rs/tutorials/counter-app/basic-app/>
- JSON-Editor, Closing Thoughts — <https://ratatui.rs/tutorials/json-editor/closing-thoughts/>
- Component-Template, Projektstruktur — <https://ratatui.rs/templates/component/project-structure/>
- Beispiel demo2 — <https://ratatui.rs/examples/apps/demo2/>
- Beispiel popup — <https://ratatui.rs/examples/apps/popup/>
- FAQ — <https://ratatui.rs/faq/>
- App-Showcase — <https://ratatui.rs/showcase/apps/>
- Third-Party-Widgets — <https://ratatui.rs/showcase/third-party-widgets/>
### API-Dokumentation
- ratatui Crate-Root und Feature-Liste — <https://docs.rs/ratatui/latest/ratatui/>
- Modul `ratatui::init` — <https://docs.rs/ratatui/latest/ratatui/init/index.html>
- `ratatui::init` — <https://docs.rs/ratatui/latest/ratatui/fn.init.html>
- `ratatui::run` — <https://docs.rs/ratatui/latest/ratatui/fn.run.html>
- `DefaultTerminal` — <https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.html>
- `Terminal` — <https://docs.rs/ratatui/0.30.2/ratatui/struct.Terminal.html>
- `TestBackend` — <https://docs.rs/ratatui/latest/ratatui/backend/struct.TestBackend.html>
- Backend-Übersicht (Termion nicht auf Windows) — <https://docs.rs/ratatui/latest/ratatui/backend/index.html>
- `widgets` — <https://docs.rs/ratatui/latest/ratatui/widgets/index.html>
- `widgets::canvas` — <https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html>
- `symbols::Marker` — <https://docs.rs/ratatui/latest/ratatui/symbols/enum.Marker.html>
- `layout::Rect` — <https://docs.rs/ratatui/latest/ratatui/layout/struct.Rect.html>
- ratatui-Features/Dependencies — <https://docs.rs/crate/ratatui/latest/features>
- ratatui-crossterm-Features (crossterm_0_29 ist Default) — <https://docs.rs/crate/ratatui-crossterm/latest/features>
- crossterm Crate-Root — <https://docs.rs/crossterm/latest/crossterm/>
- `crossterm::event::Event` — <https://docs.rs/crossterm/latest/crossterm/event/enum.Event.html>
- `crossterm::terminal::enable_raw_mode` — <https://docs.rs/crossterm/latest/crossterm/terminal/fn.enable_raw_mode.html>
### Versionen
- crates.io ratatui — <https://crates.io/api/v1/crates/ratatui>
- crates.io crossterm — <https://crates.io/api/v1/crates/crossterm>
- ratatui CHANGELOG — <https://raw.githubusercontent.com/ratatui/ratatui/main/CHANGELOG.md>
- ratatui workspace Cargo.toml (MSRV, crossterm-Version, termion-Ausschluss) — <https://github.com/ratatui/ratatui/blob/main/Cargo.toml>
- crossterm CHANGELOG (unreleased Breaking Changes) — <https://raw.githubusercontent.com/crossterm-rs/crossterm/master/CHANGELOG.md>
- crossterm 0.29 Release — <https://github.com/crossterm-rs/crossterm/releases/tag/0.29>
### Repos und Realcode
- ratatui-Templates — <https://github.com/ratatui/templates>
- `event-driven`-README — <https://github.com/ratatui/templates/blob/main/event-driven/README.md>
- `simple`-Template main.rs — <https://raw.githubusercontent.com/ratatui/templates/main/simple/template/src/main.rs>
- `component`-Trait — <https://raw.githubusercontent.com/ratatui/templates/main/component/template/src/components.rs>
- `ratatui/src/init.rs` (Panic-Hook-Implementierung) — <https://github.com/ratatui/ratatui/blob/main/ratatui/src/init.rs>
- `ratatui-core/src/terminal.rs` (Drop stellt nicht wieder her) — <https://github.com/ratatui/ratatui/blob/main/ratatui-core/src/terminal.rs>
- Forum: Multiple screens in ratatui (joshka) — <https://forum.ratatui.rs/t/multiple-screens-in-ratatui/82>
### Windows-Issues (alle geprüft)
- ratatui #347 doppelte Key-Events — <https://github.com/ratatui/ratatui/issues/347>
- ratatui #1810, #1062 dito — <https://github.com/ratatui/ratatui/issues/1810>, <https://github.com/ratatui/ratatui/issues/1062>
- ratatui #2651 Spaltenversatz bei doppelbreiten Glyphen (conhost) — <https://github.com/ratatui/ratatui/issues/2651>
- ratatui #2652, #2526, #2431, #1271 verwandt — <https://github.com/ratatui/ratatui/issues/2652>
- ratatui #555 `underline-color` bricht Rendering auf Windows 7 — <https://github.com/ratatui/ratatui/issues/555>
- ratatui #1403 / #2482 `unicode-width`-Pinning — <https://github.com/ratatui/ratatui/pull/1403>, <https://github.com/ratatui/ratatui/issues/2482>
- crossterm #745 Ursprung der Key-Release-Events auf Windows — <https://github.com/crossterm-rs/crossterm/pull/745>
- crossterm #772, #797, #778, #752 Folgeissues — <https://github.com/crossterm-rs/crossterm/issues/772>
- crossterm #1072, #1008 Emoji-Eingabe kaputt — <https://github.com/crossterm-rs/crossterm/issues/1072>
- crossterm #607 Codepage / `▄` verstümmelt — <https://github.com/crossterm-rs/crossterm/issues/607>
- crossterm #842, #968 Kitty-Protokoll nicht unterstützt — <https://github.com/crossterm-rs/crossterm/issues/842>
- crossterm #737, #962 Bracketed Paste — <https://github.com/crossterm-rs/crossterm/issues/737>
- crossterm #825 Maus/Raw-Mode-Wechselwirkung — <https://github.com/crossterm-rs/crossterm/issues/825>
- crossterm #1030 Console entfernt VT-Sequenzen aus Eingaben — <https://github.com/crossterm-rs/crossterm/pull/1030>
- crossterm `src/event.rs`, `src/command.rs`, `src/ansi_support.rs` — <https://github.com/crossterm-rs/crossterm/blob/master/src/event.rs>
- chess-tui — <https://github.com/thomas-mauran/chess-tui>
- `src/state/ui_state.rs` — <https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/state/ui_state.rs>
- `src/constants.rs` — <https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/constants.rs>
- `src/ui/main_ui.rs` — <https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/ui/main_ui.rs>
- sxtetris — <https://github.com/shixinhuang99/sxtetris>
- `src/state/focus.rs` — <https://raw.githubusercontent.com/shixinhuang99/sxtetris/main/src/state/focus.rs>
- gitui — <https://github.com/gitui-org/gitui>
- `src/components/mod.rs` — <https://raw.githubusercontent.com/gitui-org/gitui/master/src/components/mod.rs>
- `src/popup_stack.rs` — <https://raw.githubusercontent.com/gitui-org/gitui/master/src/popup_stack.rs>
- `src/app.rs` — <https://raw.githubusercontent.com/gitui-org/gitui/master/src/app.rs>
- Rebels in the Sky — <https://github.com/ricott1/rebels-in-the-sky>
- taskwarrior-tui — <https://github.com/kdheepak/taskwarrior-tui>
- yazi (`yazi-fm/src/root.rs`) — <https://raw.githubusercontent.com/sxyazi/yazi/main/yazi-fm/src/root.rs>
- television — <https://github.com/alexpasmantier/television>
- atuin — <https://github.com/atuinsh/atuin>
- bottom — <https://github.com/ClementTsang/bottom>
---
## 13. Offene Anschlussfragen für den Menschen
Nicht entschieden, bewusst offengelassen:
1. **Ein `Vec<Screen>` für Screens und Popups, oder zwei getrennte Stacks?** sxtetris nimmt einen,
chess-tui nimmt `Pages` + `Option<Popups>`. Einer ist einfacher, zwei sind klarer bei „Popup über
Popup über Screen".
2. **Wo lebt der Listen-/Tabellen-State?** `ListState` gehört nach ratatui-Logik in die UI. Wenn er
pro Screen gehalten wird, muss er beim `pop()` verworfen oder aufbewahrt werden — das ist eine
spürbare UX-Entscheidung (kehrt man ins Inventar zurück, steht der Cursor wieder oben?).
3. **Wird `game/` wirklich ratatui-frei bleiben?** Sobald Effekte Farben oder Symbole aus TOML mitbringen
(Ticket 01/02), ist die Versuchung groß, `Color` in `game/` zu ziehen. Alternative: eigene
Farb-/Symbol-Typen in `data/` und Übersetzung in `ui/theme.rs`.
4. **Wann fliegt `print_box` raus?** map.md sagt „sobald ratatui steht". Das betrifft `items.rs`,
`player.rs`, `armor.rs`, `weapon.rs` und deren `print_values`.
5. **Nur `KeyEventKind::Press` oder auch `Repeat`?** (10.1) Rundenbasiert spricht für nur `Press` —
ein Zug pro Tastendruck. Aber ein Dungeon-Grid, das man Feld für Feld antippen muss, fühlt sich
zäh an. Das ist eine UX-Entscheidung, keine technische.
6. **Wird ratatui gleich richtig eingezogen oder erst ein Durchstich?** Der Umbau von `print_box` auf
ratatui berührt jedes bestehende Modul. Ein Prototyp-Ticket, das nur Hauptmenü + Dungeon-Grid
zeigt, würde die Struktur aus 4.2 validieren, bevor sieben Bildschirme darauf gebaut werden.