How to Read a Keypress from the Keyboard in Rust, similar to getch()

Kent West - kent.west@{that mail that swore to do no evil}

  1. getchar
  2. getch-rs
  3. termion

Using the getchar crate

I struggled for days to figure out how to get a simple keypress from the keyboard. Finally! I found a crate on crates.io that makes it as simple as using "getch()" from the ncurses library.

The reason I did not use ncurses is because it requires an initialization step, which swaps out the screen for a new blank screen, which then disappears when you're finished with the curses mode.

I would have used the termion crate, but search high and low on the 'Net for a simple snippet of code that shows just the absolute bare minimum for getting a keypress, and you'll get a headache, and no solution, unless you're smarter than I am (granted, that's not a very high bar, but still...) I do not understand why coders will not give the simplest, barest-possible, COMMENTED!!!! code snippets so the newbie can get started. (Yes, I'm angry about this; Rust could be such a beautiful platform, if non-life-long coders could learn how to code in it from simple examples and good explanations.)

Here's how to get a keypress from the keyboard in Rust:

Configure 'Cargo.toml'
$ cargo add getchar
src/main.rs
fn main() {
    let keypress = getchar::getchar().unwrap(); // getchar() returns an "Option<char>" type that must be unwrapped to get to the 'char' goody inside.

    println!("You pressed {}", keypress);
} // end of main()

And that's it. Simple.

Thanks to the author of getchar, Maks Rawski! And for those who want to see the entire code for this crate, you can see it on GitHub, or below. Yep, it's that simple. That's pretty much what I've been looking for, for days. (Comments would help, as I don't entirely understand it, but it's enough I think I could come to understand it if need be).

lib.rs
use std::io;
use std::io::{Read, Write};
use termion::raw::IntoRawMode;

pub enum Error{
    Exit,
    Unknown,
}

pub fn getchar() -> Result<u8, Error> {
    let mut buffer = [0];
    let stdout = io::stdout().into_raw_mode().unwrap();
    let mut stdin = io::stdin();

    stdout.lock().flush().unwrap();

    if stdin.read_exact(&mut buffer).is_ok() {
        if buffer[0] == 3 {
            Err(Error::Exit)
        } else {
            Ok(buffer[0])
        }
    } else {
        Err(Error::Unknown)
    }
}
Back to Top

Using the getch-rs crate

This alternative method uses a crate that has more features than the above getchar, most notably it works with things like the arrow keys, whereas getchar does not.

$ cargo add getch-rs

main.rs
use getch_rs::{Getch, Key};

fn main() {
    let g = Getch::new();

    // This section demonstrates processing a single keypress.
    let result = g.getch().unwrap(); // If you omit the ".unwrap()", next line should use "Ok(Key::Char('b'))".
    if result == Key::Char('b') {
        println!("Got it!");
        println!("{:?} tells me that you pressed = 'b'.", result);
    };

    // This section demonstrates continual processing of keypresses.
    loop {
        match g.getch() {
            Ok(Key::Char('q')) | Ok(Key::Char('Q')) => break, // Time to quit the program
            Ok(Key::Up) => println!("You pressed the Up arrow."),
            Ok(Key::Char(whatever)) => println!("{}", whatever), // Display whatever other key is pressed (and displayable)
            Err(e) => println!("{}", e), // If there's an error, display the error message.
            _ => {}
        }
    }
} // main()

The keypress returned by the first example method above, getchar, returns the keypress in a "package" of an "Option<char>" type. This one, and the last one below, return their keypresses in a "Result" type. Both of these "package" types are like a delivery package from Amazon or UPS or FedEx, etc. The "Result" type, in particular, is like a delivery package with a big label on the outside that says either "Ok" or "Err". If it's an "Err", the contents won't be what you asked for, but rather an error message. If it's an "Ok", the contents will be what you asked for (hopefully). The ".unwrap()" unwraps the package, ignoring what the outer label says. You may wind up with a crashed program. Using ".expect()" instead of ".unwrap()" also opens the package, ignoring the outer label, but gives you the ability to add your own custom error message in the event of a program crash resulting from an error package. Ideally you'd properly handle the error situation, like we did here, but sometimes that's more trouble than it's worth.

We could unwrap() "g.getch" (g.getch().unwrap(), like we did above using getchar and like we do below using termion. This example uses both methods, using the unwrap in the first section, and doing better error-handling in the second section. As mentioned in the previous paragraph, the third possibility is to use expect(), which is like unwrap, except it allows us to add our own custom message to an error condition.

The "whatever" above is just a variable name. Most often you'll see the variable name "c" (for "character") used.

Back to Top

Using the termion crate in raw mode

This is essentially a sligtly different way of what the author of "getchar" did, as seen in the "lib.rs" code of that first example above. His is just more generic, returning the pressed key from a function, rather than getting the pressed key and doing something with it, all in one section of code.

main.rs
use std::io::{stdin, stdout, Write};
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;

fn main() {
    let stdin = stdin();
    let mut stdout = stdout().into_raw_mode().unwrap(); // The screen must be in raw mode.

    for c in stdin.keys() { // std::io::stdin seems to be modified by the 'use..TermRead'. W/o it, ".keys()' doesn't work.
        // Forever loop, watching for keypresses.
        match c.unwrap() {
            // Check keypress for a match to "q", "Q", left-arrow, or right-arrow.
            Key::Char('q') | Key::Char('Q') => {
                break;
            }
            Key::Char(c) => println!("You pressed'{}'.", c),
            Key::Left => println!("Left arrow was pressed"),
            Key::Right => println!("Right arrow was pressed"),
            // Any other key we assume we can try to display.
            _ => write!(stdout, "{}", termion::cursor::Show).unwrap(),
        }
    }
} // main()
Back to Top