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...
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.
You could use console as a simple cross-platform solution.
use console::Term;
fn main() {
let stdout = Term::buffered_stdout();
'game_loop: loop {
if let Ok(character) = stdout.read_char() {
match character {
'w' => todo!("Up"),
'a' => todo!("Left"),
's' => todo!("Down"),
'd' => todo!("Right"),
_ => break 'game_loop,
}
}
}
}
The snippet above shows a basic example for matching common movement characters for a platform.
Related
I am a blind Rust learner and i'm trying to make a simple audiogame engine for blind people but in rust.
Now I'm trying to make a simple window. A game for blind uses only audio and keyboard, so i have a question.
I'm trying to create an empty window and catch key presses to be able to make a self voiced user interface and game interaction using keyboard only, without mouce. what's the best solution for that?
I tryed to use winit and winit input helper, but maybe there is a better way to do that?
use winit::event::VirtualKeyCode;
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit_input_helper::WinitInputHelper;
pub fn show_window() {
let mut input = WinitInputHelper::new();
let event_loop = EventLoop::new();
let _window = WindowBuilder::new().build(&event_loop).unwrap();
event_loop.run(move |event, _, control_flow| {
// Pass every event to the WindowInputHelper.
// It will return true when the last event has been processed and it is time to run your application logic.
if input.update(&event) {
// query keypresses this update
if input.key_pressed_os(VirtualKeyCode::A) {
println!("The 'A' key was pressed on the keyboard (OS repeating)");
}
if input.key_pressed(VirtualKeyCode::A) {
println!("The 'A' key was pressed on the keyboard");
}
if input.key_released(VirtualKeyCode::Q) || input.quit() {
*control_flow = ControlFlow::Exit;
return;
}
}
});
}
I have a tui app where a user is presented with some choices through a list. Once they navigate to the choice they want and hit enter I'd like to take them to the "next" screen.
It's more complicated than just clearning existing text and printing new one because I also need to replace keybindings and basically start a new tui-rs loop. More below.
Code for Screen 1:
pub fn draw_screen() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let events = Events::new();
loop {
terminal.draw(|f| {
// user shown a list they can navigate through using arrow keys
});
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Char('\n') => {
// this is where I need to "send" them to a new screen
}
Key::Down => {
// my_list won't exist on next screen
my_list.items.next();
}
Key::Up => {
my_list.items.previous();
}
_ => {}
},
_ => {}
}
}
Ok(())
}
As can be seen the keybindings at the bottom are specific to this screen. Eg on the next screen there's not going to be a my_list and instead there might be a my_another_list or my_box or nothing at all.
So if all I did was clear the text, I'd still be left inside the same loop with the same keybindings - doesn't work.
What's the right way to initiate a new loop with fresh keybindings?
I'm writing an ncurses app with Rust.
When the user inputs a valid UTF-8 char (like ć, or some Asian letters), I want to build up a search string from it and print it to screen. Currently I have this:
use ncurses::*;
fn main() {
...
let mut search_string = String::new();
...
loop {
let user_input = getch();
match user_input {
27 => break,
KEY_UP => { ... },
KEY_DOWN => { ... },
KEY_BACKSPACE => { ... },
_ => {
search_string += &std::char::from_u32(user_input as u32).expect("Invalid char.").to_string();
mvaddstr(0, 0, &search_string);
app::autosearch();
}
}
}
}
However, this catches all other keys, such as F5, KEY_LEFT, etc.
How can I match only valid UTF-8 letters?
If getch gives you a u8, you could collect subsequent key presses into a Vec<u8> and then call e.g. from_utf8 on each getch, handling the error as appropriate (see Utf8Error for more info).
In C, you could call get_wch() instead of getch() -- it returns KEY_CODE_YES for KEY_* codes, while the actual key is stored to an address passed as a parameter. But I don't know how this translates to Rust.
I'm learning rust and trying to make a find like utility (yes another one), im using clap and trying to support command line and config file for the program's parameters(this has nothing to do with the clap yml file).
Im trying to parse the commands and if no commands were passed to the app, i will try to load them from a config file.
Now I don't know how to do this in an idiomatic way.
fn main() {
let matches = App::new("findx")
.version(crate_version!())
.author(crate_authors!())
.about("find + directory operations utility")
.arg(
Arg::with_name("paths")
...
)
.arg(
Arg::with_name("patterns")
...
)
.arg(
Arg::with_name("operation")
...
)
.get_matches();
let paths;
let patterns;
let operation;
if matches.is_present("patterns") && matches.is_present("operation") {
patterns = matches.values_of("patterns").unwrap().collect();
paths = matches.values_of("paths").unwrap_or(clap::Values<&str>{"./"}).collect(); // this doesn't work
operation = match matches.value_of("operation").unwrap() { // I dont like this
"Append" => Operation::Append,
"Prepend" => Operation::Prepend,
"Rename" => Operation::Rename,
_ => {
print!("Operation unsupported");
process::exit(1);
}
};
}else if Path::new("findx.yml").is_file(){
//TODO: try load from config file
}else{
eprintln!("Command line parameters or findx.yml file must be provided");
process::exit(1);
}
if let Err(e) = findx::run(Config {
paths: paths,
patterns: patterns,
operation: operation,
}) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}
There is an idiomatic way to extract Option and Result types values to the same scope, i mean all examples that i have read, uses match or if let Some(x) to consume the x value inside the scope of the pattern matching, but I need to assign the value to a variable.
Can someone help me with this, or point me to the right direction?
Best Regards
Personally I see nothing wrong with using the match statements and folding it or placing it in another function. But if you want to remove it there are many options.
There is the ability to use the .default_value_if() method which is impl for clap::Arg and have a different default value depending on which match arm is matched.
From the clap documentation
//sets value of arg "other" to "default" if value of "--opt" is "special"
let m = App::new("prog")
.arg(Arg::with_name("opt")
.takes_value(true)
.long("opt"))
.arg(Arg::with_name("other")
.long("other")
.default_value_if("opt", Some("special"), "default"))
.get_matches_from(vec![
"prog", "--opt", "special"
]);
assert_eq!(m.value_of("other"), Some("default"));
In addition you can add a validator to your operation OR convert your valid operation values into flags.
Here's an example converting your match arm values into individual flags (smaller example for clarity).
extern crate clap;
use clap::{Arg,App};
fn command_line_interface<'a>() -> clap::ArgMatches<'a> {
//Sets the command line interface of the program.
App::new("something")
.version("0.1")
.arg(Arg::with_name("rename")
.help("renames something")
.short("r")
.long("rename"))
.arg(Arg::with_name("prepend")
.help("prepends something")
.short("p")
.long("prepend"))
.arg(Arg::with_name("append")
.help("appends something")
.short("a")
.long("append"))
.get_matches()
}
#[derive(Debug)]
enum Operation {
Rename,
Append,
Prepend,
}
fn main() {
let matches = command_line_interface();
let operation = if matches.is_present("rename") {
Operation::Rename
} else if matches.is_present("prepend"){
Operation::Prepend
} else {
//DEFAULT
Operation::Append
};
println!("Value of operation is {:?}",operation);
}
I hope this helps!
EDIT:
You can also use Subcommands with your specific operations. It all depends on what you want to interface to be like.
let app_m = App::new("git")
.subcommand(SubCommand::with_name("clone"))
.subcommand(SubCommand::with_name("push"))
.subcommand(SubCommand::with_name("commit"))
.get_matches();
match app_m.subcommand() {
("clone", Some(sub_m)) => {}, // clone was used
("push", Some(sub_m)) => {}, // push was used
("commit", Some(sub_m)) => {}, // commit was used
_ => {}, // Either no subcommand or one not tested for...
}
What is the best way to check a hash map for a key?
Currently I am using this:
let hashmap = HashMap::<&str, &str>::new(); // Empty hashmap
let name = "random";
for i in 0..5000000 {
if !hashmap.contains_key(&name) {
// Do nothing
}
}
This seems to be fast in most cases and takes 0.06 seconds when run as shown, but when I use it in this following loop it becomes very slow and takes almost 1 min on my machine. (This is compiling with cargo run --release).
The code aims to open an external program, and loop over the output from that program.
let a = vec!["view", "-h"]; // Arguments to open process with
let mut child = Command::new("samtools").args(&a)
.stdout(Stdio::piped())
.spawn()
.unwrap();
let collect_pairs = HashMap::<&str, &str>::new();
if let Some(ref mut stdout) = child.stdout {
for line in BufReader::new(stdout).lines() {
// Do stuff here
let name = "random";
if !collect_pairs.contains_key(&name) {
// Do nothing
}
}
}
For some reason adding the if !collect_pairs.contains_key( line increases the run time by almost a minute. The output from child is around 5 million lines. All this code exists in fn main()
EDIT
This appears to fix the problem, resulting in a fast run time, but I do not know why the !hashmap.contains_key does not work well here:
let n: Option<&&str> = collect_pairs.get(name);
if match n {Some(v) => 1, None => 0} == 1 {
// Do something
}
One thing to consider is that HashMap<K, V> uses a cryptographically secure hashing algorithm by default, so it will always be a bit slow by nature.
get() boils down to
self.search(k).map(|bucket| bucket.into_refs().1)
contains_key is
self.search(k).is_some()
As such, that get() is faster for you seems strange to me, it's doing more work!
Also,
if match n {Some(v) => 1, None => 0} == 1 {
This can be written more idiomatically as
if let Some(v) = n {
Ive found my problem, Im sorry I didnt pick up until now. I wasnt checking the return of if !collect_pairs.contains_key(&name) properly. It returns true for some reason resulting in the rest of the if block being run. I assumed it was evaluating to false. Thanks for the help