Hello World

This tutorial will lead you through creating a simple “Hello World” TUI app that displays some text in the middle of the screen and waits for the user to press q to exit. It demonstrates the necessary tasks that any application developed with Ratatui needs to undertake. We assume that you have a basic understanding of the terminal a text editor or Rust IDE (if you don’t have a preference, VSCode makes a good default choice).

We’re going to build the following:

hello

The full code for this tutorial is available to view at https://github.com/ratatui-org/ratatui-book/tree/main/code/hello-world-tutorial

Install Rust

The first step is to install Rust. See the Installation section of the official Rust Book for more information. Most people tend to use rustup, a command line tool for managing Rust versions and associated tools.

Once you’ve installed Rust, verify that it’s installed by running:

rustc --version

You should see output similar to the following (the exact version, date and commit hash will vary):

rustc 1.72.1 (d5c2e9c34 2023-09-13)

Create a new project

Let’s create a new Rust project. In the terminal, navigate to a folder where you will store your projects and run:

cargo new hello-ratatui
cd hello-ratatui

The cargo new command creates a new folder called hello-ratatui with a basic binary application in it. You should see:

     Created binary (application) `hello-ratatui` package

If you examine the folders and files created this will look like:

hello-ratatui/
├── src/
│  └── main.rs
└── Cargo.toml

cargo new created a default main.rs with a small console program that prints “Hello, world!”.

fn main() {
    println!("Hello, world!");
}

Let’s build and execute the project. Run:

cargo run

You should see:

   Compiling hello-ratatui v0.1.0 (/Users/joshka/local/hello-ratatui)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/hello-ratatui`
Hello, world!

The default main.rs program is responsible for printing the last line. We’re going to replace it with something a little bit more exciting.

Install Ratatui

First up, we need to install the Ratatui crate into our project. We also need to install a backend. We will use Crossterm here as the backend as it’s compatible with most operating systems. To install the latest version of the ratatui and crossterm crates into the project run:

cargo add ratatui crossterm

Cargo will output the following (note that the exact versions may be later than the ones in this tutorial).

    Updating crates.io index
      Adding ratatui v0.24.0 to dependencies.
             Features:
             + crossterm
             - all-widgets
             - document-features
             - macros
             - serde
             - termion
             - termwiz
             - widget-calendar
      Adding crossterm v0.27.0 to dependencies.
             Features:
             + bracketed-paste
             + events
             + windows
             - event-stream
             - filedescriptor
             - serde
             - use-dev-tty
    Updating crates.io index

If you examine the Cargo.toml file, you should see that the new crates have been added to the dependencies section:

[dependencies]
crossterm = "0.27.0"
ratatui = "0.24.0"

Create a TUI application

Let’s replace the default console application code that cargo new created with a Ratatui application that displays a colored message the middle of the screen and waits for the user to press a key to exit.

Note: a full copy of the code is available below in the Running the application section.

Imports

First let’s add the module imports necessary to run our application.

Info

Ratatui has a prelude module that re-exports the most used types and traits. Importing this module with a wildcard import can simplify your application’s imports.

In your editor, open src/main.rs and add the following at the top of the file.

use crossterm::{
    event::{self, KeyCode, KeyEventKind},
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use ratatui::{
    prelude::{CrosstermBackend, Stylize, Terminal},
    widgets::Paragraph,
};
use std::io::{stdout, Result};

Setting up and restoring the terminal

Next, we need to add code to the main function to setup and restore the terminal state.

Our application needs to do a few things in order to setup the terminal for use:

  • First, the application enters the alternate screen, which is a secondary screen that allows your application to render whatever it needs to, without disturbing the normal output of terminal apps in your shell.
  • Next, the application enables raw mode, which turns off input and output processing by the terminal. This allows our application control when characters are printed to the screen.
  • The app then creates a backend and Terminal and then clears the screen.

When the application is finished it needs to restore the terminal state by leaving the alternate screen and disabling raw mode.

Replace the existing main function with the following:

fn main() -> Result<()> {
    stdout().execute(EnterAlternateScreen)?;
    enable_raw_mode()?;
    let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
    terminal.clear()?;

    // TODO main loop

    stdout().execute(LeaveAlternateScreen)?;
    disable_raw_mode()?;
    Ok(())
}

Warning

If we don’t disable raw mode before exit, terminals can act weirdly when the mouse or navigation keys are pressed. To fix this, on a Linux / macOS terminal type reset. On Windows, you’ll have to close the tab and open a new terminal.

Add a main loop

The main part of our application is the main loop. Our application repeatedly draws the ui and then handles any events that have occurred.

Replace // TODO main loop with a loop:

    loop {
        // TODO draw
        // TODO handle events
    }

Draw to the terminal

The draw method on terminal is the main interaction point that an app has with Ratatui. The draw method accepts a closure that has a single Frame parameter, and renders the entire screen. Our application creates an area that is the full size of the terminal window and renders a new Paragraph with white foreground text and a blue background. The white() and on_blue() methods are defined in the Stylize extension trait as style shorthands, rather than on the Paragraph widget.

Replace // TODO draw with the following

        terminal.draw(|frame| {
            let area = frame.size();
            frame.render_widget(
                Paragraph::new("Hello Ratatui! (press 'q' to quit)")
                    .white()
                    .on_blue(),
                area,
            );
        })?;

Handle events

After Ratatui has drawn a frame, our application needs to check to see if any events have occurred. These are things like keyboard presses, mouse events, resizes, etc. If the user has pressed the q key, we break out of the loop. We add a small timeout to the event polling to ensure that our UI remains responsive regardless of whether there are events pending (16ms is ~60fps). Note that it’s important to check that the event kind is Press otherwise Windows terminals will see each key twice.

Replace // TODO handle events with:

        if event::poll(std::time::Duration::from_millis(16))? {
            if let event::Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
                    break;
                }
            }
        }

Running the Application

Your application should look like:

main.rs
use crossterm::{
    event::{self, KeyCode, KeyEventKind},
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use ratatui::{
    prelude::{CrosstermBackend, Stylize, Terminal},
    widgets::Paragraph,
};
use std::io::{stdout, Result};

fn main() -> Result<()> {
    stdout().execute(EnterAlternateScreen)?;
    enable_raw_mode()?;
    let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
    terminal.clear()?;

    loop {
        terminal.draw(|frame| {
            let area = frame.size();
            frame.render_widget(
                Paragraph::new("Hello Ratatui! (press 'q' to quit)")
                    .white()
                    .on_blue(),
                area,
            );
        })?;

        if event::poll(std::time::Duration::from_millis(16))? {
            if let event::Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
                    break;
                }
            }
        }
    }

    stdout().execute(LeaveAlternateScreen)?;
    disable_raw_mode()?;
    Ok(())
}

Make sure you save the file! Let’s run the app:

cargo run

You should see a TUI app with Hello Ratatui! (press 'q' to quit) show up in your terminal as a TUI app.

hello

You can press q to exit and go back to your terminal as it was before.

Congratulations! 🎉

You have written a “hello world” terminal user interface with ratatui. We will learn more about how ratatui works in the next sections.

Question

Can you modify the example above to exit when pressing q or when pressing Q?