In this article, we’ll explore the world of Text User Interface (TUI) programming using Rust. TUIs provide a versatile way to create interactive command-line applications with rich graphical user interfaces. To get started, we’ll use the Cursive library, a popular Rust library for building TUI applications.
Outline:
- Why Rust?
- Why Cursive?
- Adding Cursive as a Dependency.
- Basic Structure of a Cursive Application.
- Adding a Way to Quit the Application.
- Views in Cursive.
- Dialogs
- Dialogs — Title
- Dialog — Button
- Conclusion
Why Rust?
TUI development requires a language that can provide safety, performance, and a robust ecosystem of libraries. Rust, with its unique set of features and design principles, is an excellent choice for TUI development.
Why Cursive?
Cursive uses declarative UI: the user defines the layout, then cursive handles the event loop. Cursive also handles most input (including mouse clicks), and forwards events to the currently focused view. User-code is more focused on “events” than on keyboard input.
It is well suited for more complex applications, with nested view trees, menus and pop-up windows.
Adding Cursive as a Dependency
To begin, let’s add Cursive to the list of dependencies in our Cargo.toml
file. Ensure you have the following configuration:
[package]
name = "cursive_example"
version = "0.1.0"
[dependencies]
cursive = "*"
This configuration sets up our Rust project to use the Cursive library.
Basic Structure of a Cursive Application
A typical Cursive application consists of three main phases:
- Create a Cursive Object: We start by creating a Cursive object. The
cursive::default()
method helps us with this task. - Configure the Cursive Object: After creating the Cursive object, we configure it according to our application’s needs.
- Execute the Cursive Object: Finally, we run the Cursive object to start the application.
Here’s the simplest possible Cursive application:
fn main() {
let mut siv = cursive::default(); //Create a Cursive Object
siv.run(); //Execute the Cursive Object
}
If you run this program, you’ll see a blank application window.
Adding a Way to Quit the Application
Cursive processes user input as events. Many events are ignored by default. To allow the user to quit the application by pressing ‘q’, we can use the add_global_callback
method on the root Cursive object:
siv.add_global_callback('q', |s| s.quit());
This code snippet adds a global callback that listens for the ‘q’ keypress and quits the application when triggered.
Views in Cursive
Views are the core building blocks of the user interface in a Cursive application. They define what gets displayed on the terminal. Views can be simple elements like text or complex widgets like checkboxes.
To display a text message, we can use the TextView::new("text")
constructor. Initially, the screen is empty, so we need to create a layer using add_layer
. The argument to add_layer
should be the view we want to display as a new layer.
Here’s an example that displays a “Hello TUI!” message and allows the user to quit the application by pressing ‘q’:
use cursive::views::TextView;
fn main() {
let mut siv = cursive::default();
// Add a global callback to quit the application when 'q' is pressed
siv.add_global_callback('q', |s| s.quit());
// Add a TextView with our message as a new layer
siv.add_layer(TextView::new("Hello TUI! Press <q> to quit."));
// Run the Cursive application
siv.run();
}
Running this program will display the “Hello TUI!” message, and you can exit by pressing ‘q’.
Dialogs
Dialogs are commonly used to create interactive and user-friendly text-based pop-up windows within TUI applications. They allow you to present information to the user and gather input through buttons and callbacks, enhancing the user experience.
Instead of using TextView
directly, let's use Dialog
, which is a wrapper that surrounds another view and optionally includes a title and/or buttons.
The Dialog::around
function accepts a view directly, so we can provide the TextView
directly:
use cursive::views::Dialog;
use cursive::views::TextView;
fn main() {
let mut siv = cursive::default();
siv.add_layer(Dialog::around(TextView::new("Question 1")));
siv.run();
}
Since creating a dialog window around a text view is a common task, Dialog::text
is a function that does this directly, making our code shorter (and we no longer need to import cursive::views::TextView
):
siv.add_layer(Dialog::text("Empty"));
If we run this code, we’ll see a dialog window with no title or buttons. To add a title, we can use the Dialog::title
method:
siv.add_layer(Dialog::text("did you do the thing?").title("This is the title"));
Button
Our dialog looks better than a TextView
alone, but it's still missing some action. Adding some buttons there will help.
Just like with the title, Dialog
has a Dialog::button
method for adding buttons with associated actions.
Here’s how you can add a button using Dialog::button
:
siv.add_layer(Dialog::text("...")
.title("Did you do the thing?")
.button("Yes", |s| s.quit()));
.button("No", |s| s.quit()));
.button("Uh?", |s| s.quit()));
In this example, dialog includes three buttons: “Yes,” “No,” and “Uh?,” and all of them have the action to quit the program when clicked. However, you can customize the action by replacing |s| s.quit()
with the name of your custom function without parentheses.
Let’s explore this in a more practical context:
use cursive::Cursive;
use cursive::views::Dialog;
fn main() {
let mut siv = cursive::default();
siv.add_layer(Dialog::text("This is a survey!\nPress <Next> when you're ready.")
.title("Important survey")
.button("Next", show_next));
siv.run();
}
fn show_next(_: &mut Cursive) {
// Leave this function empty for now
}
In this code, after the user clicks “Next,” we want to hide the current dialog and show a new one. We achieve this using Cursive::pop_layer
to remove the current layer.
To better understand how pop_layer
works, let's break down the process with comments:
// Try running this code on your machine!
use cursive::views::TextView;
use cursive::views::Dialog;
use cursive::Cursive;
fn main() {
let mut siv = cursive::default(); // Create a new Cursive instance.
siv.add_layer(Dialog::text("Are you of legal age?") // Add a dialog layer with a title, text, and buttons.
.title("Question 1")
.button("Yes", yes) // Add a "Yes" button with a callback yes function.
.button("No", no)); // Add a "No" button with a callback no function.
siv.run(); // Start the Cursive event loop.
}
fn yes(s: &mut Cursive) {
s.pop_layer(); // Remove the current dialog layer.
s.add_layer(TextView::new("Good! You can proceed.")); // Add a TextView layer with a message.
}
fn no(s: &mut Cursive) {
s.pop_layer(); // Remove the current dialog layer.
s.add_layer(TextView::new("You can't proceed!")); // Add a TextView layer with a message.
}
As you’ve seen, the Dialog
view is a nice way to present a TextView
, but it also works with any other content. Actually, most layers you'll add will start with a Dialog
wrapping some other view.
Conclusion
This article provides a solid starting point for building Text-based User Interfaces (TUI) with Rust and the Cursive library. With this foundation, you can confidently explore the documentation and delve into more advanced TUI development.
If you’d like to see a follow-up or more advanced tutorials, please consider leaving a clap or comment. Happy coding!