Create a new rust project and open it in your editor
Add the Ratatui and Crossterm crates (See backends for more info on why we use Crossterm).
The Cargo.toml will now have the following in the dependencies section:
Application Setup
Main Imports
In main.rs, add the necessary imports for Ratatui and crossterm. These will be used later in this
tutorial. In the tutorials, we generally use wildcard imports to simplify the code, but you’re
welcome to use explicit imports if that is your preferred style.
Main Function
A common pattern found in most Ratatui apps is that they:
Initialize the terminal
Run the application in a loop until the user exits the app
Restore the terminal back to its original state
The main function sets up the terminal by calling methods in the tui module (defined next), and
then creates and runs the App (defined later). It defers evaluating the result of calling
App::run() until after the terminal is restored to ensure that any Error results will be
displayed to the user after the application exits.
Fill out the main function:
TUI module
The counter app will be displayed on the alternate screen. This is a secondary buffer that allows the
application to avoid messing up the user’s current shell. The app also enables raw mode so that
the app can process keys immediately without having to wait for a newline and so that the keys are
not echoed to the user’s screen when pressed.
Let’s implement this by creating a new module named tui to encapsulate this functionality into an
init and a restore functions.
Add a module to main.rs after the imports section:
Create a new file named src/tui.rs for the module. Add the imports, and two new functions, init
and restore:
There is a PR to simplify this boilerplate code, but for now it’s most convenient to write a small
helper module to handle this.
Application State
The counter app needs to store a small amount of state, a counter and a flag to indicate that the
application should exit. The counter will be an 8-bit unsigned int, and the exit flag can be a
simple bool. Applications that have more than one main state or mode might instead use an enum to
represent this flag.
Create an App struct to represent your application’s state:
Calling App::default() will create an App initialized with counter set to 0, and exit set to
false.
Application Main loop
Most apps have a main loop that runs until the user chooses to exit. Each iteration of the loop
draws a single frame by calling Terminal::draw() and then updates the state of the app.
Create an impl block for the App with a new run method that will act as the application’s main
loop:
Displaying the application
Render a Frame
To render the UI, an application calls Terminal::draw() with a closure that accepts a Frame. The
most important method on Frame is render_widget() which renders any type that implements the
Widget trait such as Paragraph, List etc. We will implement the Widget
trait for the App struct so that the code related to rendering is organized in a single place.
This allows us to call Frame::render_widget() with the app in the closure passed to
Terminal::draw.
First, add a new impl Widget for &App block. We implement this on a reference to the App type, as
the render function will not mutate any state, and we want to be able to use the app after the call
to draw. The render function will create a block with a title, instruction text on the bottom, and
some borders. Render a Paragraph widget with the application’s state (the value of the Apps
counter field) inside the block. The block and paragraph will take up the entire size of the widget:
Next, render the app as a widget:
Testing the UI Output
To test how how Ratatui will display the widget when render is called, you can render the app to a
buffer in a test.
Add the following tests module to main.rs:
To run this test run the following in your terminal:
You should see:
Interactivity
The application needs to accept events that come from the user via the standard input. The only
events this application needs to worry about are key events. For information on other available
events, see the Crossterm events module docs. These include window resize and focus, paste, and
mouse events.
In more advanced applications, events might come from the system, over the network, or from other
parts of the application.
Handle Events
The handle_events method that you defined earlier is where the app will wait for and handle any
events that are provided to it from crossterm.
Update the handle_events method that you defined earlier:
Handle Keyboard Events
Your counter app will update the state of the App struct’s fields based on the key that was
pressed. The keyboard event has two fields of interest to this app:
kind: It’s important to check that this equals KeyEventKind::Press as otherwise your
application may see duplicate events (for key down, key repeat, and key up).
code: the KeyCode representing which specific key that was pressed.
Add a handle_key_event method to App, to handle the key events.
Next, add some methods to handle updating the application’s state. It’s usually a good idea to
define these on the app rather than just in the match statement as it gives you an easy way to unit
test the application’s behavior separately to the events.
Testing Keyboard Events
Splitting the keyboard event handling out to a separate function like this makes it easy to test the
application without having to emulate the terminal. You can write tests that pass in keyboard events
and test the effect on the application.
Add tests for handle_key_event in the tests module.
Run the tests.
You should see:
The Finished App
Putting this altogether, you should now have the following files:
Running the app
Make sure you save all the files and that the imports listed above are still at the top of the
file (some editors remove unused imports automatically).
Now run the app:
You will see the following UI:
Press the Left and Right arrow keys to interact with the counter. Press Q to quit.
Note what happens when you press Left when the counter is 0.
On a Mac / Linux console you can run reset to fix the console. On a Windows console, you may need
to restart the console to clear the problem. We will properly handle this in the next section of
this tutorial on Error Handling.
Conclusion
By understanding the structure and components used in this simple counter application, you are set
up to explore crafting more intricate terminal-based interfaces using ratatui.