Now we'll look at doing text-mode graphics with the termion crate. This crate is written in Rust, so it's perhaps the most "native" of "higher-level" Terminal User Interfaces (TUIs), with ANSI being a lower-level.
Like we did with ncurses, we'll need to add the crate (which, like ncurses, is available from "crates.io") to our project. But first, to keep it separate from other earlier work we've done, let's create a new project.
Move into the "~/projects/RUST" directory and run:
$ cargo new termion_experiment
Now take a look at the just-created "Cargo.toml" file.
The above is mine before any changes have been made to it.
To add the termion installation line in "Cargo.toml", we could manually edit the file to add this line, or we can let Cargo do the work for us, with this command:
$ cargo add termion
which adds a dependency line like so:
Now you have told Cargo that "termion" of a certain version number is needed by this "termion_experiment" project, and it will go get it from "crates.io" the next time you try to compile "main.rs". Let's do that now. You know the procedure:
$ cargo run
Your results should look something like this:
/projects/RUST/termion_experiment$ cargo run
Compiling libc v0.2.149
Compiling numtoa v0.1.0
Compiling termion v2.0.1
Compiling termion_experiment v0.1.0 (/home/westk/projects/RUST/termion_experiment)
Finished dev [unoptimized + debuginfo] target(s) in 1.05s
Running `target/debug/termion_experiment`
Hello, world!
~/projects/RUST/termion_experiment$
Use termion to clear the screen.
If you compile and run the "main.rs" program as it is, it doesn't first clear the screen. We can use the termion crate to clear the screen before displaying "Hello, World!"
Notice that in the "print!" statement we had to use the entire path to the "All" function, starting at "termion", then going to "clear", and then on to "All". Soon we'll use some "use" statements to simplify these statements.
Unlike with ncurses, we don't need to do any special housekeeping/setup steps when using termion for text-mode graphics.
Use termion to Add Some Color
Suppose you wanted to add a little color.
Simplify paths.
Notice that although the path to the "All" function and the path to the "Red" and "Reset" functions start out the same, starting at "termion::", the "All" function descends through the "termion::clear::" path whereas the "Red" and "Reset" and "Fg" (Foreground color) functions descend throught the "termion::color::" path. So if we use a "use" statement to simplify these paths, we'll need several of them, like so:
We can simplify that further:
If we wanted to add more colors, we could add the color of each name we wanted, or we could just pull all the color names in, like so:
However, notice the line:
print!("{}", All);
If it wasn't for the comment above it, a casual reader of our program (or you, yourself, six months from now), might wonder, "What is this 'All'?" This is a case where it might make for more readable code to leave the path in place, like so:
print!("{}", termion::clear::All);
Doing this, with the rest of the program left as-is, will cause the Cargo compiler to generate a warning that the import "use termion::clear::All" is unused (which you won't see because the screen clears too quickly for us to see that warning). We can ignore that warning, as it's not hurting anything, or we could remove that import.
If you want to see that warning, you can add the following code temporarily:
Unless you want to leave in this "Press ENTER to continue", don't forget to remove the above code when you're finished using it.
To remove that now-unused "use termion::clear::All" import, just delete that line, like so:
Use constants instead of variables.
We can probably save a tiny bit of memory, and slightly improve our run-time speed, by using constants instead of variables. If we declare the constants in the "main()" function, that function is the only place those constants will be known. If we declare them outside any function, they'll be known to all parts of the program within our "main.rs" file, such as in other functions we may later create.
Use Termion to Determine Screen-Size
It's often helpful to know the dimensions of the terminal window in which you're working. Here's how to do that:
When you compile/run this (cargo run), you should see something like:
The screen dimensions are: 95 columns x 39 rows
Hello, world!
~/projects/RUST/termion_experiment$
Of course, your screensize will likely be different.
The "terminal_size()" function returns a type of data called a "Result". A "Result" type is like an Amazon delivery package with a sign on the outside that either says "Ok" or "Error". If it's "Ok", then the box's contents are what you expect; if it's "Err[or]", then there's a note that explains a little of what went wrong -- "Lost", "Broken", "Shipper went out of business", etc.
In general there are three ways to handle a return type of "Result":
The proper way: Do error-handling based on the "Result".
Ignore the "Result", risking a "panic"-crash of the program. Sometimes it's easiest to just ignore the sign on the outside and rip into the box and hope for the best. We can do that by adding ".unwrap()" to the function call, like terminal_size().unwrap(). If there really is a problem with the contents, the program will panic and die. But a lot of time we trust that the program will work, and that if it does crash, it won't hurt anything.
A slightly better way to handle the "Result" type is to use ".expect()" instead, as we did above. Although the program will still "panic"-crash if there's an error, you can include your own customized error along with the "Err" message provided in the shipping carton, in our case, "Error getting terminal size."
In this case of "terminal_size()", the contents of the Result-type "shipping box" is a "tuple". A tuple, instead of being one value assigned to one variable name, is a collection of more than one value. In this case, it is two values. But "tuple" has no relation to "two"; it could be three values, or thirty, or three-hundred. In our case with "terminal_size()", we're getting back a tuple of two values, which we're binding to two variable names, "cols" and "rows".
Then we're printing those two values.
If we wanted to, instead of breaking up the tuple into two variables, we could leave it as a tuple, and give the tuple a name like "screen-dimensions" (or "size", or "dims", or whatever), and then access the contents as ".0" and ".1". (If there was a third element to the tuple, it would be accessed with ".2", etc.) That would look something like this:
Using the discrete values of "cols" and "rows" is more self-documenting, but a tuple can be a little easier to pass around into functions and etc. Pros and cons either way. For now at least, I'll stick to the discrete values.
Move the Cursor to Where You Want It
If you wanted to draw an asterisk in the center of the screen, you could do this:
If you don't want to specify the "termion::" path on the "cursor" lines, you could modify the "use" statement:
For reference, here's the entire program.
Use UTF-8 Characters
A pretty big advantage of termion over ncurses is that termion is UTF-8-friendly. This means you can print many more characters than what is in the standard ASCII chart. For example, let's replace the asterisk in the above program with a tiny airplane. NOTE: Not all editors (e.g., nano) will recognize the Ctrl-Shift-U key-sequence; in such a case, just copy/paste the symbol from below.
Animate the cursor across the screen
Suppose we wanted to fly that tiny airplane across the screen, left to right. We just set up a loop to move the cursor across the screen, print the plane, put in a slight delay so our eyes can register the plane before it moves on to the next space, print a space over the plane to erase it, and we're done.
If you run this program as it, you may be surprised to see that it doesn't work. You never see the plane printed on the screen. Whaa-aa-aah-h?
There's a feature about the "print!()" and "println!()" macros that often confuses newcomers to Rust. "print!()" does not actually display output to the terminal window. Instead, it (like "println!()" does also) buffers its output in a "buffer". Then when a certain trigger occurs, that buffer is flushed to the terminal, and it is at that point that the printed material actually shows up on the terminal window. One of the most common triggers that flushes this output buffer, and the trigger that causes the "println!()" macro to immediately display output, is a newline.
So the easy fix for our program above is to add a newline when we want the buffer flushed to the terminal window. We could convert all the "print!()" statements to "println!()"; or we could convert just one or two as needed to accomplish what we want to do. Or we could add a "\n" to one or more of our print statements.
The reason I did not use "println!()" throughout is because that would move the insertion point (the "cursor") down one line and all the way to the left of the screen, which would then mean moving the insertion point farther than necessary when it may need to be up a line and farther to the right. This could, in theory at least, save some execution time. If this were a game, you might like that little extra speed savings in your gameplay. In our case, with the program written as it is, it's not really a concern. Modifications could be made which might make the program more efficient, but it's efficient enough as-is, and pretty understandable as-is, so we won't try for those mods. However, you may want to experiment with the below mods to see what happens in each case.
Get rid of the block cursor
When you run the program, you may have noticed the insertion point ("cursor") hanging out on the left-hand side of the window as the plane flies across. Kindda ugly. Let's turn that off:
There, that's better.
A better delay
The loop we used for a delay is just a quick-and-dirty method of wasting some CPU cycles to put a slight delay into the loop for moving the plane across the screen. But Rust has a better mechanism than that, which is dependent on actual time passed, rather than simply counting to some number repeatedly. If you have read the lesson Text-Mode Graphics, in Rust, using 'curses', you should already be familiar with this function.
You can simplify the paths in the program code a bit with a "use" statement:
Keyboard Input - Raw Mode
Raw Mode makes several changes to the way the terminal behaves. As Ticki wrote six years ago, it makes these changes:
It disables the line buffering: As you might notice, your command-line application tends to behave like the command-line. The programs will first get the input when the user types \n. Raw mode makes the program get the input after every key stroke.
It disables displaying the input: Without raw mode, the things you type appear on the screen, making it insufficient for most interactive TTY applications, where keys can represent controls and not textual input.
It disables canonicalization of the output: For example, \n represents “go one cell down” not “break the line”, for line breaks \n\r is needed.
There are two modes in which terminals can operate:
Canonical mode: In canonical mode, the inputs from the user are processed line by line, and the user has to press the Enter key for the characters to be sent to the program for processing.
Non-canonical or raw mode: In raw mode, terminal input is not collected into lines, but the program can read each character as it is typed by the user.