Как я могу прочитать один символ из stdin, не нажав enter?

Я хочу запустить исполняемый файл, который блокируется на stdin и когда нажата клавиша, тот же самый символ печатается сразу без нажатия Enter.

Как я могу прочитать один символ из stdin, не нажав Enter? Я начал с этого примера:

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);
}

Я просмотрел API и попытался заменить read_line() на bytes(), но все, что я пытаюсь, требует, чтобы я ударил Enter перед чтением.

Этот вопрос задавали для C/С++, но, похоже, нет стандартного способа сделать это: Захват символов со стандартного ввода без ожидания нажатия клавиши

Возможно, это не выполнимо в Rust, считая его не простым в C/С++.

Ответы

Ответ 1

Используйте одну из доступных теперь библиотек ncurses, например this one.

Добавить зависимость в Cargo

[dependencies]
ncurses = "5.86.0"

и включите в main.rs:

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

Следуйте за примерами в библиотеке, чтобы инициализировать ncurses и ждать ввода одного символа следующим образом:

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

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

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

/* Terminate ncurses. */
endwin();

Ответ 2

В то время как решение @Jon, использующее ncurses, работает, ncurses очищает экран по дизайну. Я придумал это решение, которое использует termios crate для моего маленького проекта, чтобы узнать Rust. Идея состоит в том, чтобы изменить флаги ECHO и ICANON, обратившись к tcsetattr через привязки termios.

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
}

Одним из преимуществ чтения одного байта является захват клавиш со стрелками, ctrl и т.д. Расширенные F-ключи не захватываются (хотя ncurses могут их фиксировать).

Это решение предназначено для UNIX-подобных платформ. У меня нет опыта работы с Windows, но в соответствии с этим форум возможно, что-то подобное может быть достигнуто с помощью SetConsoleMode в Windows.

Ответ 3

Вы также можете использовать termion, но вам нужно будет включить необработанный режим TTY, который также изменяет поведение стандартного stdout. Смотрите пример ниже (протестировано с Rust 1.34.0)

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));
    }
}