65 KiB
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 StatefulWidgets 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,updateandrenderfunctions 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
ratatuiapplication. Becauseratatuiis 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
ratatuiproject." … "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— nursrc/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 plusEventHandler, der "spawns a new thread and listens for the events using mpsc channels";ui.rs= das Rendering (dieWidget-Impl fürAppliegt bewusst nicht inapp.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
componentsfolder."
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):
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):
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):
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):
#[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:
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:
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:
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:
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ätzlichesMessage/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 anStatefulWidgetscheitert (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 keinBox<dyn>/Downcasting an. Das offiziellecomponent-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. EinVec<Screen>löst das in ~30 Zeilen (sxtetris), ein einzelnes Feld erzwingt einprevious_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:
// 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.rsgeführt. Popup-Stack getrennt vom Screen-Stack halten — oder beides im selbenVec<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
Clearwidget 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):
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 Layouts 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/):
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::pollfunction 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 legtAppinapp.rsund dessenWidget-Impl bewusst inui.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 StatefulWidgets
(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.
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:
-
Custom
impl Widgetmit direktem Buffer-Zugriff — die Empfehlung.pub trait Widget { fn render(self, area: Rect, buf: &mut Buffer); }(https://ratatui.rs/recipes/widgets/custom/) Im
renderpro Kachelbuf[(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. -
ParagraphausLines vonSpans. EineLinepro Kartenzeile,Spanpro Farbwechsel. Einfacher zu schreiben, alloziert aber pro Frame. Gut als erster Wurf, später auf (1) umstellen. -
Canvas— nicht für Kacheln geeignet.Canvasist 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;Labelist 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 HalfBlock2×2 portabelste Wahl Quadrant2×2 wie HalfBlock, aber ohne sichtbare Bänder zwischen Zellen Braille2×4 höchste klassische Dichte, sichtbare Lücken Sextant2×3 braucht Font-Unterstützung Octant2×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):
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):
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 undlibc::EXIT_FAILURE
Der Kern der ersten Seite:
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::Pressand one for …KeyEventKind::Release. On MacOS and Linux onlyKeyEventKind::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
kindfield of theKeyEvent."
Weitere offene Issues dazu: crossterm
#772 (reproduziert in CMD, PowerShell und
Windows Terminal), #797,
#778 (Vorschlag, das opt-in zu machen),
#752 (spurioses Enter-Release beim Start);
ratatui #1810,
#1062.
Lösung, eine Zeile. Klassisch:
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().
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
termiosin einem prozessglobalenMutex<Option<Termios>>und stellt exakt diesen Zustand wieder her. Idempotent. - Windows: speichert keinen Zustand.
enable_raw_modelöschtENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUTam 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_INPUTnicht.
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 (offen), wörtlich:
"After printing a double-width grapheme at
x, the terminal cursor sits atx + 2, but the backend believes it is atx + 1" — mit ausdrücklicher Nennung von Windows conhost / Command Prompt. Symptom: "Panel borders shift outside their intended boundaries". Verwandt: #2652, #2526, #2431, #1271. - Emoji-Eingabe kaputt: crossterm-Issue #1072 (offen) — Surrogate-Paare aus dem Windows-Emoji-Panel werden zerstört, weil seit #745 auch Key-Up-Records weitergereicht werden. Auch #1008.
- Codepage: crossterm-Issue #607
(offen) —
▄wird im Windows-Terminal von VS Code verstümmelt; Abhilfe istSetConsoleOutputCP(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 pinnte auf 0.2.0, #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;HalfBlockist 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.", undPushKeyboardEnhancementFlagsliefert immerErr(Unsupported)— auch im Windows Terminal. Folgen in freier Wildbahn: crossterm-Issue #842, das mitgelieferte Beispiel bricht mit Exit-Code 1 ab. Immer mitterminal::supports_keyboard_enhancement().unwrap_or(false)absichern. Relevant für rustkeep, sobald man Tastenkombis wieShift+Pfeiloder Key-Release unterscheiden will — geht auf Windows nicht. - Bracketed Paste: #737 (offen) —
"just yields a series of Event::char"; #962
(offen) — Einfügen mit Zeilenumbrüchen hängt
Enterein falschesCONTROL-Modifier an. - Maus-Teardown-Reihenfolge (undokumentiert):
enable_mouse_capturesetzt den Console-Mode absolut und schaltet dabei nebenbei Line-Input/Echo aus.disable_mouse_capturestellt einen einmal pro Prozess genommenen Snapshot wieder her — der typischerweise Line-Input/Echo wieder einschaltet und damitenable_raw_mode()still rückgängig macht. Also: erstDisableMouseCapture, danndisable_raw_mode(). Verwandt: #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 — 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
areaclippen, 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
ratatui::run(...)benutzen; fallscolor_eyre, danninstall()?davor.- Auf jedem Key-Event
KeyEventKind::Pressfiltern (ggf.| Repeat). Nicht verhandelbar. PushKeyboardEnhancementFlagsnie ungeprüft aufrufen.- Sich nicht auf
Event::Pasteverlassen. - Teardown-Reihenfolge:
DisableMouseCapture→disable_raw_mode(). Besser: gar keine Maus. - Bei Unicode-Rahmen die Console-Codepage selbst auf UTF-8 setzen.
- Im Grid ASCII/BMP statt Emoji — conhost verschiebt sonst Spalten.
- Nicht auf
DropvonTerminalbauen; das zeigt nur den Cursor wieder an.
11. Was ich NICHT verifizieren konnte
Explizit als ungeprüft markiert, statt geraten:
- Versionen von 10 der 15 Drittanbieter-Widget-Crates (8.4) — nur
ratatui-image,ratatui-textarea,tui-popup,tui-widgetsundtui-widget-listsind geprüft. - 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. - Ob television/yazi in nicht geöffneten Untermodulen doch
Clearbenutzen. - 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.
- 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.
- 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. - Die crossterm-README widerspricht der
Cargo.tomlbei der Feature-Tabelle. Maßgeblich sindCargo.tomlund 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.htmlratatui::run— https://docs.rs/ratatui/latest/ratatui/fn.run.htmlDefaultTerminal— https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.htmlTerminal— https://docs.rs/ratatui/0.30.2/ratatui/struct.Terminal.htmlTestBackend— 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.htmlwidgets::canvas— https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.htmlsymbols::Marker— https://docs.rs/ratatui/latest/ratatui/symbols/enum.Marker.htmllayout::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.htmlcrossterm::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.mdsimple-Template main.rs — https://raw.githubusercontent.com/ratatui/templates/main/simple/template/src/main.rscomponent-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.rsratatui-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-colorbricht 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.rssrc/constants.rs— https://raw.githubusercontent.com/thomas-mauran/chess-tui/main/src/constants.rssrc/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.rssrc/popup_stack.rs— https://raw.githubusercontent.com/gitui-org/gitui/master/src/popup_stack.rssrc/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:
- Ein
Vec<Screen>für Screens und Popups, oder zwei getrennte Stacks? sxtetris nimmt einen, chess-tui nimmtPages+Option<Popups>. Einer ist einfacher, zwei sind klarer bei „Popup über Popup über Screen". - Wo lebt der Listen-/Tabellen-State?
ListStategehört nach ratatui-Logik in die UI. Wenn er pro Screen gehalten wird, muss er beimpop()verworfen oder aufbewahrt werden — das ist eine spürbare UX-Entscheidung (kehrt man ins Inventar zurück, steht der Cursor wieder oben?). - Wird
game/wirklich ratatui-frei bleiben? Sobald Effekte Farben oder Symbole aus TOML mitbringen (Ticket 01/02), ist die Versuchung groß,Coloringame/zu ziehen. Alternative: eigene Farb-/Symbol-Typen indata/und Übersetzung inui/theme.rs. - Wann fliegt
print_boxraus? map.md sagt „sobald ratatui steht". Das betrifftitems.rs,player.rs,armor.rs,weapon.rsund derenprint_values. - Nur
KeyEventKind::Pressoder auchRepeat? (10.1) Rundenbasiert spricht für nurPress— 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. - Wird ratatui gleich richtig eingezogen oder erst ein Durchstich? Der Umbau von
print_boxauf 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.