Ratatui: Testing & Ecosystem
|
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. |
A full-screen program is hard to test by hand: it takes over the terminal, it redraws constantly and its output is
a grid of styled cells, not a stream of lines. Ratatui makes that easier than most libraries, because every widget
renders into an in-memory Buffer and the library ships a TestBackend that keeps the final buffer in memory
instead of writing escape sequences. This page covers the rest of what a Ratatui project needs once the screen
works: testing the rendering, the keyboard handling and the resize behaviour; recording demonstrations; choosing
third-party widgets and understanding their maintenance status; deciding between Ratatui alone and a framework layer
such as tui-realm; running Ratatui where there is no ordinary terminal; and upgrading a library that has not reached
1.0.
The page was written against Ratatui 0.30.2 (with ratatui-core 0.1.2), crossterm 0.29.0, insta 1.49.0
and cargo-insta 1.49.0, ratatui-textarea 0.9.3, tui-input 0.15.5, tui-tree-widget 0.24.1,
tui-big-text 0.8.10, ratatui-image 11.1.0, tui-realm 4.1.0 and VHS v0.12.1, as listed on crates.io,
docs.rs and GitHub on 2026-10-25. The platforms are Windows, macOS and Linux; the test code in the first half is
platform-independent and runs the same everywhere cargo test runs. Neither cargo nor rustc is installed on the
machine used to write this page, so the Rust examples were not compiled or run: every type, method and macro in them
was checked against the documentation on docs.rs, but compile each snippet before relying on it, and expect to
adjust the expected strings of the snapshot and buffer tests to whatever your first real run prints. The
application itself (state, layout, widgets, the event loop) is built on
Ratatui: Getting Started,
Ratatui: Layout, Widgets & Styling and
Ratatui: Events, Async & Application Architecture; the
concepts shared by all TUI libraries are on
TUI Architecture & Concepts; testing through a real
pseudo-terminal, for every language, is on
Testing Terminal Apps & TUIs. The design rules for command-line
tests (exit codes, streams, golden output) are on Testing CLIs.
Ratatui has no 1.0
Before anything else: Ratatui is a 0.x library. The latest release on crates.io is 0.30.2 (published 2026-06-19), and neither the crate documentation nor the website announces a 1.0 release or a roadmap towards one. In Cargo, that has concrete consequences.
-
Under the semver rules Cargo applies, a 0.x minor release is allowed to break the API. a dependency on
ratatuiwith the requirement0.30means at least 0.30.0 and below 0.31.0, so Cargo will not move you to 0.31 by itself, and moving there is a deliberate edit. -
Ratatui keeps a manually compiled
BREAKING-CHANGES.mdin its repository, with one section per version, and labels the pull requests that break the API. At the time of writing, the file on the main branch already has a section for an unreleased 0.31.0 (theBackendtrait gains cursor save and restore methods), which shows that the next minor version will break something again. -
The contribution guide asks maintainers to deprecate an item before removing it, to wait at least two versions before removing deprecated items, and to put breaking changes in
BREAKING-CHANGES.md. That is a project practice, not a stability guarantee: it does not promise how many releases there will be, or when. -
The same guide says the code must compile on stable Rust and on the minimum supported Rust version (MSRV). The MSRV itself moves between minor versions; the breaking-changes file records 1.86.0 for 0.30.0 and 1.88.0 from 0.30.1.
-
The crate split in 0.30 was introduced partly "to provide better API stability for third-party widget libraries":
ratatui-coreholds the fundamental types and traits and changes more slowly than the main crate. Widget authors can depend on it; application authors can keep depending onratatui.
The practical rule is to treat every Ratatui upgrade as a small migration, to pin the minor version, and to have tests that fail loudly when rendering changes. The rest of this page is mostly about the second half of that sentence, and a section at the end covers the first.
Testing the rendering
The pieces
A Ratatui test needs no terminal at all. Four types from the library are involved.
| Type | Role in a test |
|---|---|
|
A rectangle of |
|
One screen position: a grapheme ( |
|
A |
|
The same |
Since 0.30 the TestBackend uses core::convert::Infallible as its error type instead of std::io::Error, one of
the changes listed for that release, so Terminal::new(TestBackend::new(w, h)) and terminal.draw(...) cannot
fail in a test and .unwrap() is always safe there.
Source: docs.rs — TestBackend, docs.rs — Buffer, docs.rs — Cell.
The TestBackend and Buffer API
| Method | Purpose |
|---|---|
|
Empty screen of the given size. |
|
Screen pre-filled from lines; the size is taken from them. |
|
Borrow the current buffer for custom assertions or cell-level inspection. |
|
Panics with a diff if the screen differs from a |
|
Panics with a diff if the screen differs from the given lines. Compares symbols and also styles, so it is meant for unstyled expectations; use |
|
Changes the size of the virtual screen. The next |
|
Inspect or assert where the application left the cursor, which matters for text inputs. |
|
Inspect what has been pushed above an inline viewport (see the inline viewport section of Ratatui: Events, Async & Application Architecture). |
|
Formatting the backend with the standard display placeholder renders the buffer as quoted rows of text; this is what snapshot tests record. |
On the Buffer side the useful calls in tests are Buffer::with_lines(lines) and Buffer::empty(area) to build
an expectation, set_string(x, y, text, style) and set_style(area, style) to paint the expectation, cell(position)
for a checked lookup that returns an Option, and indexing with buffer[(x, y)], which panics outside the area.
Buffer::diff returns the cells that differ between two buffers, which is what the real backend sends to the
terminal; it panics if the two buffers do not have the same area.
A testable catalogue screen
The shelf TUI is a catalogue browser. For the tests below it is reduced to its smallest testable shape: a state
struct, a pure function for key handling, and a Widget implementation for &Catalogue, so that nothing in it
needs a real terminal. The same split (state, handle_key, render into a Buffer) is the one recommended by the
Elm-style architecture described on the architecture page.
# Cargo.toml
[package]
name = "shelf"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30"
[dev-dependencies]
insta = "1.49"
// src/catalogue.rs
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::{Block, List, ListItem, Widget};
pub struct Book {
pub title: &'static str,
pub on_loan: bool,
}
pub struct Catalogue {
pub books: Vec<Book>,
pub selected: usize,
pub quit: bool,
}
impl Catalogue {
pub fn new(books: Vec<Book>) -> Self {
Self { books, selected: 0, quit: false }
}
/// Pure state transition: no terminal, no I/O.
pub fn handle_key(&mut self, key: KeyEvent) {
// Windows reports releases too; see the Ratatui FAQ.
if key.kind != KeyEventKind::Press {
return;
}
match key.code {
KeyCode::Down | KeyCode::Char('j') => {
if self.selected + 1 < self.books.len() {
self.selected += 1;
}
}
KeyCode::Up | KeyCode::Char('k') => {
self.selected = self.selected.saturating_sub(1);
}
KeyCode::Char('q') | KeyCode::Esc => self.quit = true,
_ => {}
}
}
}
impl Widget for &Catalogue {
fn render(self, area: Rect, buf: &mut Buffer) {
let block = Block::bordered().title("Shelf");
let inner = block.inner(area);
block.render(area, buf);
let rows = self.books.iter().enumerate().map(|(index, book)| {
let marker = if index == self.selected { ">" } else { " " };
let loan = if book.on_loan { " [on loan]" } else { "" };
let item = ListItem::new(format!("{marker} {}{loan}", book.title));
if index == self.selected {
item.style(Style::new().add_modifier(Modifier::REVERSED))
} else {
item
}
});
List::new(rows).render(inner, buf);
}
}
Source: Ratatui — Widgets, docs.rs — Widget.
Asserting on the screen
The tests live in the same file. Each one builds the state, draws it through a real Terminal over a
TestBackend, and asserts on the backend.
// src/catalogue.rs (continued)
#[cfg(test)]
mod tests {
use super::*;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::crossterm::event::KeyModifiers;
fn sample() -> Catalogue {
Catalogue::new(vec![
Book { title: "Dune", on_loan: false },
Book { title: "Neuromancer", on_loan: true },
Book { title: "Hyperion", on_loan: false },
])
}
fn draw(app: &Catalogue, width: u16, height: u16) -> Terminal<TestBackend> {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal.draw(|frame| frame.render_widget(app, frame.area())).unwrap();
terminal
}
#[test]
fn renders_the_three_books() {
let terminal = draw(&sample(), 26, 5);
terminal.backend().assert_buffer_lines([
"┌Shelf───────────────────┐",
"│> Dune │",
"│ Neuromancer [on loan] │",
"│ Hyperion │",
"└────────────────────────┘",
]);
}
}
assert_buffer_lines is the fastest way to pin down layout: the expected value is a picture of the screen, a
mismatch panics with a row-by-row diff, and nothing is written to disk. Its weakness is that any border style or
character you did not intend (Ratatui lets a Block use plain, rounded, double or thick borders) shows up as a
test failure, which is usually exactly what you want from a rendering test.
Colours and modifiers need a different assertion. The selected row is rendered with the reversed modifier, which the
text rows above cannot show. A full Buffer expectation compares styles too:
#[test]
fn the_selected_row_is_reversed() {
let mut app = sample();
app.handle_key(KeyEvent::from(KeyCode::Down));
let terminal = draw(&app, 26, 5);
let mut expected = Buffer::with_lines([
"┌Shelf───────────────────┐",
"│ Dune │",
"│> Neuromancer [on loan] │",
"│ Hyperion │",
"└────────────────────────┘",
]);
expected.set_style(
Rect::new(1, 2, 24, 1),
Style::new().add_modifier(Modifier::REVERSED),
);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn only_the_selected_row_is_reversed() {
let mut app = sample();
app.handle_key(KeyEvent::from(KeyCode::Down));
let terminal = draw(&app, 26, 5);
let buffer = terminal.backend().buffer();
assert!(buffer[(1, 2)].modifier.contains(Modifier::REVERSED));
assert!(!buffer[(1, 1)].modifier.contains(Modifier::REVERSED));
assert!(!buffer[(1, 3)].modifier.contains(Modifier::REVERSED));
}
Rect::new(1, 2, 24, 1) is the inside of the border (x 1, y 2, width 24, height 1): the second row of the list.
The second test shows the cell-level alternative: when only one property matters, reading buffer[(x, y)] produces
a failure message about that property rather than a screen diff.
Testing input and resize
Because handle_key is a plain method, the keyboard is tested without drawing anything. Constructing a
KeyEvent needs no terminal either: KeyEvent::from(KeyCode::Down) builds a press, and
KeyEvent::new_with_kind builds anything else. That is how the Windows pitfall is pinned down by a test. On Windows,
crossterm reports both the press and the release of a key, while on macOS and Linux it reports presses only unless
the keyboard-enhancement flags are enabled, so a handler that ignores KeyEvent::kind moves the selection twice per
keystroke on one platform and once on the others. The test below fails on every platform if the guard is removed,
instead of only on a Windows machine.
#[test]
fn navigation_clamps_at_both_ends() {
let mut app = sample();
app.handle_key(KeyEvent::from(KeyCode::Up));
assert_eq!(app.selected, 0);
for _ in 0..5 {
app.handle_key(KeyEvent::from(KeyCode::Char('j')));
}
assert_eq!(app.selected, 2);
}
#[test]
fn key_releases_are_ignored() {
let mut app = sample();
let release = KeyEvent::new_with_kind(
KeyCode::Down,
KeyModifiers::NONE,
KeyEventKind::Release,
);
app.handle_key(release);
assert_eq!(app.selected, 0);
}
#[test]
fn q_quits() {
let mut app = sample();
app.handle_key(KeyEvent::from(KeyCode::Char('q')));
assert!(app.quit);
}
#[test]
fn a_narrow_terminal_truncates_long_titles() {
let app = sample();
let mut terminal = draw(&app, 26, 5);
terminal.backend_mut().resize(14, 5);
terminal.draw(|frame| frame.render_widget(&app, frame.area())).unwrap();
terminal.backend().assert_buffer_lines([
"┌Shelf───────┐",
"│> Dune │",
"│ Neuromance│",
"│ Hyperion │",
"└────────────┘",
]);
}
}
The last test simulates a resize the way the real program experiences one: the backend changes size, the next
draw call detects it, and the widgets lay themselves out in the new area. A test of the real event path (the
Event::Resize that crossterm delivers, or the signal that triggers it) needs a real pseudo-terminal and belongs to
the end-to-end layer described below.
Source: Ratatui — FAQ, docs.rs — crossterm KeyEvent.
Snapshot tests with insta
Hand-written expected screens stop being practical once the screen is large. The Ratatui website documents a
snapshot workflow with the insta crate and its cargo-insta companion: the first run records
the rendered screen, later runs compare against it, and an intended change is accepted in a review step.
cargo install cargo-insta
cargo add insta --dev
#[test]
fn catalogue_snapshot() {
let terminal = draw(&sample(), 40, 8);
insta::assert_snapshot!(terminal.backend());
}
assert_snapshot! takes anything that implements Display, and TestBackend does: it prints the buffer as one
quoted string per row. The first run writes a file under snapshots/ next to the source file, named after the
crate, the module path and the test, and the documented shape of such a file is a small header followed by the rows:
---
source: src/catalogue.rs
expression: terminal.backend()
---
"┌Shelf─────────────────────────────────┐"
"│> Dune │"
"│ Neuromancer [on loan] │"
"│ Hyperion │"
"│ │"
"│ │"
"│ │"
"└──────────────────────────────────────┘"
The workflow around it is the one insta documents for all Rust code.
| Command or variable | Effect |
|---|---|
|
Compares each snapshot. A new or changed snapshot makes the test fail and, outside CI, writes the new value next to the old one with a |
|
Interactive review: accept, reject or skip each pending snapshot. |
|
Runs the tests and collects the pending changes from all assertions in one pass; |
|
Chooses what happens on a mismatch: |
|
Insta detects CI through this variable and does not write snapshot files there, so a changed screen fails the build instead of silently updating. |
Commit the .snap files, review them in pull requests like any other code, and never commit .snap.new files.
What the layers can and cannot see
| Layer | Sees | Does not see |
|---|---|---|
|
Symbols, geometry, and (with |
Escape sequences, real terminal behaviour |
insta snapshot of |
The symbols of every row. The Ratatui page states that asserting colour in these snapshots is not supported (it links an open upstream issue), so the snapshot records text only |
Colours, modifiers, the cursor position |
State tests on |
Every transition, deterministically and fast |
Whether the real input source produces those events |
Pseudo-terminal end-to-end tests |
The compiled binary: raw mode, the alternate screen, exit codes, real resize |
Nothing is mocked, but they are slow and timing-sensitive |
Two further facts follow from how TestBackend works. It never writes escape sequences, so a bug in how a backend
translates cells into SGR codes or cursor moves is invisible to it; those bugs belong to the backend crates and are
platform-specific (the same cell can be coloured differently by the legacy Windows console and by a VT terminal,
see Windows Console & Processes and
Escape Sequences & Terminal Capabilities). And it has
a fixed width and height, so a snapshot says nothing about the sizes you did not test: pick two or three (a
minimum, a typical 80x24 and a wide one) and test each.
Whole-application tests through a pseudo-terminal
TestBackend tests rendering in-process. They do not cover the event loop, terminal setup and teardown, panic
hooks or exit codes. The Ratatui snapshot recipe points at running the compiled binary in a pseudo-terminal, sending
it keystrokes and asserting on the rendered screen, and names one crate for it, termlens
(0.11.6 on crates.io, a young crate with a small number of downloads at the time of writing, so evaluate it before
committing to it). The recipe’s example spawns the binary through the CARGO_BIN_EXE_<name> variable that Cargo sets
for integration tests, waits for visible text instead of sleeping, and ends with wait_exit and an assertion that the
program succeeded. Real-PTY testing for every language, including the Windows ConPTY case and the tools
(expect, pexpect, rexpect, tmux), is the subject of
Testing Terminal Apps & TUIs; the terminal model underneath is on
Terminals, TTYs & Pseudo-Terminals.
Debugging while the library owns the terminal
A running Ratatui program owns the screen, so println! and dbg! corrupt it. The documentation suggests two
techniques: write logs to a file, or use the tui-logger widget crate (0.18.3),
which captures log records and shows them inside the TUI; and for widget state, render the Debug text of the state
in a panel that a key toggles on and off, which the recipe shows with a show_debug field on the application.
Source: Ratatui — Debugging Widget State.
Recording demonstrations with VHS
A screenshot of a TUI goes stale as fast as the code changes. VHS (from
Charm, v0.12.1) turns a text script, a tape, into a GIF, MP4, WebM or PNG sequence by driving a real terminal,
so the same tape regenerates the demo whenever the UI changes. The Ratatui project itself keeps .tape files for the
recipes in its website repository. VHS is written in Go and is independent of the language of the program it
records.
# docs/shelf-tui.tape
Output docs/shelf-tui.gif
Output docs/shelf-tui.txt
Require shelf
Set Shell "bash"
Set FontSize 20
Set Width 1000
Set Height 500
Set TypingSpeed 80ms
Type "shelf tui"
Enter
Sleep 1s
Down 2
Sleep 500ms
Type "q"
Sleep 500ms
vhs docs/shelf-tui.tape
The commands used above are documented in the VHS reference: Output names each artifact (several can be listed,
and a .txt or .ascii output records the final text screen), Require fails early when a program is not on the
PATH and must appear before any other command, Set configures the shell, font, size and typing speed, Type
and Enter send input, Down 2 presses an arrow key twice, and Sleep waits. Besides demos, the README describes
the text output as a way to keep golden files in version control and notice output changes between runs.
| Platform | Installing VHS |
|---|---|
macOS and Linux |
|
Windows |
|
Any |
|
Pitfalls: a recording that relies on timing (Sleep) is only as deterministic as the program is, so make the
program start in a known state (a fixed data file, no clock on screen); recordings render in VHS’s own terminal
and font, so they show how the program looks there and not in the reader’s terminal; and vhs publish uploads a GIF
to a public server, which is not something to do with a demo that shows private data.
Source: VHS — README.
Third-party widgets and crates
Ratatui itself ships the widgets described on
Ratatui: Layout, Widgets & Styling. The Ratatui website keeps a
Third Party Widgets showcase for community crates and an ecosystem section for larger pieces. Two cautions apply
to all of them: they are maintained by other people, on their own schedules, and a crate is only usable with a given
Ratatui if its dependency requirement overlaps. Since 0.30, widget crates can depend on ratatui-core and
ratatui-widgets instead of ratatui, which makes them less likely to break on every Ratatui release.
The table lists the crates mentioned in the showcase and relevant to the shelf TUI. Versions and release dates are the ones crates.io reported on 2026-10-25; the "Ratatui requirement" column is read from each crate’s published dependencies.
| Crate | Latest release | Ratatui requirement | What it is and its status |
|---|---|---|---|
|
0.9.3 (2026-10-05) |
|
Multi-line text editor widget, a fork of |
|
0.7.0 (2024-10-22) |
|
The original editor widget by rhysd. Its last release on crates.io is from October 2024, so its published dependencies stop at Ratatui 0.29 (it is described here as dormant for that reason, from the release history alone). Use |
|
0.15.5 (2026-09-26) |
|
Single-line input buffer with cursor handling and several backends (crossterm, termion). Not listed in the showcase page, and fits a search box. |
|
0.24.1 (2026-08-09) |
|
Tree view with open and closed nodes and a |
|
0.8.10 (2026-09-24) |
|
Large pixel text from the |
|
11.1.0 stable (2026-10-18; a 12.0.0 release candidate is also published) |
|
Image widget with Sixel, Kitty and iTerm2 protocols and a Unicode half-block fallback. Listed in the showcase. It queries the terminal, so see the pitfalls below. |
|
0.18.3 (2026-07-04) |
|
Log capture and display widget, recommended by the debugging recipe and listed in the showcase. |
|
0.6.8 (2026-09-24) |
|
Scrollable view over content larger than the screen. Listed in the showcase. |
|
0.3.4 (2026-04-07) |
|
A pseudo-terminal widget, for embedding another program’s output in a pane. Listed in the showcase. |
|
0.11.1 (2026-06-19) |
|
Spinner and activity indicator widget. Listed in the showcase. |
Pitfalls common to this list:
-
Before adding a dependency, run
cargo tree -dto see whether it pulls in a second Ratatui or a second crossterm major version. Different crossterm majors keep separate event queues and separate raw-mode state, cannot exchange types even when the names match, and can leave the terminal in raw mode on exit; the symptoms are confusing type errors and lost events. Ratatui’s backends page describes theratatui-crosstermpackage, which selects a crossterm major version throughcrossterm_{version}feature flags, as the mitigation; the main package uses crossterm 0.29 by default. -
Check the date of the last release and the Ratatui version it depends on. A crate that last released against 0.2x will pull in an old Ratatui next to the one the application uses.
-
Treat every one of these crates as 0.x: their own APIs change between minor versions just as Ratatui’s does.
Example: a notes box with ratatui-textarea
The shelf TUI lets the reader attach a note to a book. ratatui-textarea provides the editor: the README’s
minimal example creates a TextArea::default(), renders it by reference with render_widget(&textarea, area),
passes key events to textarea.input(key) and reads the content back with textarea.lines(). Its default
key bindings are Emacs-like (Ctrl+A and Ctrl+E for line start and end, Ctrl+K to delete to the end of the line,
Ctrl+U and Ctrl+R for undo and redo, Ctrl+Y to paste), and input_without_shortcuts() turns them off so that
the application can define its own. The crossterm backend is the default feature; termion and termwiz are
selected by turning default features off.
// src/notes.rs
use ratatui::crossterm::event::{KeyEvent, KeyEventKind};
use ratatui::layout::Rect;
use ratatui::Frame;
use ratatui_textarea::TextArea;
pub struct NoteEditor {
textarea: TextArea<'static>,
}
impl NoteEditor {
pub fn new() -> Self {
Self { textarea: TextArea::default() }
}
pub fn handle_key(&mut self, key: KeyEvent) {
// Same guard as in the catalogue: Windows also reports releases.
if key.kind == KeyEventKind::Press {
self.textarea.input(key);
}
}
pub fn render(&self, frame: &mut Frame, area: Rect) {
frame.render_widget(&self.textarea, area);
}
pub fn text(&self) -> String {
self.textarea.lines().join("\n")
}
}
The TextArea<'static> lifetime parameter follows the tui-textarea design that the crate inherits, and the key
type that input accepts depends on the enabled backend feature, so check the signature of your version on docs.rs
before copying. The README does not state which Ratatui versions the crate supports in prose, only the
dependency requirements listed above. The guard on KeyEventKind::Press is this page’s recommendation, taken from the
Ratatui FAQ’s advice for Windows; the README does not discuss it.
Pitfalls of terminal-querying widgets
ratatui-image guesses the graphics protocol from environment variables and then asks the terminal with control
sequences, and also queries the font size in pixels so image pixels map to character cells; if nothing works it falls
back to half-block characters. Its documentation lists a Picker for detection and says its caveats include doing
the query before Ratatui is initialised. A terminal that does not answer the query leaves a program waiting, so test
on the terminals you support, and see
Escape Sequences & Terminal Capabilities for how the
queries work and why a multiplexer or an SSH hop can swallow them. The crate offers an image widget that is stateless
and a stateful one whose resize and encoding block, and recommends running the latter on another thread or async
task.
Source: docs.rs — ratatui-image.
Beyond widgets: effects, components and frameworks
The Ratatui website’s ecosystem section lists larger building blocks. They are mentioned here as pointers; the website links their quickstarts and API documentation.
| Project | What the Ratatui documentation says about it |
|---|---|
tachyonfx |
An effects and animation library for Ratatui applications: simple effects composed and layered into transitions. Latest crates.io release 0.25.2. |
ratcn |
Themeable terminal UI components (buttons, lists, tabs, dialogs) inspired by shadcn/ui; an optional runtime routes focus, hover, keyboard and mouse events while the application keeps its own state and event loop. |
tui-realm |
A framework on top of Ratatui; see below. |
Library or framework: Ratatui and tui-realm
The Ratatui FAQ answers the question directly. A library is something your code calls and control flow stays with the application; a framework inverts control and calls into code you plug in. Ratatui is the first kind: it does not dictate how the application is structured, it draws what you give it on each iteration, it does not read input (you use crossterm or another backend for that) and it leaves the event loop to you. tui-realm (4.1.0) is described as the second kind, with more guidelines about how the application should be structured and how data flows through it, in exchange for more features out of the box and, potentially, less application code.
| Concern | Ratatui alone | tui-realm |
|---|---|---|
Event loop |
Written by the application (the architecture page shows polling, |
Provided by the |
Structure of the code |
Free: The Elm Architecture, components or Flux are all available |
Components with props and state, messages and events, an Elm-inspired update routine, a view that handles mounting, unmounting and focus |
Event routing |
The application decides which widget gets a key |
Components declare subscriptions that decide which events they receive |
Backends |
crossterm, termion, termwiz, and others |
crossterm (default), termion and termwiz through features |
Standard components |
The widgets of |
Provided by the separate |
Dependency footprint |
One crate family |
Ratatui 0.30 plus its own layers; its major version is independent of Ratatui’s |
Choose Ratatui alone for something the size of the shelf TUI: a list, a detail pane, a search box, a loans table. Consider a framework layer when many independent components each need focus and event routing, or when the team wants one prescribed structure instead of designing it. The cost is one more dependency whose upgrades must follow Ratatui’s, the same 0.x discipline described earlier.
The FAQ also compares Ratatui with Cursive, a library with a declarative UI and a built-in event loop that supports several backends; the contrast is the same: Ratatui leaves the loop, the state and the redrawing to the application.
Source: Ratatui — FAQ, docs.rs — tuirealm.
Running Ratatui where there is no ordinary terminal
The crossterm backend writes to the process’s standard output, so a Ratatui program run inside an SSH session, a
terminal multiplexer or a container works the way any full-screen program does: the remote side allocates a
pseudo-terminal, sizes and key presses travel over the connection, and the program draws to it. What this involves
(allocating a PTY with ssh -t, SIGWINCH forwarding, escape sequences passing through) is general terminal
behaviour, covered on Terminals, TTYs & Pseudo-Terminals. What the
Ratatui documentation does not provide is a guide to serving a Ratatui application to remote users from an SSH
server written in Rust; the only SSH mention found in the Ratatui documentation is that the ratcn showcase can be tried
with ssh ratcn.com. Anything built that way is outside what the official pages describe.
Ratatui’s rendering separates what is drawn from where it goes, which the ecosystem section uses.
| Target | Project | What the Ratatui website says |
|---|---|---|
Browser (WebAssembly) |
Ratzilla 0.3.1 |
Builds terminal-themed web applications with Rust and WebAssembly; links a live demo, a quickstart, API documentation and examples. Needs Ratatui 0.30.1 or newer. |
Embedded displays |
mousefood 0.5.2 |
An |
UEFI (before the operating system starts) |
ratatuefi |
A |
None of these is the right choice for the shelf tool on a laptop; they matter when the reason for choosing
Ratatui is that the same widget code should also run in a browser demo or on a device. The cost is that every
backend has its own input model, so the handle_key function above would be fed from a different event source.
Source: Ratatui — Ratzilla, Ratatui — Mousefood, Ratatui — UEFI.
Migrating
From tui-rs
Ratatui is a fork of tui-rs, created in February 2023 with the blessing of its original author to continue
maintenance. The migration recipe offers three options: replace tui with ratatui everywhere (preferred, a
find-and-replace of tui:: with ratatui:: plus a dependency edit), use ratatui as a drop-in replacement aliased
to tui in Cargo.toml (a tui dependency that sets its package key to ratatui) and update the terminal crate, or support both
through feature flags in a library. The recipe’s own Cargo.toml snippets use 0.28; today the first step after
the rename is to move from there to the current version, one minor release at a time, through the table that
follows. Code that targets the pre-fork tui-rs is several years of breaking changes behind current Ratatui.
Source: Ratatui — Migrate from tui-rs, Ratatui — FAQ.
Between Ratatui releases
Two official documents describe what changes between releases: the highlights pages on the website, one per
minor release from v0.21.0 on, which present the new features with examples, and BREAKING-CHANGES.md, which lists
every breaking change and how to migrate. The table samples the entries that most often touch an application like
shelf.
| Release | Breaking changes recorded in BREAKING-CHANGES.md |
|---|---|
0.30.0 |
|
0.30.1 |
Adding |
0.29.0 |
|
0.28.0 |
|
0.27.0 |
|
A reliable upgrade routine, using only Cargo and the tests described above:
-
Read the highlights page and the
BREAKING-CHANGES.mdsection for the target version first. -
Make sure the test suite passes before touching the version. Snapshot tests are the safety net: any unintended rendering change shows up as a snapshot diff.
-
Change the version in
Cargo.toml, runcargo update -p ratatui, and fix compile errors. Fix deprecation warnings too: the project removes deprecated items after at least two versions, so a warning left in place becomes an error one or two releases later. -
Run
cargo tree -dto confirm there is one Ratatui and one crossterm in the dependency graph. -
Review the snapshot diffs with
cargo insta review. Accept only the ones that are intended, for example a border or spacing change documented in the breaking-changes file. -
Upgrade third-party widget crates to releases that require the same Ratatui. If one has not been released for the new Ratatui, either wait, fork, or replace it.
-
Repeat for each minor release instead of jumping several at once.
Pitfalls
-
Snapshot tests that depend on the clock, the working directory or an unsorted map fail intermittently; render from fixed data.
-
A snapshot records symbols only. A change that only alters colours or modifiers passes the snapshot test, so keep a
Bufferor cell-level assertion for anything where style carries meaning (the selection highlight, error colours). -
Widget and layout code that behaves differently depending on the terminal size needs a test at each size; one 80x24 snapshot does not cover a 40-column terminal.
-
Do not call
ratatui::initorratatui::runin tests: they put the real terminal in raw mode. Tests constructTerminal::new(TestBackend::new(...))themselves, which is why thedrawhelper above takes the state and returns the terminal. -
Unicode width: CJK characters and emoji occupy two cells, and a
TestBackendsnapshot shows the effect on the following cell. See the width section of Escape Sequences & Terminal Capabilities. -
The Windows duplicate-key behaviour is invisible on macOS and Linux developer machines; keep the
KeyEventKind::Presstest in the suite so it is exercised on every platform. -
Lock the minor version (a requirement of
0.30onratatui), commitCargo.lockfor applications, and upgrade deliberately.