52

I want to run an executable that blocks on stdin and when a key is pressed that same character is printed immediately without Enter having to be pressed.

How can I read one character from stdin without having to hit Enter? I started with this example:

fn main() {
    println!("Type something!");

    let mut line = String::new();
    let input = std::io::stdin().read_line(&mut line).expect("Failed to read line");

    println!("{}", input);
}

I looked through the API and tried replacing read_line() with bytes(), but everything I try requires me to hit Enter before read occurs.

This question was asked for C/C++, but there seems to be no standard way to do it: Capture characters from standard input without waiting for enter to be pressed

It might not be doable in Rust considering it's not simple in C/C++.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
bantl23
  • 879
  • 2
  • 9
  • 8
  • 4
    This is a platform issue, not a language one. On windows there are character input functions, but on unix/linux you have to take the terminal out of line buffered mode. – Chris Stratton Oct 12 '14 at 03:42
  • You can use the `getch` function from the mentioned SO link. You just have to compile that into a shared object, and use it from Rust: https://gist.github.com/ihrwein/a4558d63d9250ee0bbf6 You will need a C compiler and it works only on Linux (at least I tested it there). – Tibor Benke Jan 05 '16 at 19:50

4 Answers4

19

While @Jon's solution using ncurses works, ncurses clears the screen by design. I came up with this solution that uses the termios crate for my little project to learn Rust. The idea is to modify ECHO and ICANON flags by accessing tcsetattr through termios bindings.

extern crate termios;
use std::io;
use std::io::Read;
use std::io::Write;
use termios::{Termios, TCSANOW, ECHO, ICANON, tcsetattr};

fn main() {
    let stdin = 0; // couldn't get std::os::unix::io::FromRawFd to work 
                   // on /dev/stdin or /dev/tty
    let termios = Termios::from_fd(stdin).unwrap();
    let mut new_termios = termios.clone();  // make a mutable copy of termios 
                                            // that we will modify
    new_termios.c_lflag &= !(ICANON | ECHO); // no echo and canonical mode
    tcsetattr(stdin, TCSANOW, &mut new_termios).unwrap();
    let stdout = io::stdout();
    let mut reader = io::stdin();
    let mut buffer = [0;1];  // read exactly one byte
    print!("Hit a key! ");
    stdout.lock().flush().unwrap();
    reader.read_exact(&mut buffer).unwrap();
    println!("You have hit: {:?}", buffer);
    tcsetattr(stdin, TCSANOW, & termios).unwrap();  // reset the stdin to 
                                                    // original termios data
}

One advantage of reading a single byte is capturing arrow keys, ctrl etc. Extended F-keys are not captured (although ncurses can capture these).

This solution is intended for UNIX-like platforms. I have no experience with Windows, but according to this forum perhaps something similar can be achieved using SetConsoleMode in Windows.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
dojuba
  • 2,199
  • 1
  • 18
  • 16
15

Use one of the 'ncurses' libraries now available, for instance this one.

Add the dependency in Cargo

[dependencies]
ncurses = "5.86.0"

and include in main.rs:

extern crate ncurses;
use ncurses::*; // watch for globs

Follow the examples in the library to initialize ncurses and wait for single character input like this:

initscr();
/* Print to the back buffer. */
printw("Hello, world!");

/* Update the screen. */
refresh();

/* Wait for a key press. */
getch();

/* Terminate ncurses. */
endwin();
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Jon Anderson
  • 166
  • 1
  • 3
  • 4
    This works, but it seems it is impossible to avoid clearing the screen which is enforced by `initscr()` as discussed [here](http://stackoverflow.com/questions/4772061/curses-library-c-getch-without-clearing-screen) and [there](http://stackoverflow.com/questions/654471/ncurses-initialization-without-clearing-the-screen). – dojuba May 24 '16 at 10:03
  • 4
    Note that ncurses only works on Unix systems, use [pancurses](https://github.com/ihalila/pancurses) if you need cross platform support for Unix and Windows. – Anthony Mansour Jun 27 '19 at 17:55
15

You can also use termion, but you will have to enable the raw TTY mode which changes the behavior of stdout as well. See the example below (tested with Rust 1.34.0). Note that internally, it also wraps the termios UNIX API.

Cargo.toml

[dependencies]
termion = "1.5.2"

main.rs

use std::io;
use std::io::Write;
use std::thread;
use std::time;

use termion;
use termion::input::TermRead;
use termion::raw::IntoRawMode;

fn main() {
    // Set terminal to raw mode to allow reading stdin one key at a time
    let mut stdout = io::stdout().into_raw_mode().unwrap();

    // Use asynchronous stdin
    let mut stdin = termion::async_stdin().keys();

    loop {
        // Read input (if any)
        let input = stdin.next();

        // If a key was pressed
        if let Some(Ok(key)) = input {
            match key {
                // Exit if 'q' is pressed
                termion::event::Key::Char('q') => break,
                // Else print the pressed key
                _ => {
                    write!(
                        stdout,
                        "{}{}Key pressed: {:?}",
                        termion::clear::All,
                        termion::cursor::Goto(1, 1),
                        key
                    )
                    .unwrap();

                    stdout.lock().flush().unwrap();
                }
            }
        }
        thread::sleep(time::Duration::from_millis(50));
    }
}
BenC
  • 8,729
  • 3
  • 49
  • 68
0

Here's a lightweight solution only using the libc crate based some code from the console crate:

fn setup_raw_terminal() -> io::Result<()> {
    unsafe {
        let tty;
        let fd = if libc::isatty(libc::STDIN_FILENO) == 1 {
            libc::STDIN_FILENO
        } else {
            tty = fs::File::open("/dev/tty")?;

            tty.as_raw_fd()
        };

        let mut ptr = core::mem::MaybeUninit::uninit();

        if libc::tcgetattr(fd, ptr.as_mut_ptr()) == 0 {
            let mut termios = ptr.assume_init();
            let c_oflag = termios.c_oflag;

            libc::cfmakeraw(&mut termios);
            termios.c_oflag = c_oflag;

            if libc::tcsetattr(fd, libc::TCSADRAIN, &termios) == 0 {
                return Ok(());
            }
        }
    }

    Err(io::Error::last_os_error())
}

It needs to be called before reading stdin:

let mut buf = [0u8; 1024];
let mut stdin = io::stdin();

setup_raw_terminal()?;

loop {
    let size = stdin.read(&mut buf)?;
    let data = &buf[0..size];

    println!("stdin data: {}", data);
}
Fathy
  • 4,939
  • 1
  • 23
  • 25