Search code examples
rustkeyboardkeyboard-eventsrust-cargo

Detect keydown?


I would like to detect a keydown event in Rust and then check if a combination of keys is pressed, in order to do further actions based on that. So basically support keyboard shortcuts in my Rust application.

I've looked at some crates for example ncurses but they did not match my requirements...


Solution

  • Best solution for ANSI terminals (Linux, macOS)

    If you don't need support for Windows then the best is termion.

    It's a library for manipulating the terminal. In which you can detect key events and even keyboard shortcuts. And it's also really lightweight! Only 22.78 kB (as of version 1.5.5).

    Here's a quick program I put together to showcase few shortcuts.

    Add this code to main.rs, add termion = "1.5.5" to Cargo.toml and start it with cargo run!

    use std::io::{stdin, stdout, Write};
    use termion::event::Key;
    use termion::input::TermRead;
    use termion::raw::IntoRawMode;
    
    fn main() {
        let stdin = stdin();
        //setting up stdout and going into raw mode
        let mut stdout = stdout().into_raw_mode().unwrap();
        //printing welcoming message, clearing the screen and going to left top corner with the cursor
        write!(stdout, r#"{}{}ctrl + q to exit, ctrl + h to print "Hello world!", alt + t to print "termion is cool""#, termion::cursor::Goto(1, 1), termion::clear::All)
                .unwrap();
        stdout.flush().unwrap();
    
        //detecting keydown events
        for c in stdin.keys() {
            //clearing the screen and going to top left corner
            write!(
                stdout,
                "{}{}",
                termion::cursor::Goto(1, 1),
                termion::clear::All
            )
            .unwrap();
    
            //i reckon this speaks for itself
            match c.unwrap() {
                Key::Ctrl('h') => println!("Hello world!"),
                Key::Ctrl('q') => break,
                Key::Alt('t') => println!("termion is cool"),
                _ => (),
            }
    
            stdout.flush().unwrap();
        }
    }
    

    Cross Platform Solution

    If you need to support Windows and all other platforms, then you can use crossterm. It's a pretty decent library and quite heavier than termion. It's 98.06 kB (as of version 0.16.0).

    Here's the same program as above but written using crossterm.

    Add this code to main.rs, add crossterm = "0.16.0" to Cargo.toml and try it with cargo run!

    //importing in execute! macro
    #[macro_use]
    extern crate crossterm;
    
    use crossterm::cursor;
    use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
    use crossterm::style::Print;
    use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
    use std::io::{stdout, Write};
    
    fn main() {
        let mut stdout = stdout();
        //going into raw mode
        enable_raw_mode().unwrap();
    
        //clearing the screen, going to top left corner and printing welcoming message
        execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0), Print(r#"ctrl + q to exit, ctrl + h to print "Hello world", alt + t to print "crossterm is cool""#))
                .unwrap();
    
        //key detection
        loop {
            //going to top left corner
            execute!(stdout, cursor::MoveTo(0, 0)).unwrap();
    
            //matching the key
            match read().unwrap() {
                //i think this speaks for itself
                Event::Key(KeyEvent {
                    code: KeyCode::Char('h'),
                    modifiers: KeyModifiers::CONTROL,
                    //clearing the screen and printing our message
                }) => execute!(stdout, Clear(ClearType::All), Print("Hello world!")).unwrap(),
                Event::Key(KeyEvent {
                    code: KeyCode::Char('t'),
                    modifiers: KeyModifiers::ALT,
                }) => execute!(stdout, Clear(ClearType::All), Print("crossterm is cool")).unwrap(),
                Event::Key(KeyEvent {
                    code: KeyCode::Char('q'),
                    modifiers: KeyModifiers::CONTROL,
                }) => break,
                _ => (),
            }
        }
    
        //disabling raw mode
        disable_raw_mode().unwrap();
    }
    

    I'm not going to lie, this is a bit harder to read than the termion solution, but it does the same job. I have no prior experience with crossterm so this code may actually not be the best but it's decent.

    Looking for a way to detect only key press without any modifier (Shift, Control, Alt)? Check this simplified code:

    //-- code --
    
    loop {
        //--code--
    
        match read().unwrap() {
            Event::Key(KeyEvent {
                code: KeyCode::Char('a'),
                modifiers: KeyModifiers::NONE,
            }) => //--code--
        }
    
        //--code--
    }
    
    //--code--
    

    The important part here is the use of KeyModifiers::NONE.