Rust terminal UI tutorial

Writing first "Hello world" terminal app

Post coverPost cover

Create new Rust project

Let’s create a new Rust project. Open your terminal, navigate to the directory where you want to store your projects, and run:
$ mkdir ratatui-hello-world
$ cargo init --bin --vcs git --name ratatui-hello-world
$ cd ratatui-hello-world
The cargo new command creates a new folder named hello-ratatui with a basic binary application. You should see:
Created binary (application) `ratatui-hello-world` package
If you look at the created folders and files, it will look like this:
$ ls
ratatui-hello-world/
├── src/
│   └── main.rs
└── Cargo.toml
cargo new has created a default main.rs with a simple program that prints “Hello, world!”.
// src/main.rs
fn main() {
    println!("Hello, world!");
}

Add ratatui

First, install the Ratatui crate into your project. You also need a backend. For this tutorial, use Crossterm as it works with most operating systems. To install the latest versions of the ratatui and crossterm crates, run:
$ cargo add ratatui crossterm
Cargo will output something like this (note that the exact versions may be different):
Updating crates.io index
  Adding ratatui v0.24.0 to dependencies.
  Adding crossterm v0.27.0 to dependencies.
Updating crates.io index
If you look at the Cargo.toml file, you should see the new crates added to the dependencies section:
[package]
name = "ratatui-hello-world"
version = "0.1.0"
edition = "2021"

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

Create a TUI Application

Replace the default console application code created by cargo new with a Ratatui application that displays a colored message in the middle of the screen and waits for the user to press a key to exit.

Imports

First, add the necessary module imports to your application. Open src/main.rs and add the following at the top:
use crossterm::{
    event::{self, KeyCode, KeyEventKind},
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use ratatui::{
    prelude::{CrosstermBackend, Stylize, Terminal},
    widgets::{Block, Borders, Paragraph},
};
use std::io::{stdout, Result};

Setting Up and Restoring the Terminal

Next, add code to the main function to set up and restore the terminal state. This involves entering the alternate screen, enabling raw mode, creating a backend and terminal, and then clearing the screen. When the application exits, it needs to restore the terminal state.
Replace the existing main function with:
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(())
}

Add a Main Loop

The main part of the application is the main loop, which repeatedly draws the UI and handles events. Replace // TODO main loop with:
loop {
    // TODO draw the UI
    // TODO handle events
}

Draw to the Terminal

The draw method on the terminal is the main interaction point with Ratatui. It accepts a closure with a single Frame parameter and renders the entire screen. Your application will create an area that is the full size of the terminal window and render a new Paragraph with white foreground text and a blue background.
Replace // TODO draw with:
terminal.draw(|frame| {
    let area = frame.size();
    frame.render_widget(
        Paragraph::new("Hello Ratatui! (press 'q' to quit)")
            .white()
            .on_blue()
            .block(Block::default().borders(Borders::ALL)),
        area,
    );
})?;

Handle Events

After Ratatui has drawn a frame, your application needs to check for events such as keyboard presses, mouse events, and resizes. If the user presses the q key, the app should exit the loop.
Add a small timeout to the event polling to ensure the UI remains responsive (16ms is ~60fps). It's important to check that the event kind is Press to avoid handling the same key multiple times.
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 now look like this:
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(())
}
$ cargo run
You should see a TUI app with the message "Hello Ratatui! (press 'q' to quit)" displayed in your terminal. Press q to exit and return to your terminal.
Congratulations! 🎉