At the moment, our keys are hard coded into the app.
impl Component for Home {
fn handle_key_events ( & mut self , key : KeyEvent) -> Action {
Mode :: Normal | Mode :: Processing => {
KeyCode :: Char ( 'q' ) => Action :: Quit,
KeyCode :: Char ( 'd' ) if key . modifiers . contains (KeyModifiers :: CONTROL ) => Action :: Quit,
KeyCode :: Char ( 'c' ) if key . modifiers . contains (KeyModifiers :: CONTROL ) => Action :: Quit,
KeyCode :: Char ( 'z' ) if key . modifiers . contains (KeyModifiers :: CONTROL ) => Action :: Suspend,
KeyCode :: Char ( '?' ) => Action :: ToggleShowHelp,
KeyCode :: Char ( 'j' ) => Action :: ScheduleIncrement,
KeyCode :: Char ( 'k' ) => Action :: ScheduleDecrement,
KeyCode :: Char ( '/' ) => Action :: EnterInsert,
KeyCode :: Esc => Action :: EnterNormal,
KeyCode :: Enter => Action :: EnterNormal,
self. input . handle_event ( & crossterm :: event :: Event :: Key ( key ));
If a user wants to press Up
and Down
arrow key to ScheduleIncrement
and ScheduleDecrement
,
the only way for them to do it is having to make changes to the source code and recompile the app.
It would be better to provide a way for users to set up a configuration file that maps key presses
to actions.
For example, assume we want a user to be able to set up a keyevents-to-actions mapping in a
config.toml
file like below:
" j " = " ScheduleIncrement "
" k " = " ScheduleDecrement "
We can set up a Config
struct using
the excellent config
crate :
use std :: collections :: HashMap;
use color_eyre :: eyre :: Result;
use ratatui :: crossterm :: event :: KeyEvent;
use serde_derive :: Deserialize;
use crate :: action :: Action;
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize)]
pub struct KeyMap( pub HashMap<KeyEvent, Action>);
pub fn new () -> Result< Self , config :: ConfigError> {
let mut builder = config :: Config :: builder ();
. add_source (config :: File :: from ( config_dir . join ( " config.toml " )) . format (config :: FileFormat :: Toml) . required ( false ));
builder . build () ?. try_deserialize ()
We are using serde
to deserialize from a TOML file.
Now the default KeyEvent
serialized format is not very user friendly, so let’s implement our own
version:
#[derive(Clone, Debug, Default)]
pub struct KeyMap( pub HashMap<KeyEvent, Action>);
impl <'de> Deserialize<'de> for KeyMap {
fn deserialize <D>( deserializer : D) -> Result< Self , D :: Error> where D : Deserializer<'de>,
impl <'de> Visitor<'de> for KeyMapVisitor {
fn visit_map <M>( self , mut access : M) -> Result<KeyMap, M :: Error>
let mut keymap = HashMap :: new ();
while let Some(( key_str , action )) = access . next_entry :: <String, Action>() ? {
let key_event = parse_key_event ( & key_str ) . map_err (de :: Error :: custom ) ? ;
keymap . insert ( key_event , action );
deserializer . deserialize_map (KeyMapVisitor)
Now all we need to do is implement a parse_key_event
function.
You can check the source code for an example of this implementation .
With that implementation complete, we can add a HashMap
to store a map of KeyEvent
s and Action
in the Home
component:
pub keymap : HashMap<KeyEvent, Action>,
Now we have to create an instance of Config
and pass the keymap to Home
:
pub fn new ( tick_rate : (u64, u64)) -> Result< Self > {
let config = Config :: new () ? ;
let h = h . keymap ( config . keymap . 0. clone ());
let home = Arc :: new (Mutex :: new ( h ));
Ok( Self { tick_rate , home , should_quit : false , should_suspend : false , config })
And in the handle_key_events
we get the Action
that should to be performed from the HashMap
directly.
impl Component for Home {
fn handle_key_events ( & mut self , key : KeyEvent) -> Action {
Mode :: Normal | Mode :: Processing => {
if let Some( action ) = self. keymap . get ( & key ) {
KeyCode :: Esc => Action :: EnterNormal,
KeyCode :: Enter => Action :: EnterNormal,
self. input . handle_event ( & crossterm :: event :: Event :: Key ( key ));
In the template, it is set up to handle Vec<KeyEvent>
mapped to an Action
. This allows you to
map for example:
<g><j>
to Action::GotoBottom
<g><k>
to Action::GotoTop
And because we are now using multiple keys as input, you have to update the app.rs
main loop
accordingly to handle that:
if let Some( e ) = tui . next () . await {
tui :: Event :: Key ( key ) => {
if let Some( keymap ) = self. config . keybindings . get ( &self. mode) {
// If the key is a single key action
if let Some( action ) = keymap . get ( & vec! [ key . clone ()]) {
log :: info! ( " Got action: {action:?} " );
action_tx . send ( action . clone ()) ? ;
// If the key was not handled as a single key action,
// then consider it for multi-key combinations.
self. last_tick_key_events . push ( key );
// Check for multi-key combinations
if let Some( action ) = keymap . get ( &self. last_tick_key_events) {
log :: info! ( " Got action: {action:?} " );
action_tx . send ( action . clone ()) ? ;
while let Ok( action ) = action_rx . try_recv () {
for component in self. components . iter_mut () {
if let Some( action ) = component . update ( action . clone ()) ? {
Here’s the JSON configuration we use for the counter application:
"<q>" : " Quit " , // Quit the application
"<j>" : " ScheduleIncrement " ,
"<k>" : " ScheduleDecrement " ,
"<Ctrl-d>" : " Quit " , // Another way to quit
"<Ctrl-c>" : " Quit " , // Yet another way to quit
"<Ctrl-z>" : " Suspend " , // Suspend the application