In the first part of this tutorial, we set up our project and created a new Rust binary called pound. So far it only displays “Hello world!” in the terminal. Let us now try and read key presses from the user.
This code contains a lot of information, so let’s go over it. To obtain user input, we need to bring the io
(input/output) library into scope. The io
library comes from the standard library ( std
). std::io::Read
is a trait
which provides .read
method. The stdin
function returns an instance of std::io::Stdin
, which is a type that represents a handle to the standard input for your terminal. Since we want to read 1 byte at a time, we pass [0;1]
to the read
function. When read()
reaches end of file it returns 0.
To exit the above program, press Ctrl-D to tell read()
that it’s reached the end of file. Or you can always press Ctrl-C to signal the process to terminate immediately.
Press q To Quit ⚒
Let’s add a new way to quit the program.
Now to quit the program, you need to enter text with ‘q’ in it and press Enter
. The program would read each letter, store it in the buf
variable and then check if that letter is the same as q
. Note the [b'q']
. buf
is of type [u8;1]
and can only be compared with a similar type. The prefix b
provides byte literal which is equivalent to u8
.
Instead of
buf != [b'q']
you can writebuf[0] != b'q'
in this case.
Canonical and Raw Mode ⛓
By default, your terminal starts in canonical mode. This means that the keyboard input is sent to your program after you press Enter
key. The input is also echoed in the terminal that’s why you can see what you typed. This mode can be helpful since you can edit what you typed, and use keys like delete
and backspace
to edit before you press Enter
so the program reads your input.
What we want is raw mode, which is quite opposite of canonical mode. Fortunately the crossterm
crate provides an easy way to get the terminal into raw mode. Recall that in the first part, we added the crossterm
dependency.
Since we’re using the
crossterm
crate, we don’t have to manually modify the terminal attributes ourselves.
Enable Raw Mode 🔓
As already mentioned, the function to enable raw mode is found in crossterm
crate:
Now let us try it out. Compile and run the program. You’d realize that the keys you type aren’t showing again. You’d also realize that you don’t have to press q
and then Enter
to exit the program. Only pressing q
exits the program. You may also notice that no input is displaying on the terminal even after the program exits. Let us modify the code to restore the terminal state when the program exits.
Disable Raw Mode 🔐
It may seem adding terminal::disable_raw_mode()
to the last line of our code might do the trick, but that would introduce a bug in our code. Let’s try it out.
Now suppose a function after enable_raw_made()
throws an error and panics, disable_raw_mode()
won’t be called and the terminal would remain in raw mode. Try it by placing panic!("Error occurred")
anywhere after terminal::enable_raw_mode()
. To fix let’s create struct
called CleanUp
:
Let’s break it down. We created a new struct CleanUp
and implemented Drop for it. Now drop()
is called in cases such as when the instance of the struct (in our case, the _clean_up
variable) goes out of scope normally (in this case when main()
returns) or when there’s a panic while the instance is still in scope. Run the program and observe the difference compared to the one with the bug
The
panic!()
line can be added anywhere after theterminal::enable::raw_mode()
line to observe similar effects
Now we’re sure the terminal returns to it’s original state after our program exits, we can remove the panic!()
.
You might have also realized that Ctrl+D and Ctrl+C doesn’t end the program again. Only q
ends the program. Let’s take a peek as to what’s happening under the hood
Display Keypresses 📟
Modify the code:
.is_control()
tests whether a character is a control character. Control characters are non-printable characters that we don’t want to print to the screen. ASCII codes 0–31 are all control characters, and 127 is also a control character. ASCII codes 32–126 are all printable. (Check out the ASCII table to see all of the characters)
Note that we’re using println!()
with \r
as the last argument. This is because we enabled raw mode. Remove \r
like this and notice the difference:
Using println!()
without \r
you’ll see that the newline characters we’re printing are only moving the cursor down, and not to the left side of the screen. This constantly moves the output to the right. \r
moves the cursor back to the left side of the screen.
Now it’s time to try out our program and discover a few things. Run the program and try out keys such as the arrow keys, or Escape
, or Page Up
, or Page Down
, or Home
, or End
, or Backspace
, or Delete
, or Enter
. Try key combinations with Ctrl, like Ctrl-A, Ctrl-B, etc.
You’ll notice a few interesting things:
- Arrow keys, Page Up, Page Down, Home, and End all input 3 or 4 bytes to the terminal:
27
,'['
, and then one or two other characters. This is known as an escape sequence. All escape sequences start with a27
byte. Pressing Escape sends a single27
byte as input. - Backspace is byte
127
. Delete is a 4-byte escape sequence. - Enter is either byte
10
, which is a newline character, also known as'\n'
or byte13
, which is carriage return, also known as\r
. - Ctrl-A is
1
, Ctrl-B is2
, Ctrl-C is… oh, that terminates the program, right. But the Ctrl key combinations that do work seem to map the letters A–Z to the codes 1–26
Now we know how various keypresses translate into the bytes.
Migrating Project To Use crossterm 🦾
It could be difficult keeping all the various keys and their corresponding byte literal in mind. You could keep referring but that can be time consuming. The crossterm
crate also provides abstraction over various key events so we don’t have to keep in mind that Arrow Up
maps to 27[A
and so on.
Let us use those crossterm
features:
Now that’s a lot. Let us try and wrap our head around it. loop { .. }
works similar to while()
. We moved our condition from the while
function and placed it in the loop
block to make it more readable. We’re using crossterm::event::read()
instead of the default std::io::read()
. This makes it easier to interpret the events from the terminal. event::read()
returns an Event
, which is an enum
. Since we’re only interested in key presses for now, we check if the Event
returned is a Key
. We then check if the key pressed is q
. If the user pressed q
, we break out of the loop
and the program terminates.
Run the program and type various key combinations. This would help in understanding how the program works.
Timeout for read()
⏳
Currently read()
will wait indefinitely for input from the keyboard before it returns. What if we want to do something like animate something on the screen while waiting for user input? Or what if we want to end the program after some time of idleness? We can set a timeout, so that read()
returns if it doesn’t get any input for a certain amount of time. We will use the crossterm::event::poll
function along with read
:
event::poll
checks whether an Event
is available within the specified time given. Play around with the given time. If no Event
is available within the given time, poll
returns false
.
Error handling 🚫
Since we’re done migrating our project to use crossterm
, let’s handle potential errors. So far we’ve been using .expect(" .. ")
. Having to write .expect()
every time could get tiring. Let’s use a shorthand provided by Rust. Let us modify our main()
function this way:
The ?
operator can only be used in a method that returns Result
or Option
so we have to modify our main()
to return a Result
. The crossterm::Result<T>
can be expanded to std::result::Result<T, std::io::Error>
. So for our main()
function, the return type can be translated to std::result::Result<(), std::io::Error>
. Let’s take the poll
function for instance. It returns std::result::Result<Event, std::io::Error>
. Since the second argument in Result
returned by poll
(i.e. std::io::Error
) is the same type as the Result
returned by main()
, we can use the ?
operator on poll
function within the main()
method.
Next Steps 👣
Let’s conclude this part of the tutorial by quickly reviewing what we have done. First, we read keypresses, then we modified the program to exit when q
is pressed. We then enabled raw mode (which enabled us to see how various keys such as Home
is translated into bytes) and also found a way to properly disable raw mode when our program exits. We also integrated crossterm
to our project. Lastly we modified how we handled errors.
In the next part, we’ll do some more low-level terminal input and output handling, and use that to draw to the screen and allow the user to move the cursor around!