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

ratatui

The main crate and the recommended entry point for applications. Re-exports the core types and the widgets, and adds the run, init and restore helpers, the DefaultTerminal alias and the optional backends.

Always, in applications

ratatui-core

The core traits and types: Widget, StatefulWidget, Buffer, Cell, Rect, Layout, Style, Text and the Backend trait, among others.

Third-party widget libraries that should not pull in every built-in widget

ratatui-widgets

The built-in widget implementations (Block, Paragraph, List, Table, Gauge, Chart, Calendar and the others).

Rarely; ratatui re-exports them under ratatui::widgets

ratatui-crossterm

The crossterm backend. Re-exported as the default backend.

Libraries that need the backend type without the whole application crate

ratatui-termion

The termion backend (Unix only).

Selected with the termion feature of ratatui

ratatui-termwiz

The termwiz backend.

Selected with the termwiz feature of ratatui

ratatui-termina

The backend for the termina terminal library, new in the 0.30 line.

Selected with the termina feature of ratatui

ratatui-macros

Macros that reduce boilerplate (for spans, lines, text and layouts). Enabled by the default macros feature.

Rarely; use ratatui::macros

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

crossterm

Enables the crossterm backend and the run, init, try_init, restore and DefaultTerminal items. The documentation of each of those items states that it is available only with this feature.

crossterm_0_28, crossterm_0_29

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.

termion, termwiz, termina

Enable another backend. All backend features imply std.

layout-cache

A cache that speeds up repeated layout computations. Turning off default features also turns it off, which can cost performance.

widget-calendar (through all-widgets)

The Calendar widget.

serde

Serialization for styles, layout types and backend types.

palette, anstyle

Colour conversions with the palette crate and, in 0.30, the anstyle types (the latter is off by default).

portable-atomic

Support for targets without native atomic types, for the no_std builds.

scrolling-regions

Enables the use of terminal scrolling regions in the backends (see the crate documentation for details).

unstable, unstable-widget-ref, unstable-rendered-line-info, unstable-backend-writer

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.

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.

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

ratatui::run

pub fn run<F, R>(f: F) -> R where F: FnOnce(&mut DefaultTerminal) -> R

Calls init, passes the terminal to your closure, calls restore when the closure returns, and returns the closure’s result. Intended for simple applications that need a default terminal for their whole lifetime. New in 0.30.

ratatui::init

pub fn init() -> DefaultTerminal

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.

ratatui::try_init

pub fn try_init() -> Result<DefaultTerminal>

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).

ratatui::restore

pub fn restore()

Disables raw mode and leaves the alternate screen. If either step fails the error is printed to standard error and ignored.

ratatui::try_restore

the fallible counterpart of restore

Returns the error instead of printing it. The signature is not shown in the pages consulted.

DefaultTerminal

type DefaultTerminal = Terminal<CrosstermBackend<Stdout>>

The terminal type returned by init. Use the generic Terminal with a backend of your choice for anything else.

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(());
            }
        }
    })
}

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.

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:

  1. Autoresize: the terminal compares the backend’s size with the previous one and resizes its buffers if the user resized the window.

  2. Render: your closure receives a Frame and fills an empty buffer through it. Because every pass starts from an empty buffer, the closure must draw everything that should stay visible.

  3. Flush: the current buffer is compared with the previous one, and only the changed cells are sent to the backend.

  4. Cursor: the cursor is shown at the position set with Frame::set_cursor_position, or hidden if none was set.

  5. 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

pub const fn area(&self) -> Rect

The rectangle covering the whole terminal. The older size() is deprecated in favour of area().

pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect)

Draws a widget into a region. Widgets are consumed by the call, so build them inside the closure.

pub fn render_stateful_widget<W: StatefulWidget>(&mut self, widget: W, area: Rect, state: &mut W::State)

Draws a widget whose selection or scroll position lives in a separate state value, such as List with ListState.

pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P)

Shows the terminal cursor at a cell, for text input widgets.

pub const fn buffer_mut(&mut self) -> &mut Buffer

Direct access to the cell buffer, for custom drawing.

pub const fn count(&self) -> usize

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.

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.

flowchart TD A["main: ratatui::run or ratatui::init"] --> B["raw mode on, alternate screen entered, panic hook installed"] B --> C["terminal.draw: render widgets from the current state"] C --> D["buffer diff: only changed cells are written to the terminal"] D --> E["event::read: wait for a key, mouse, paste or resize event"] E --> F{"Key press?"} F -->|"No: resize or other event"| C F -->|"Yes"| G["handle the key and update the state"] G --> H{"Quit requested?"} H -->|"No"| C H -->|"Yes"| I["ratatui::restore: raw mode off, alternate screen left"] I --> J["return to the shell with the original screen"]

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.

  • render takes &mut self because rendering a stateful widget updates the ListState (it adjusts the scroll offset so the selected row stays visible). The closure passed to draw borrows the whole App mutably for the duration of the call, which is allowed because draw takes a closure and does not touch the App itself.

  • ListState::select_next and select_previous change the selected index. with_selected(Some(0)) selects the first row so that the highlight is visible at start-up.

  • List::new accepts any iterator whose items convert to list items; a String does. The list consumes the iterator, so the borrow of self.books ends before render_stateful_widget borrows self.list_state.

  • Block::bordered().title("shelf tui") draws the frame around the list; Style::new().reversed() comes from the Stylize trait 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 Table widget, which does the column layout for you, is introduced on the layout and widgets page.

  • A main that returns std::io::Result<()> prints the error with Debug formatting 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 after restore has 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

pub fn read() -> Result<Event>

Blocks until an event is available. The simplest loop, and the right one for a program that changes only in response to input.

pub fn poll(timeout: Duration) -> Result<bool>

Waits up to timeout for an event and returns whether one is available. A zero timeout returns immediately. When it returns true, the next read does not block. Use it to wake the loop periodically (for a clock, a blinking cursor, or checking a background channel).

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 kind

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

Event::Resize from the console input

Event::Resize, from SIGWINCH

Event::Resize, from SIGWINCH

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:

  1. The quit key sets running to false (or break`s out of the loop); no code calls `std::process::exit from inside the loop, because that would skip restore.

  2. ratatui::run restores on return, or the program calls ratatui::restore() after its own loop.

  3. Errors travel up as Result values and are printed after restoration. The panic hook installed by init covers panics.

  4. main returns after restoration; a non-success ExitCode can 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 compare kind with KeyEventKind::Press).

  • Two crossterm versions: a direct crossterm dependency whose version differs from the one Ratatui uses produces Event values of a different type. Import through ratatui::crossterm, or pin the same version.

  • Printing while the interface is active: println! and eprintln! 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::exit and std::mem::forget bypass it, and a terminal left in raw mode does not echo input. Typing reset in 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 unstable have 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