Trying Ratatui TUI
Rust Text-based User Interface Apps
Ratatui and Text-based User Interfaces
I have been trying Ratatui TUI, a Rust crate for building Text-based User Interface (TUI) apps, which typically run in the Terminal. Ratatui, along with Go-based Bubble Tea might be responsible for the recent growth in TUIs. Some examples of TUI apps are Lazygit, glow, Soft Serve, ATAC and rlt. C++ programmers are not left out — FTXUI is a modern C++ library for building TUIs.
You use the Ratatui crate to build command line apps in Rust, though it is a little different to other Rust libraries like clap and inquire. clap focusses on managing and parsing command line flags (its name is an acronym: Command Line Argument Parser). inquire, is designed for interacting with the user, prompting them for information via the command line. Ratatui is different to both, and gives you more control over the text-based app’s user interface layout and handling interaction.
🧑🏽🎓 Ratatui Learning Resources
Ratatui is a powerful library, so there is a little to learn, and the project provides three beginner tutorials to help flatten the learning curve. These are:
- Ratatui Hello World Tutorial, which is a good starting point if you are quite new to the Rust ecosystem;
- Counter App, which takes things up a level and introduces immediate mode rendering; and
- JSON Editor, which introduces more sophisticated layouts.
If this is your first Rust app, you might want to start with either Command line app in Rust, or probably The Rust Programming Language, both free web books.
🧱 What I Built
After following the tutorials, a nice extension was to create a basic numbers game, inspired by the UK TV Countdown quiz. For this arithmetic challenge, the player randomly picks six numbers, then using +, -, * and / operators, tries to arrive at a target.
This little game provided a natural extension to the JSON editor tutorial, mentioned above.
⚙️ Project setup
Beyond the ratatui
and crossbeam
crates used in the JSON tutorial, I added num_parser
and rand
. Naturally, rand
was useful for generating the target and helping select the six, initial numbers.
[package]
name = "countdown-numbers"
version = "0.1.0"
edition = "2021"
license = "BSD-3-Clause"
repository = "https://github.com/rodneylab/countdown-numbers"
rust-version = "1.74"
description = "Trying Ratatui TUI 🧑🏽🍳 building a text-based UI number game in the Terminal 🖥️ in Rust with Ratatui immediate mode rendering."
[dependencies]
crossterm = "0.27.0"
num_parser = "1.0.2"
rand = "0.8.5"
ratatui = "0.26.3"
I set the game up, so the player typed in their solution attempt, with their chosen numbers, using familiar algebraic notation. For example, let’s say the target is 326
and the six numbers are 1
, 2
, 2
, 75
, 50
and 25
. Here, it is possible to reach the target exactly, and the player could enter their solution into the Ratatui app as:
(2 * 2 * 75) + 25 + 1
The num_parser
crate can parse that expression and yield the result (326
), significantly reducing the number of lines of code I had to write.
That was all I needed for dependencies: crossterm
, num_parser
, rand
and ratatui
.
🧑🏽🍳 What I Liked about Ratatui
📚 Fantastic Docs
The three tutorials are very well written, and provide a great introduction to using Ratatui, including unit testing your app. Beyond the tutorials, I found the official Ratatui API docs were detailed enough to solve all the queries I had as I built the app.
📦 Leveraging crates.io
Being able to leverage the Rust crate ecosystem was a huge help. I mentioned num_parser
did a lot of heavy-lifting. This was quick to find num_parser
using crates.io. Although a similar C++ library might exist, I don’t think I could have found it so easily. I am not familiar enough with the Go ecosystem to comment on how that would have worked out for me, drop a comment below if you are familiar, or if you are a C++ aficionado, and know an easy way to find C++ libraries.
🖥️ Trying Ratatui TUI: Immediate Mode
I kept the app structure from the JSON tutorial, that is, with three main source files:
src/main.rs
: contains the main loop, draws the UI and listens for UI events, immediately calling app methods;src/app.rs
: manages app state and provides helper functions the app calls on user event; andsrc/ui.rs
: generates the UI based on the current app state.
The immediate mode pattern worked well, writing how the app UI should look for any given state, with Rust pattern matching helping me get going quicker, making sure I covered all possible states, with no surprises.
As an example of how this worked, we might look at the initial number picking. The player presses [ to pick a small number and ] for a large one. These key presses are captured in event listener code in src/main.rs
:
match app.current_screen {
CurrentScreen::Introduction => {
if key.code == KeyCode::Enter {
app.current_screen = CurrentScreen::PickingNumbers;
}
}
CurrentScreen::PickingNumbers => match key.code {
KeyCode::Enter => {
if app.is_number_selection_complete() {
app.current_screen = CurrentScreen::Playing;
}
}
KeyCode::Char(']') => {
app.pick_random_large_number();
}
KeyCode::Char('[') => {
app.pick_random_small_number();
}
_ => {}
},
// TRUNCATED...
}
So pressing those square bracket keys triggers an app state update (src/app.rs
):
pub fn pick_random_large_number(&mut self) {
if let Some(index_value) = self.random_available_large_number_index() {
let result = self.available_large_numbers[index_value];
let picked_index = self.selected_numbers.iter().position(|&val| val.is_none());
if let Some(picked_index_value) = picked_index {
if result.is_some() {
self.selected_numbers[picked_index_value] = result;
self.available_large_numbers[index_value] = None;
};
}
}
}
Then the UI model is updated (src/ui.rs
):
fn create_selected_numbers_block(app: &App) -> Paragraph {
let mut selected_numbers_text = app.selected_numbers.into_iter().fold(
vec![Span::styled("Numbers: ", Style::default())],
|mut accum, val| {
if let Some(value) = val {
accum.push(Span::styled(
format!("{value} "),
Style::default().fg(Color::Green),
));
} else {
accum.push(Span::styled("_ ", Style::default().fg(Color::Green)));
};
accum
},
);
// TRUNCATED...
}
Hence, the current app state gets reflected on the next enumeration of the event loop.
There is a link further down to the complete code for the project.
🏁 Trying Ratatui TUI: What Next?
So far, I have a minimal proof of concept. I would like to remove some rough edges. In particular:
- add some fireworks to make the victory screen feel a little more of a celebration;
- add sound feedback when the user hits keys; and
- improve gameplay.
For improving gameplay, there are already different messages depending on how close to the target you got. However, I would like to take this to the next level, adding a game timer and putting best times on a high-score board. This definitely provides motivation for me playing Sudoku Pi (also written in Rust), and I hope it will translate to this game.
🙌🏽 Trying Ratatui TUI: Wrapping Up
In this post on trying Ratatui TUI, we looked got an introduction to immediate mode user interfaces in Rust. In particular, we saw:
- TUI alternatives to Rust’s Ratatui in Go and C++;
- other Rust Terminal app tooling and how Ratatui is different; and
- some learning resources for getting started with Ratatui.
I hope you found this useful. As promised, you can get the full project code on the Rodney Lab GitHub repo. I would love to hear from you, if you are also new to Rust game development. Do you have alternative resources you found useful? How will you use this code in your own projects?
🙏🏽 Trying Ratatui TUI: Feedback
If you have found this post useful, see links below for further related content on this site. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on X (previously Twitter) and also, join the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Game Dev as well as Rust and C++ (among other topics). Also, subscribe to the newsletter to keep up-to-date with our latest projects.