Ratatui: Getting Started
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Ratatui is a Rust library for building text user interfaces: full-screen programs that draw widgets (lists, tables, paragraphs, charts) into a terminal and react to key presses and mouse input. It is the most widely used TUI library in the Rust ecosystem and the one the rest of this section uses to build the shelf tui catalogue browser. This page is the first of four Ratatui pages: it explains what the library is, how it is split into crates since 0.30, how to start a project, how the terminal is set up and restored, how one frame is drawn, and how to read events and quit cleanly. Layout, the built-in widgets and styling are on Ratatui: Layout, Widgets & Styling; event handling in depth, async code and application architectures on Ratatui: Events, Async & Application Architecture; testing and the surrounding crates on Ratatui: Testing & Ecosystem. The concepts shared by all TUI libraries (the main loop, cell buffers, terminal lifecycle) are on TUI Architecture & Concepts, and the terminal mechanics underneath (raw mode, the alternate screen, escape sequences) on Terminals, TTYs & Pseudo-Terminals and Escape Sequences & Terminal Capabilities. The command-line half of a Rust tool (clap, error reporting, signals) is on Command-Line Programs in Rust.
The page was written against Ratatui 0.30.2 (published 2026-06-19), ratatui-core 0.1.2, ratatui-widgets
0.3.2 and crossterm 0.29.0, with Rust 2024 edition and the minimum supported Rust version declared by the 0.30.2
manifest, 1.88. Ratatui has not reached 1.0, and no 1.0 roadmap is published: each 0.x release may break APIs, so
the project keeps release highlights and a breaking-changes document that you should read before upgrading (linked in
the references). Neither cargo nor rustc is installed on the machine used to write this page, so the Rust examples
were not compiled or run; every function signature, type and feature name in them was checked against the documentation
on docs.rs and the Ratatui repository instead. The ratatui.rs website did not respond while this page was written, so
the website’s tutorials are cited from the repositories and API documentation they derive from. Compile each snippet
before relying on it.
What Ratatui is
Ratatui draws text into a buffer of cells (one character, one foreground colour, one background colour and some
modifiers per cell) and writes the differences between two consecutive buffers to the terminal. It is an
immediate-mode library: there is no widget tree that persists between frames. On every iteration of the loop your
program describes the whole screen again by calling render_widget for each thing that should be visible, and Ratatui
works out which cells changed. Application state (the list of books, the selected row, the search text) lives in your
own structs, not inside the library. The trade-offs of that model against retained-mode toolkits are discussed on
TUI Architecture & Concepts.
Ratatui is a fork of the older tui-rs crate: the Ratatui README states that it was forked in 2023 in order to continue
the development of tui-rs, and credits tui-rs’s author for the design it inherits. Code and tutorials that mention
tui:: imports belong to the old crate; the official documentation has a recipe for migrating from tui-rs.
Ratatui is only a rendering library. It does not read the keyboard: input comes from the terminal backend crate
(crossterm by default), and the program is responsible for the loop that alternates reading events and drawing. It
does not parse command lines either; combine it with clap as described on
Command-Line Programs in Rust so that shelf list stays a one-shot
command and shelf tui starts the full-screen interface.
The crate split of Ratatui 0.30
Until 0.29, ratatui was a single crate. Release 0.30.0 turned the repository into a workspace of smaller crates, so
that widget libraries can depend on a small, stable core and so that the core can build without the standard library
(the release highlights list no_std support for the core and widgets crates, aimed at embedded systems). For an
application the change is mostly invisible, because the ratatui crate re-exports the types you use; it matters when
you write a widget library, choose a backend explicitly, or read the documentation, because items now live in several
crates.
| Crate | Contents | When to depend on it directly |
|---|---|---|
|
The main crate and the recommended entry point for applications. Re-exports the core types and the widgets, and adds the |
Always, in applications |
|
The core traits and types: |
Third-party widget libraries that should not pull in every built-in widget |
|
The built-in widget implementations ( |
Rarely; |
|
The crossterm backend. Re-exported as the default backend. |
Libraries that need the backend type without the whole application crate |
|
The termion backend (Unix only). |
Selected with the |
|
The termwiz backend. |
Selected with the |
|
The backend for the termina terminal library, new in the 0.30 line. |
Selected with the |
|
Macros that reduce boilerplate (for spans, lines, text and layouts). Enabled by the default |
Rarely; use |
The ratatui crate exposes the choices as Cargo features. The defaults, as listed in the 0.30.2 manifest on crates.io,
are all-widgets, crossterm, layout-cache, macros and underline-color. The other features that matter on a first
project are summarised below.
| Feature | Effect |
|---|---|
|
Enables the crossterm backend and the |
|
Pin the crossterm version the backend uses, so that a widget library can stay compatible with an older crossterm. The latest is enabled by default. |
|
Enable another backend. All backend features imply |
|
A cache that speeds up repeated layout computations. Turning off default features also turns it off, which can cost performance. |
|
The |
|
Serialization for styles, layout types and backend types. |
|
Colour conversions with the |
|
Support for targets without native atomic types, for the |
|
Enables the use of terminal scrolling regions in the backends (see the crate documentation for details). |
|
Experimental APIs without stability guarantees. |
Two consequences follow for application authors. First, use ratatui::crossterm; gives access to the exact crossterm
version that Ratatui was built against (the crate documentation lists crossterm as a re-export when the feature is
enabled), which avoids the classic problem of two crossterm versions in one dependency tree, where an Event read with
one version is a different type from the Event of the other. The examples below import crossterm through Ratatui for
that reason. Second, code written for 0.29 mostly keeps compiling because the old paths are re-exported, but the
0.30 breaking-changes document lists changes worth checking: the Backend trait gained an associated Error type and a
required clear_region method, TestBackend now uses Infallible as its error type, Block::title handling changed
(titles are Line values and the block::Title type is gone), layout::Alignment is renamed HorizontalAlignment
(with a compatibility alias), and Flex::SpaceAround now behaves like CSS flexbox.
Creating a project
There are two routes: a template that generates a working program, or a manual cargo add.
From a template
The Ratatui organisation publishes a repository of templates. The README lists six: Hello World, Simple, Simple Async, Event Driven, Event Driven Async and Component. The repository documents the generator command and nothing more; it does not list the files each template produces, so open the generated project to see them.
cargo install --locked cargo-generate
cargo generate ratatui/templates
cargo generate asks for a project name and which template to use. The Ratatui README recommends starting from the
Hello World template, whose program is the smallest complete application: it calls ratatui::init, loops drawing a
frame and waiting for a key press, and calls ratatui::restore before returning. The Simple and Event Driven templates
introduce an App struct, the Component template the component architecture, and the async variants a Tokio event
task; those designs are the subject of
Ratatui: Events, Async & Application Architecture.
Source: Ratatui — templates, Ratatui — README
By hand
For the shelf tool, add the library to a binary crate. The default features already include the crossterm backend, so
no separate crossterm dependency is needed when the code imports it through ratatui::crossterm.
cargo new shelf
cd shelf
cargo add ratatui
The resulting dependency section is:
[package]
name = "shelf"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
[dependencies]
ratatui = "0.30"
Because each 0.x release can contain breaking changes, the version requirement "0.30" accepts 0.30.x updates but not
0.31, which is the behaviour you want from a library without a stable 1.0 release.
Source: Ratatui — crate documentation
The terminal lifecycle: run, init and restore
A TUI changes the terminal’s state and must put it back. On start-up it enables raw mode (so that every key press reaches the program immediately and is not echoed) and enters the alternate screen buffer (so that the user’s shell history is still there when the program exits). On exit, including when the program panics, both must be undone, or the user is left with a terminal that does not echo what they type. The mechanics are explained on Terminals, TTYs & Pseudo-Terminals; Ratatui wraps them in a few functions.
| Item | Signature | Behaviour |
|---|---|---|
|
|
Calls |
|
|
Creates a crossterm backend writing to standard output, enables raw mode, enters the alternate screen and installs a panic hook that restores the terminal before the panic message is printed. Panics if raw mode, the alternate screen or the terminal size cannot be obtained. |
|
|
The same set-up, but returns the failure as an error, for programs that want to report it (for example when standard output is not a terminal). |
|
|
Disables raw mode and leaves the alternate screen. If either step fails the error is printed to standard error and ignored. |
|
the fallible counterpart of |
Returns the error instead of printing it. The signature is not shown in the pages consulted. |
|
|
The terminal type returned by |
All of these require the crossterm feature. Terminal::new and Terminal::with_options give full control when the
defaults do not fit; for example, with_options takes a TerminalOptions value that selects an inline viewport instead
of the alternate screen, which is covered on
Ratatui: Events, Async & Application Architecture.
The smallest program uses run. The closure is the whole application; when it returns, the terminal is restored.
use ratatui::crossterm::event;
fn main() -> std::io::Result<()> {
ratatui::run(|terminal| {
loop {
terminal.draw(|frame| frame.render_widget("shelf: press any key to quit", frame.area()))?;
if event::read()?.is_key_press() {
break Ok(());
}
}
})
}
Source: Ratatui — run, Ratatui — crate quick start
When the program needs its own error type, wants to run set-up before the terminal changes, or wants to print a
result after the terminal is restored, use init and restore explicitly. This is the shape of the Hello World
template:
use ratatui::{DefaultTerminal, Frame, crossterm::event::{self, Event}};
fn main() -> std::io::Result<()> {
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> std::io::Result<()> {
loop {
terminal.draw(render)?;
if matches!(event::read()?, Event::Key(_)) {
break Ok(());
}
}
}
fn render(frame: &mut Frame) {
frame.render_widget("shelf", frame.area());
}
The point of keeping restore outside run is that the result is held in a variable while the terminal is put back,
so an error message printed afterwards appears on the normal screen. An error printed while the alternate screen is
still active disappears when the screen is left.
Panics and other ways out
init installs a panic hook that restores the terminal before the previous hook runs, which is why the documentation
says to call init after any other panic hooks the application installs: the order guarantees that the message of a
color-eyre or human-panic hook is printed on the normal screen and is not lost. What the documentation of run
does not state is what happens to the terminal when the closure panics; the safe reading is that the hook installed by
init does the restoration, and that restore after run returning is the normal path. Hooks are developed further
in Ratatui: Events, Async & Application Architecture.
Signals are a separate matter. In raw mode the terminal driver no longer turns Ctrl+C into SIGINT, because the
ISIG flag is cleared, so Ctrl+C arrives as a key event with the Control modifier and your program decides what it
means. A signal that is delivered from outside (kill, a closing terminal window sending SIGHUP) bypasses the key
handling and is not covered by the panic hook; see POSIX Processes,
Signals & I/O and Windows Console & Processes for the signal and
console-control-event models, and Command-Line Programs in Rust for the
ctrlc and signal-hook crates.
Source: Ratatui — init, Ratatui — restore
Drawing a frame
Terminal::draw is the heart of the library. Its signature is:
pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, <B as Backend>::Error>
where
F: FnOnce(&mut Frame<'_>),
Each call is one render pass, in this order:
-
Autoresize: the terminal compares the backend’s size with the previous one and resizes its buffers if the user resized the window.
-
Render: your closure receives a
Frameand fills an empty buffer through it. Because every pass starts from an empty buffer, the closure must draw everything that should stay visible. -
Flush: the current buffer is compared with the previous one, and only the changed cells are sent to the backend.
-
Cursor: the cursor is shown at the position set with
Frame::set_cursor_position, or hidden if none was set. -
Swap: the two buffers swap roles for the next pass, and the backend’s own output buffer is flushed.
A failing step skips the later ones. try_draw is the variant whose closure may return an error. The documentation says
Ratatui treats a failed pass as fatal for the session, so higher-level code should restore the terminal.
The Frame handed to the closure offers the operations a first application needs.
| Method | Purpose |
|---|---|
|
The rectangle covering the whole terminal. The older |
|
Draws a widget into a region. Widgets are consumed by the call, so build them inside the closure. |
|
Draws a widget whose selection or scroll position lives in a separate state value, such as |
|
Shows the terminal cursor at a cell, for text input widgets. |
|
Direct access to the cell buffer, for custom drawing. |
|
The number of the current frame, starting at zero. |
A &str is itself a widget, which is why frame.render_widget("shelf", frame.area()) works in the quick start. Real
programs wrap text in a Paragraph and surround regions with a Block, as in the next section. draw returns a
CompletedFrame that exposes the buffer that was just rendered; tests use it, as shown on
Ratatui: Testing & Ecosystem.
The cost of a frame is proportional to the number of cells in the terminal for the buffer comparison and to the number
of changed cells for the output. A program that redraws only when something changed (when an event arrived, rather than
in a tight loop) uses almost no processor time while idle; the loop below blocks in event::read and therefore
redraws only after an input event, a property that changes when background tasks need to update the screen, which is
discussed in the events page.
Source: Ratatui — Terminal, Ratatui — Frame
The application loop
Every Ratatui program, whatever its size, repeats the same cycle: draw the state, wait for an event, change the state, and decide whether to continue.
The loop is deliberately written by the application rather than hidden in the library; that is what allows the
different architectures (a single App struct, components, message passing) to be built on the same primitives.
The first shelf application
The shelf tui of this section is a catalogue browser. This first version has a bordered list of books, a status line
and three behaviours: move the selection, toggle whether the selected book is on loan, and quit. It keeps its state in
an App struct, which is the pattern of the Simple template, and draws a vertical layout of a growing list area and a
one-row status line. The layout system (constraints, Flex, nested layouts) is explained on
Ratatui: Layout, Widgets & Styling; here Layout::vertical
with Constraint::Fill(1) and Constraint::Length(1) is enough.
use ratatui::{
DefaultTerminal, Frame,
crossterm::event::{self, Event, KeyCode, KeyEvent},
layout::{Constraint, Layout},
style::{Style, Stylize},
widgets::{Block, List, ListState, Paragraph},
};
struct Book {
title: &'static str,
author: &'static str,
on_loan: bool,
}
struct App {
books: Vec<Book>,
list_state: ListState,
running: bool,
}
impl App {
fn new() -> Self {
let books = vec![
Book { title: "The Left Hand of Darkness", author: "Ursula K. Le Guin", on_loan: false },
Book { title: "A Pattern Language", author: "Christopher Alexander", on_loan: true },
Book { title: "The Dispossessed", author: "Ursula K. Le Guin", on_loan: false },
Book { title: "Gödel, Escher, Bach", author: "Douglas Hofstadter", on_loan: false },
];
Self { books, list_state: ListState::default().with_selected(Some(0)), running: true }
}
fn run(&mut self, terminal: &mut DefaultTerminal) -> std::io::Result<()> {
while self.running {
terminal.draw(|frame| self.render(frame))?;
self.handle_events()?;
}
Ok(())
}
fn render(&mut self, frame: &mut Frame) {
let [list_area, status_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(frame.area());
let rows = self.books.iter().map(|book| {
let state = if book.on_loan { "on loan" } else { "on shelf" };
format!("{:<30} {:<24} {}", book.title, book.author, state)
});
let list = List::new(rows)
.block(Block::bordered().title("shelf tui"))
.highlight_style(Style::new().reversed())
.highlight_symbol("> ");
frame.render_stateful_widget(list, list_area, &mut self.list_state);
let loaned = self.books.iter().filter(|book| book.on_loan).count();
let status = format!("j/k move l loan or return q quit {loaned} of {} on loan", self.books.len());
frame.render_widget(Paragraph::new(status), status_area);
}
fn handle_events(&mut self) -> std::io::Result<()> {
if let Event::Key(key) = event::read()? {
if key.is_press() {
self.on_key(key);
}
}
Ok(())
}
fn on_key(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.running = false,
KeyCode::Char('j') | KeyCode::Down => self.list_state.select_next(),
KeyCode::Char('k') | KeyCode::Up => self.list_state.select_previous(),
KeyCode::Char('l') | KeyCode::Enter => {
if let Some(index) = self.list_state.selected() {
if let Some(book) = self.books.get_mut(index) {
book.on_loan = !book.on_loan;
}
}
}
_ => {}
}
}
}
fn main() -> std::io::Result<()> {
ratatui::run(|terminal| App::new().run(terminal))
}
A few details of the listing are worth spelling out.
-
rendertakes&mut selfbecause rendering a stateful widget updates theListState(it adjusts the scroll offset so the selected row stays visible). The closure passed todrawborrows the wholeAppmutably for the duration of the call, which is allowed becausedrawtakes a closure and does not touch theAppitself. -
ListState::select_nextandselect_previouschange the selected index.with_selected(Some(0))selects the first row so that the highlight is visible at start-up. -
List::newaccepts any iterator whose items convert to list items; aStringdoes. The list consumes the iterator, so the borrow ofself.booksends beforerender_stateful_widgetborrowsself.list_state. -
Block::bordered().title("shelf tui")draws the frame around the list;Style::new().reversed()comes from theStylizetrait and swaps foreground and background for the highlighted row. -
The catalogue is hard-coded. The row text is formatted with fixed column widths, which assumes single-width characters; text with wide or combining characters needs the width handling discussed on Escape Sequences & Terminal Capabilities, and the
Tablewidget, which does the column layout for you, is introduced on the layout and widgets page. -
A
mainthat returnsstd::io::Result<()>prints the error withDebugformatting and exits with status 1 when the loop fails. Programs that follow the exit-code conventions of Streams & Exit Codes should map errors to specific codes afterrestorehas run.
Reading events
The crossterm backend produces events through the ratatui::crossterm::event module (crossterm 0.29). The Event enum
has the variants FocusGained, FocusLost, Key(KeyEvent), Mouse(MouseEvent), Paste(String) and
Resize(u16, u16). Only keys and resizes arrive by default; mouse capture, bracketed paste and focus reporting must be
enabled explicitly, which the events page covers.
Two functions read events:
| Function | Behaviour |
|---|---|
|
Blocks until an event is available. The simplest loop, and the right one for a program that changes only in response to input. |
|
Waits up to |
A KeyEvent has four fields: code (the key, a KeyCode), modifiers (Shift, Control, Alt), kind and state.
Event and KeyEvent have helper methods (is_key_press, is_press, as_key_press_event and their release and
repeat counterparts) that make the common check short.
A polling variant of the shelf loop that also ticks once a second looks like this:
use std::time::Duration;
use ratatui::crossterm::event::{self, Event};
fn handle_events(&mut self) -> std::io::Result<()> {
if event::poll(Duration::from_secs(1))? {
if let Event::Key(key) = event::read()? {
if key.is_press() {
self.on_key(key);
}
}
} else {
self.on_tick();
}
Ok(())
}
Windows, macOS and Linux differences
The one difference that bites every Ratatui newcomer is the key event kind. The crossterm documentation states that
the kind field of a KeyEvent is always set on Windows, while on Unix it is set only when the program enables the
REPORT_EVENT_TYPES keyboard-enhancement flag with PushKeyboardEnhancementFlags. The practical result is that on
Windows a key press produces a press event and, when the key is released, a release event, so a handler that does not
check kind acts twice per keystroke (a counter that jumps by two, a selection that moves two rows). The Ratatui FAQ
mentions duplicate key events on Windows; the cure is the filter used in the listing above, key.is_press(). On macOS
and Linux without the enhancement flags only press events are produced, so the check is harmless, and writing it
always keeps the program identical on every platform.
Other differences at this level:
| Concern | Windows | macOS | Linux |
|---|---|---|---|
Terminal host |
Windows Terminal, conhost; crossterm uses the console API and virtual-terminal sequences |
Terminal.app, iTerm2 and others; escape sequences over a pseudo-terminal |
Any emulator or the virtual console; escape sequences over a pseudo-terminal |
Key |
Always reported: press and release events |
Press only, unless enhancement flags are enabled and supported |
Press only, unless enhancement flags are enabled and supported |
Backends |
crossterm and termwiz; termion does not support Windows |
crossterm, termion, termwiz |
crossterm, termion, termwiz |
Resize |
|
|
|
The terminal-level reasons are covered on Windows Console & Processes and Terminals, TTYs & Pseudo-Terminals. Which backend to choose, and the platform notes for each, are in Ratatui: Events, Async & Application Architecture.
Quitting cleanly
A TUI quits cleanly when four things happen in order: the loop ends, the terminal is restored, any pending error or result is reported on the normal screen, and the process exits with a meaningful status. The patterns in this page achieve that:
-
The quit key sets
runningto false (orbreak`s out of the loop); no code calls `std::process::exitfrom inside the loop, because that would skiprestore. -
ratatui::runrestores on return, or the program callsratatui::restore()after its own loop. -
Errors travel up as
Resultvalues and are printed after restoration. The panic hook installed byinitcovers panics. -
mainreturns after restoration; a non-successExitCodecan be returned from there.
A --help or --version request should never reach ratatui::init: parse the command line first, and enter the
alternate screen only for shelf tui. A program whose standard output is not a terminal (a pipe, a redirected file,
or a continuous-integration job) should refuse to start the interface, because init writes escape sequences to
standard output; use std::io::IsTerminal to check, and try_init to report a failure instead of panicking. The
design reasons are in TUIs vs. CLIs.
Pitfalls
-
Duplicate key events on Windows: always filter on
is_press()(or comparekindwithKeyEventKind::Press). -
Two crossterm versions: a direct
crosstermdependency whose version differs from the one Ratatui uses producesEventvalues of a different type. Import throughratatui::crossterm, or pin the same version. -
Printing while the interface is active:
println!andeprintln!write into the alternate screen and corrupt the display. Log to a file or collect messages in the state and draw them. -
Skipping
restore:std::process::exitandstd::mem::forgetbypass it, and a terminal left in raw mode does not echo input. Typingresetin the shell recovers it. -
Forgetting to redraw: because the buffer is cleared on every pass, a widget that is not rendered in a frame disappears from that frame.
-
Blocking the loop: a call that takes a long time (a network request, a large file read) inside the handler freezes the interface, since drawing and reading share one thread in this design. The async designs in the events page move that work off the loop.
-
Using unstable features: the features whose names start with
unstablehave no compatibility guarantee between 0.x releases. -
Untrusted text: strings read from files or the network may contain escape sequences. Ratatui renders cell contents and does not interpret them as terminal control sequences, but code that writes directly to standard output while the interface is active can; the injection risks are explained on Escape Sequences & Terminal Capabilities.
What is next
The next page, Ratatui: Layout, Widgets & Styling, replaces the
hand-formatted rows of the list with a Table, splits the screen into a list, a details pane and a search box, and
styles them. The shelf application grows into the full catalogue browser over the following pages, and the same screen
is built with other libraries for comparison on Go TUIs with Bubble Tea and
Python TUIs with Textual, Rich & prompt_toolkit.
References
-
Ratatui website (concepts, tutorials, recipes, templates, highlights, FAQ): https://ratatui.rs/
-
Ratatui API documentation: https://docs.rs/ratatui/latest/ratatui/
-
ratatui::run: https://docs.rs/ratatui/latest/ratatui/fn.run.html -
ratatui::init: https://docs.rs/ratatui/latest/ratatui/fn.init.html -
ratatui::try_init: https://docs.rs/ratatui/latest/ratatui/fn.try_init.html -
ratatui::restore: https://docs.rs/ratatui/latest/ratatui/fn.restore.html -
DefaultTerminal: https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.html -
Terminal: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html -
Frame: https://docs.rs/ratatui/latest/ratatui/struct.Frame.html -
List: https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html -
ListState: https://docs.rs/ratatui/latest/ratatui/widgets/struct.ListState.html -
Layout: https://docs.rs/ratatui/latest/ratatui/layout/struct.Layout.html -
Paragraph: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html
-
-
ratatui-core: https://docs.rs/ratatui-core/latest/ratatui_core/ -
ratatui-widgets: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/ -
Ratatui repository and README: https://github.com/ratatui/ratatui
-
Ratatui breaking changes: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
-
Ratatui v0.30.0 release notes: https://github.com/ratatui/ratatui/releases/tag/ratatui-v0.30.0
-
Ratatui templates: https://github.com/ratatui/templates
-
crossterm events: https://docs.rs/crossterm/latest/crossterm/event/
-
crossterm
KeyEvent: https://docs.rs/crossterm/latest/crossterm/event/struct.KeyEvent.html -
crossterm
poll: https://docs.rs/crossterm/latest/crossterm/event/fn.poll.html