Solution 1:

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 --
let no_modifiers = KeyModifiers::empty();

loop {
    //--code--

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

    //--code--
}

//--code--

The important part here is the creation of the no_modifiers variable. Since we don't want to use any modifiers then the only way to create a KeyModifiers instance without any modifiers is to call the empty() function which doesn't contain any modifiers. We need this variable because we can't call functions in patterns (in this case struct intialization).