Fork me on GitHub

Project Notes

#325 catsay TUI

Building the text UI catsay example from Practical Rust Projects, learning about making text UI programs with Rust.

Notes

The text-base user interface catsay example from Practical Rust Projects is used to demonstrate techniques for making text UI programs with Rust.

Building catsay

Start a new project:

$ cargo new --bin catsay
    Creating binary (application) `catsay` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ cd catsay
$ cargo run
   Compiling catsay v0.1.0 (/Users/paulgallagher/MyGithub/tardate/LittleCodingKata/rust/catsay-tui/catsay)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.49s
     Running `target/debug/catsay`
Hello, world!

Step 1: setup the app framework with cursive

The example uses the Cursive crate. Cursive is a TUI (Text User Interface) library for rust. It uses the crossterm backend by default, but other backends are available.

Use the Cursive crate. Add cursive = "0.11.2" to Cargo.toml, and update the code:

extern crate cursive;

use cursive::Cursive;

fn main() {
    let mut siv = Cursive::default();

    siv.run();
}

When invoked with cargo run, displays a simple blue screen.

Step 2: showing a dialog box

Seems I need to explicitly enable the cross term backend to get cursive to display anything. I want to use the multiplatform crossterm rather than ncurses used in the book (so I can easily run on macOS).

Update the cursive dependency and enable crossterm: cursive = { version = "0.21", features = ["crossterm-backend"] }.

extern crate cursive;

use cursive::views::TextView;

fn main() {
    let mut siv = cursive::crossterm();
    let cat_text = "{message}
\\
 \\
   /\\_/\\
  ( {eye}.{eye} )
   > ^ <";

    let cat_template = cat_text.replace("{message}", "Meow!");
    let cat_template = cat_template.replace("{eye}", "o");
    siv.add_layer(TextView::new(cat_template));

    siv.run();
}

Working nicely with cargo run:

step2

Step 3: handling simple keyboard inputs

Add a global callback to handle key events - in this case ESC key

use cursive::event::Key;
...
siv.add_global_callback(Key::Esc, |s| s.quit());

Step 4: adding a dialog box

Wrap the TextView with a Dialog box

use cursive::views::{Dialog, TextView};
...
siv.add_layer(
    Dialog::around(TextView::new(cat_template))
        .title("The cat says...")
        .button("OK", |s| s.quit())
);

step4

Step 5: multi-step dialog box

First dialog is to accept options for the cat, then show the cat accordingly.

extern crate cursive;

use cursive::traits::Nameable;
use cursive::views::{Checkbox, Dialog, EditView, ListView, TextView};
use cursive::event::Key;
use cursive::Cursive;

struct CatsayOptions<'a> {
    message: &'a str,
    dead: bool,
}

fn input_step(siv: &mut Cursive) {
    siv.add_layer(
        Dialog::new()
            .title("Please fill out the form for the cat")
            .content(
                ListView::new()
                    .child("Message:", EditView::new().with_name("message"))
                    .child("Dead?", Checkbox::new().with_name("dead")),
            )
            .button("OK", |s| {
                let message = s
                    .call_on_name("message", |t: &mut EditView| t.get_content())
                    .unwrap();
                let is_dead = s
                    .call_on_name("dead", |t: &mut Checkbox| t.is_checked())
                    .unwrap();
                let options = CatsayOptions {
                    message: &message,
                    dead: is_dead,
                };
                result_step(s, &options)
            }),
    );
}

fn result_step(siv: &mut Cursive, options: &CatsayOptions) {
    let eye = if options.dead { "x" } else { "o" };

    let cat_text = format!(
        "{message}
 \\
  \\
    /\\_/\\
   ( {eye}.{eye} )
    > ^ <",
        message = options.message,
        eye = eye
    );

    siv.pop_layer();
    siv.add_layer(
        Dialog::around(TextView::new(cat_text))
            .title("The cat says...")
            .button("OK", |s| s.quit()),
    );
}

fn main() {
    let mut siv = cursive::crossterm();

    input_step(&mut siv);

    siv.add_global_callback(Key::Esc, |s| s.quit()); // listen for ESC key and quit

    siv.run();
}

now on cargo run there’s a two step interaction:

step5a

step5b

Note: code has been updated to use Nameable trait instead of Identifiable. These were changed as part of the Cursive “great renaming”.

Credits and References

About LCK#325 Rust

This page is a web-friendly rendering of my project notes shared in the LittleCodingKata GitHub repository.

Project Source on GitHub Return to the LittleCodingKata Catalog
About LittleCodingKata

LittleCodingKata is my collection of programming exercises, research and code toys broadly spanning things that relate to programming and software development (languages, frameworks and tools).

These range from the trivial to the complex and serious. Many are inspired by existing work and I'll note credits and references where applicable. The focus is quite scattered, as I variously work on things new and important in the moment, or go back to revisit things from the past.

This is primarily a personal collection for my own edification and learning, but anyone who stumbles by is welcome to borrow, steal or reference the work here. And if you spot errors or issues I'd really appreciate some feedback - create an issue, send me an email or even send a pull-request.

Follow the Blog follow projects and notes as they are published in your favourite feed reader