TUI programming with Rust

Link4Rust
6 min readSep 25, 2023

--

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?

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:

  1. Create a Cursive Object: We start by creating a Cursive object. The cursive::default() method helps us with this task.
  2. Configure the Cursive Object: After creating the Cursive object, we configure it according to our application’s needs.
  3. 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.

result

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.

here’s what will happen when “yes” is pressed

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!

--

--

Link4Rust

Welcome to Link4Rust! Discover informative articles, tutorials, and updates about rust programming language here.