Basic Counter App
A full copy of the code for this page is available in the github repository for the website at:
https://github.com/ratatui/ratatui-website/tree/main/code/tutorials/counter-app-basic.
Create a new project
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 the ratatui::init
and ratatui::restore
methods and then creates and runs the App (defined later). It defers propagating the return of
App::run()
’s result 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:
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 App
s
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 equalsKeyEventKind::Press
as otherwise your application may see duplicate events (for key down, key repeat, and key up).code
: theKeyCode
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
.