Repository: kkawakam/rustyline
Pull request #492 (2021-02-20 10:46:10)

Conditional binding
Introduce `ConditionalEventHandler` and `EventContext`.
You should be able to return different `Cmd` depending on input state.

Fixme
- [X] key sequence 

Replace #293

- [X] [Conditional Bind Sequences](https://github.com/kkawakam/rustyline/issues/269) : original issues
- [X] [Conditional Bind Sequences](https://github.com/kkawakam/rustyline/pull/293) : incomplete proposal
- [ ] [Extend Meta-F,Alt+Right feature for hint partial completion](https://github.com/kkawakam/rustyline/pull/430) ?
- [X] [Use Ctrl-E for hint completion](https://github.com/kkawakam/rustyline/pull/407)
- [X] [Insert tab when the cursor is ontop a whitespace character](https://github.com/kkawakam/rustyline/issues/488)
- [X] [Prompt for numeric entry only](https://github.com/kkawakam/rustyline/issues/436): `Event::Any`


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 2742656..86c548d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,14 +20,20 @@ members = ["rustyline-derive"]
 [dependencies]
 bitflags = "1.2"
 cfg-if = "1.0"
+# For file completion
 # https://rustsec.org/advisories/RUSTSEC-2020-0053.html
 dirs-next = { version = "2.0", optional = true }
+# For History
 fs2 = "0.4"
 libc = "0.2"
 log = "0.4"
 unicode-width = "0.1"
 unicode-segmentation = "1.0"
 memchr = "2.0"
+# For custom bindings
+# https://rustsec.org/advisories/RUSTSEC-2021-0003.html
+smallvec = "1.6.1"
+radix_trie = "0.2"
 
 [target.'cfg(unix)'.dependencies]
 nix = "0.20"
diff --git a/examples/custom_key_bindings.rs b/examples/custom_key_bindings.rs
new file mode 100644
index 0000000..77957e3
--- /dev/null
+++ b/examples/custom_key_bindings.rs
@@ -0,0 +1,110 @@
+use smallvec::smallvec;
+use std::borrow::Cow::{self, Borrowed, Owned};
+
+use rustyline::highlight::Highlighter;
+use rustyline::hint::{Hinter, HistoryHinter};
+use rustyline::{
+    Cmd, ConditionalEventHandler, Context, Editor, Event, EventContext, EventHandler, KeyEvent,
+    RepeatCount,
+};
+use rustyline_derive::{Completer, Helper, Validator};
+
+#[derive(Completer, Helper, Validator)]
+struct MyHelper(HistoryHinter);
+
+impl Hinter for MyHelper {
+    type Hint = String;
+
+    fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
+        self.0.hint(line, pos, ctx)
+    }
+}
+
+impl Highlighter for MyHelper {
+    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
+        &'s self,
+        prompt: &'p str,
+        default: bool,
+    ) -> Cow<'b, str> {
+        if default {
+            Owned(format!("\x1b[1;32m{}\x1b[m", prompt))
+        } else {
+            Borrowed(prompt)
+        }
+    }
+
+    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
+        Owned(format!("\x1b[1m{}\x1b[m", hint))
+    }
+}
+
+#[derive(Clone)]
+struct CompleteHintHandler;
+impl ConditionalEventHandler for CompleteHintHandler {
+    fn handle(&self, evt: &Event, _: RepeatCount, _: bool, ctx: &EventContext) -> Option<Cmd> {
+        if !ctx.has_hint() {
+            return None; // default
+        }
+        if let Some(k) = evt.get(0) {
+            #[allow(clippy::if_same_then_else)]
+            if *k == KeyEvent::ctrl('E') {
+                Some(Cmd::CompleteHint)
+            } else if *k == KeyEvent::alt('f') && ctx.line().len() == ctx.pos() {
+                Some(Cmd::CompleteHint) // TODO give access to hint
+            } else {
+                None
+            }
+        } else {
+            unreachable!()
+        }
+    }
+}
+
+struct TabEventHandler;
+impl ConditionalEventHandler for TabEventHandler {
+    fn handle(&self, evt: &Event, n: RepeatCount, _: bool, ctx: &EventContext) -> Option<Cmd> {
+        debug_assert_eq!(*evt, Event::from(KeyEvent::from('\t')));
+        if ctx.line()[..ctx.pos()]
+            .chars()
+            .rev()
+            .next()
+            .filter(|c| c.is_whitespace())
+            .is_some()
+        {
+            Some(Cmd::SelfInsert(n, '\t'))
+        } else {
+            None // default complete
+        }
+    }
+}
+
+fn main() {
+    let mut rl = Editor::<MyHelper>::new();
+    rl.set_helper(Some(MyHelper(HistoryHinter {})));
+
+    let ceh = Box::new(CompleteHintHandler);
+    rl.bind_sequence(KeyEvent::ctrl('E'), EventHandler::Conditional(ceh.clone()));
+    rl.bind_sequence(KeyEvent::alt('f'), EventHandler::Conditional(ceh));
+    rl.bind_sequence(
+        KeyEvent::from('\t'),
+        EventHandler::Conditional(Box::new(TabEventHandler)),
+    );
+    rl.bind_sequence(
+        Event::KeySeq(smallvec![KeyEvent::ctrl('X'), KeyEvent::ctrl('E')]),
+        EventHandler::Simple(Cmd::Suspend), // TODO external editor
+    );
+
+    loop {
+        let readline = rl.readline("> ");
+        match readline {
+            Ok(line) => {
+                rl.add_history_entry(line.as_str());
+                println!("Line: {}", line);
+            }
+            Err(err) => {
+                println!("Error: {:?}", err);
+                break;
+            }
+        }
+    }
+}
diff --git a/examples/example.rs b/examples/example.rs
index c627471..7b3ccc8 100644
--- a/examples/example.rs
+++ b/examples/example.rs
@@ -97,8 +97,8 @@ fn main() -> rustyline::Result<()> {
     };
     let mut rl = Editor::with_config(config);
     rl.set_helper(Some(h));
-    rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
-    rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
+    rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward);
+    rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward);
     if rl.load_history("history.txt").is_err() {
         println!("No previous history.");
     }
diff --git a/examples/numeric_input.rs b/examples/numeric_input.rs
new file mode 100644
index 0000000..8071d2b
--- /dev/null
+++ b/examples/numeric_input.rs
@@ -0,0 +1,41 @@
+use rustyline::{
+    Cmd, ConditionalEventHandler, Editor, Event, EventContext, EventHandler, KeyCode, KeyEvent,
+    Modifiers, RepeatCount,
+};
+
+struct FilteringEventHandler;
+impl ConditionalEventHandler for FilteringEventHandler {
+    fn handle(&self, evt: &Event, _: RepeatCount, _: bool, _: &EventContext) -> Option<Cmd> {
+        if let Some(KeyEvent(KeyCode::Char(c), m)) = evt.get(0) {
+            if m.contains(Modifiers::CTRL) || m.contains(Modifiers::ALT) || c.is_ascii_digit() {
+                None
+            } else {
+                Some(Cmd::Noop) // filter out invalid input
+            }
+        } else {
+            None
+        }
+    }
+}
+
+fn main() {
+    let mut rl = Editor::<()>::new();
+
+    rl.bind_sequence(
+        Event::Any,
+        EventHandler::Conditional(Box::new(FilteringEventHandler)),
+    );
+
+    loop {
+        let readline = rl.readline("> ");
+        match readline {
+            Ok(line) => {
+                println!("Num: {}", line);
+            }
+            Err(err) => {
+                println!("Error: {:?}", err);
+                break;
+            }
+        }
+    }
+}
diff --git a/src/binding.rs b/src/binding.rs
new file mode 100644
index 0000000..8fde7be
--- /dev/null
+++ b/src/binding.rs
@@ -0,0 +1,227 @@
+/// Custom event handlers
+use crate::{
+    Cmd, EditMode, InputMode, InputState, KeyCode, KeyEvent, Modifiers, Refresher, RepeatCount,
+};
+
+use radix_trie::TrieKey;
+use smallvec::{smallvec, SmallVec};
+
+/// Input event
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Event {
+    /// Wildcard.
+    /// Useful if you want to filter out some keys.
+    Any,
+    /// Key sequence
+    // TODO Validate 2 ?
+    KeySeq(SmallVec<[KeyEvent; 2]>),
+    /// TODO Mouse event
+    Mouse(),
+}
+
+impl Event {
+    /// See [`KeyEvent::normalize`]
+    pub(crate) fn normalize(mut self) -> Self {
+        if let Event::KeySeq(ref mut keys) = self {
+            for key in keys.iter_mut() {
+                *key = KeyEvent::normalize(*key);
+            }
+        }
+        self
+    }
+
+    /// Return `i`th key event
+    pub fn get(&self, i: usize) -> Option<&KeyEvent> {
+        if let Event::KeySeq(ref ks) = self {
+            ks.get(i)
+        } else {
+            None
+        }
+    }
+}
+
+impl From<KeyEvent> for Event {
+    fn from(k: KeyEvent) -> Event {
+        Event::KeySeq(smallvec![k])
+    }
+}
+
+const BASE: u32 = 0x0010ffff + 1;
+const BASE_CONTROL: u32 = 0x02000000;
+const BASE_META: u32 = 0x04000000;
+const BASE_SHIFT: u32 = 0x01000000;
+const ESCAPE: u32 = 27;
+const PAGE_UP: u32 = BASE + 1;
+const PAGE_DOWN: u32 = PAGE_UP + 1;
+const DOWN: u32 = PAGE_DOWN + 1;
+const UP: u32 = DOWN + 1;
+const LEFT: u32 = UP + 1;
+const RIGHT: u32 = LEFT + 1;
+const HOME: u32 = RIGHT + 1;
+const END: u32 = HOME + 1;
+const DELETE: u32 = END + 1;
+const INSERT: u32 = DELETE + 1;
+//const F1: u32 = INSERT + 1;
+const MOUSE: u32 = /*F24 + 1*/ INSERT + 25;
+const PASTE_START: u32 = MOUSE + 1;
+const PASTE_FINISH: u32 = PASTE_START + 1;
+const ANY: u32 = PASTE_FINISH + 1;
+
+impl KeyEvent {
+    fn encode(&self) -> u32 {
+        let mut u = match self.0 {
+            KeyCode::UnknownEscSeq => 0,
+            KeyCode::Backspace => u32::from('H') | BASE_CONTROL,
+            KeyCode::BackTab => u32::from('I') | BASE_CONTROL | BASE_SHIFT,
+            KeyCode::BracketedPasteStart => PASTE_START,
+            KeyCode::BracketedPasteEnd => PASTE_FINISH,
+            KeyCode::Char(c) => u32::from(c),
+            KeyCode::Delete => DELETE,
+            KeyCode::Down => DOWN,
+            KeyCode::End => END,
+            KeyCode::Enter => u32::from('M') | BASE_CONTROL,
+            KeyCode::F(i) => INSERT + i as u32,
+            KeyCode::Esc => ESCAPE,
+            KeyCode::Home => HOME,
+            KeyCode::Insert => INSERT,
+            KeyCode::Left => LEFT,
+            KeyCode::Null => 0,
+            KeyCode::PageDown => PAGE_DOWN,
+            KeyCode::PageUp => PAGE_UP,
+            KeyCode::Right => RIGHT,
+            KeyCode::Tab => u32::from('I') | BASE_CONTROL,
+            KeyCode::Up => UP,
+        };
+        if self.1.contains(Modifiers::CTRL) {
+            u |= BASE_CONTROL;
+        }
+        if self.1.contains(Modifiers::ALT) {
+            u |= BASE_META;
+        }
+        if self.1.contains(Modifiers::SHIFT) {
+            u |= BASE_SHIFT;
+        }
+        u
+    }
+}
+
+impl TrieKey for Event {
+    fn encode_bytes(&self) -> Vec<u8> {
+        match self {
+            Event::Any => ANY.to_be_bytes().to_vec(),
+            Event::KeySeq(keys) => {
+                let mut dst = Vec::with_capacity(keys.len() * 4);
+                for key in keys {
+                    dst.extend_from_slice(&key.encode().to_be_bytes());
+                }
+                dst
+            }
+            Event::Mouse() => MOUSE.to_be_bytes().to_vec(),
+        }
+    }
+}
+
+/// Event handler
+pub enum EventHandler {
+    /// unconditional command
+    Simple(Cmd),
+    /// handler behaviour depends on input state
+    Conditional(Box<dyn ConditionalEventHandler>),
+    /* invoke multiple actions
+     * TODO Macro(), */
+}
+
+impl From<Cmd> for EventHandler {
+    fn from(c: Cmd) -> EventHandler {
+        EventHandler::Simple(c)
+    }
+}
+
+/// Give access to user input.
+pub struct EventContext<'r> {
+    mode: EditMode,
+    input_mode: InputMode,
+    wrt: &'r dyn Refresher,
+}
+
+impl<'r> EventContext<'r> {
+    pub(crate) fn new(is: &InputState, wrt: &'r dyn Refresher) -> Self {
+        EventContext {
+            mode: is.mode,
+            input_mode: is.input_mode,
+            wrt,
+        }
+    }
+
+    /// emacs or vi mode
+    pub fn mode(&self) -> EditMode {
+        self.mode
+    }
+
+    /// vi input mode
+    pub fn input_mode(&self) -> InputMode {
+        self.input_mode
+    }
+
+    /// Returns `true` if there is a hint displayed.
+    pub fn has_hint(&self) -> bool {
+        self.wrt.has_hint()
+    }
+
+    /// currently edited line
+    pub fn line(&self) -> &str {
+        self.wrt.line()
+    }
+
+    /// Current cursor position (byte position)
+    pub fn pos(&self) -> usize {
+        self.wrt.pos()
+    }
+}
+
+/// May behave differently depending on:
+///  * edit mode (emacs vs vi)
+///  * vi input mode (insert vs replace vs command modes)
+///  * empty line
+///  * cursor position
+///  * repeat count
+///  * original key pressed (when same command is bound to different key)
+///  * hint
+///  * ...
+pub trait ConditionalEventHandler: Send + Sync {
+    /// Takes the current input state and
+    /// returns the command to be performed or `None` to perform the default
+    /// one.
+    fn handle(
+        &self,
+        evt: &Event,
+        n: RepeatCount,
+        positive: bool,
+        ctx: &EventContext,
+    ) -> Option<Cmd>;
+}
+
+#[cfg(test)]
+mod test {
+    use super::{Event, EventHandler};
+    use crate::{Cmd, KeyEvent};
+    use radix_trie::Trie;
+    use smallvec::smallvec;
+
+    #[test]
+    fn encode() {
+        let mut trie = Trie::new();
+        let evt = Event::KeySeq(smallvec![KeyEvent::ctrl('X'), KeyEvent::ctrl('E')]);
+        trie.insert(evt.clone(), EventHandler::from(Cmd::Noop));
+        let prefix = Event::from(KeyEvent::ctrl('X'));
+        let subtrie = trie.get_raw_descendant(&prefix);
+        assert!(subtrie.is_some());
+        let subtrie = subtrie.unwrap();
+        let sub_result = subtrie.get(&evt);
+        assert!(sub_result.is_ok());
+        assert!(sub_result.unwrap().is_some());
+        let prefix = Event::from(KeyEvent::ctrl('O'));
+        let subtrie = trie.get_raw_descendant(&prefix);
+        assert!(subtrie.is_none())
+    }
+}
diff --git a/src/edit.rs b/src/edit.rs
index 578962f..540f907 100644
--- a/src/edit.rs
+++ b/src/edit.rs
@@ -277,6 +277,14 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> {
     fn has_hint(&self) -> bool {
         self.hint.is_some()
     }
+
+    fn line(&self) -> &str {
+        self.line.as_str()
+    }
+
+    fn pos(&self) -> usize {
+        self.line.pos()
+    }
 }
 
 impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> {
diff --git a/src/keymap.rs b/src/keymap.rs
index d7714d5..c70dee3 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -1,14 +1,13 @@
 //! Bindings from keys to command for Emacs and Vi modes
-use std::collections::HashMap;
 use std::sync::{Arc, RwLock};
 
 use log::debug;
+use radix_trie::Trie;
 
 use super::Result;
-use crate::config::Config;
-use crate::config::EditMode;
 use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M};
 use crate::tty::{RawReader, Term, Terminal};
+use crate::{Config, EditMode, Event, EventContext, EventHandler};
 
 /// The number of times one command should be repeated.
 pub type RepeatCount = usize;
@@ -329,8 +328,9 @@ impl Movement {
     }
 }
 
-#[derive(PartialEq)]
-enum InputMode {
+/// Vi input modes
+#[derive(Clone, Copy, PartialEq)]
+pub enum InputMode {
     /// Vi Command/Alternate
     Command,
     /// Insert/Input mode
@@ -341,9 +341,9 @@ enum InputMode {
 
 /// Transform key(s) to commands based on current input mode
 pub struct InputState {
-    mode: EditMode,
-    custom_bindings: Arc<RwLock<HashMap<KeyEvent, Cmd>>>,
-    input_mode: InputMode, // vi only ?
+    pub(crate) mode: EditMode,
+    custom_bindings: Arc<RwLock<Trie<Event, EventHandler>>>,
+    pub(crate) input_mode: InputMode, // vi only ?
     // numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7
     num_args: i16,
     last_cmd: Cmd,                        // vi only
@@ -376,10 +376,14 @@ pub trait Refresher {
     fn is_cursor_at_end(&self) -> bool;
     /// Returns `true` if there is a hint displayed.
     fn has_hint(&self) -> bool;
+    /// currently edited line
+    fn line(&self) -> &str;
+    /// Current cursor position (byte position)
+    fn pos(&self) -> usize;
 }
 
 impl InputState {
-    pub fn new(config: &Config, custom_bindings: Arc<RwLock<HashMap<KeyEvent, Cmd>>>) -> Self {
+    pub fn new(config: &Config, custom_bindings: Arc<RwLock<Trie<Event, EventHandler>>>) -> Self {
         Self {
             mode: config.edit_mode(),
             custom_bindings,
@@ -419,6 +423,61 @@ impl InputState {
         }
     }
 
+    fn custom_binding(
+        &self,
+        wrt: &mut dyn Refresher,
+        evt: &Event,
+        n: RepeatCount,
+        positive: bool,
+    ) -> Option<Cmd> {
+        let bindings = self.custom_bindings.read().unwrap();
+        let handler = bindings.get(&evt).or_else(|| bindings.get(&Event::Any));
+        if let Some(handler) = handler {
+            match handler {
+                EventHandler::Simple(cmd) => Some(cmd.clone()),
+                EventHandler::Conditional(handler) => {
+                    let ctx = EventContext::new(self, wrt);
+                    handler.handle(&evt, n, positive, &ctx)
+                }
+            }
+        } else {
+            None
+        }
+    }
+
+    fn custom_seq_binding<R: RawReader>(
+        &self,
+        rdr: &mut R,
+        wrt: &mut dyn Refresher,
+        evt: &mut Event,
+        n: RepeatCount,
+        positive: bool,
+    ) -> Result<Option<Cmd>> {
+        let bindings = self.custom_bindings.read().unwrap();
+        while let Some(subtrie) = bindings.get_raw_descendant(&evt) {
+            let snd_key = rdr.next_key(true)?;
+            if let Event::KeySeq(ref mut key_seq) = evt {
+                key_seq.push(snd_key);
+            } else {
+                break;
+            }
+            let handler = subtrie.get(&evt).unwrap();
+            if let Some(handler) = handler {
+                let cmd = match handler {
+                    EventHandler::Simple(cmd) => Some(cmd.clone()),
+                    EventHandler::Conditional(handler) => {
+                        let ctx = EventContext::new(self, wrt);
+                        handler.handle(&evt, n, positive, &ctx)
+                    }
+                };
+                if cmd.is_some() {
+                    return Ok(cmd);
+                }
+            }
+        }
+        Ok(None)
+    }
+
     fn emacs_digit_argument<R: RawReader>(
         &mut self,
         rdr: &mut R,
@@ -472,16 +531,14 @@ impl InputState {
             key = self.emacs_digit_argument(rdr, wrt, digit)?;
         }
         let (n, positive) = self.emacs_num_args(); // consume them in all cases
-        {
-            let bindings = self.custom_bindings.read().unwrap();
-            if let Some(cmd) = bindings.get(&key) {
-                debug!(target: "rustyline", "Custom command: {:?}", cmd);
-                return Ok(if cmd.is_repeatable() {
-                    cmd.redo(Some(n), wrt)
-                } else {
-                    cmd.clone()
-                });
-            }
+
+        let mut evt = key.into();
+        if let Some(cmd) = self.custom_binding(wrt, &evt, n, positive) {
+            return Ok(if cmd.is_repeatable() {
+                cmd.redo(Some(n), wrt)
+            } else {
+                cmd
+            });
         }
         let cmd = match key {
             E(K::Char(c), M::NONE) => {
@@ -536,11 +593,19 @@ impl InputState {
             E(K::Char('N'), M::CTRL) => Cmd::NextHistory,
             E(K::Char('P'), M::CTRL) => Cmd::PreviousHistory,
             E(K::Char('X'), M::CTRL) => {
-                let snd_key = rdr.next_key(true)?;
-                match snd_key {
-                    E(K::Char('G'), M::CTRL) | E::ESC => Cmd::Abort,
-                    E(K::Char('U'), M::CTRL) => Cmd::Undo(n),
-                    _ => Cmd::Unknown,
+                if let Some(cmd) = self.custom_seq_binding(rdr, wrt, &mut evt, n, positive)? {
+                    cmd
+                } else {
+                    let snd_key = match evt {
+                        // we may have already read the second key in custom_seq_binding
+                        Event::KeySeq(ref key_seq) if key_seq.len() > 1 => key_seq[1],
+                        _ => rdr.next_key(true)?,
+                    };
+                    match snd_key {
+                        E(K::Char('G'), M::CTRL) | E::ESC => Cmd::Abort,
+                        E(K::Char('U'), M::CTRL) => Cmd::Undo(n),
+                        _ => Cmd::Unknown,
+                    }
                 }
             }
             E(K::Backspace, M::ALT) => {
@@ -579,7 +644,7 @@ impl InputState {
             // TODO ESC-R (r): Undo all changes made to this line.
             E(K::Char('U'), M::ALT) | E(K::Char('u'), M::ALT) => Cmd::UpcaseWord,
             E(K::Char('Y'), M::ALT) | E(K::Char('y'), M::ALT) => Cmd::YankPop,
-            _ => self.common(rdr, key, n, positive)?,
+            _ => self.common(rdr, wrt, evt, key, n, positive)?,
         };
         debug!(target: "rustyline", "Emacs command: {:?}", cmd);
         Ok(cmd)
@@ -622,20 +687,17 @@ impl InputState {
         }
         let no_num_args = self.num_args == 0;
         let n = self.vi_num_args(); // consume them in all cases
-        {
-            let bindings = self.custom_bindings.read().unwrap();
-            if let Some(cmd) = bindings.get(&key) {
-                debug!(target: "rustyline", "Custom command: {:?}", cmd);
-                return Ok(if cmd.is_repeatable() {
-                    if no_num_args {
-                        cmd.redo(None, wrt)
-                    } else {
-                        cmd.redo(Some(n), wrt)
-                    }
+        let evt = key.into();
+        if let Some(cmd) = self.custom_binding(wrt, &evt, n, true) {
+            return Ok(if cmd.is_repeatable() {
+                if no_num_args {
+                    cmd.redo(None, wrt)
                 } else {
-                    cmd.clone()
-                });
-            }
+                    cmd.redo(Some(n), wrt)
+                }
+            } else {
+                cmd
+            });
         }
         let cmd = match key {
             E(K::Char('$'), M::NONE) | E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine),
@@ -785,7 +847,7 @@ impl InputState {
                 None => Cmd::Unknown,
             },
             E::ESC => Cmd::Noop,
-            _ => self.common(rdr, key, n, true)?,
+            _ => self.common(rdr, wrt, evt, key, n, true)?,
         };
         debug!(target: "rustyline", "Vi command: {:?}", cmd);
         if cmd.is_repeatable_change() {
@@ -800,16 +862,13 @@ impl InputState {
         wrt: &mut dyn Refresher,
         key: KeyEvent,
     ) -> Result<Cmd> {
-        {
-            let bindings = self.custom_bindings.read().unwrap();
-            if let Some(cmd) = bindings.get(&key) {
-                debug!(target: "rustyline", "Custom command: {:?}", cmd);
-                return Ok(if cmd.is_repeatable() {
-                    cmd.redo(None, wrt)
-                } else {
-                    cmd.clone()
-                });
-            }
+        let evt = key.into();
+        if let Some(cmd) = self.custom_binding(wrt, &evt, 0, true) {
+            return Ok(if cmd.is_repeatable() {
+                cmd.redo(None, wrt)
+            } else {
+                cmd
+            });
         }
         let cmd = match key {
             E(K::Char(c), M::NONE) => {
@@ -837,7 +896,7 @@ impl InputState {
                 wrt.done_inserting();
                 Cmd::Move(Movement::BackwardChar(1))
             }
-            _ => self.common(rdr, key, 1, true)?,
+            _ => self.common(rdr, wrt, evt, key, 1, true)?,
         };
         debug!(target: "rustyline", "Vi insert: {:?}", cmd);
         if cmd.is_repeatable_change() {
@@ -938,6 +997,8 @@ impl InputState {
     fn common<R: RawReader>(
         &mut self,
         rdr: &mut R,
+        wrt: &mut dyn Refresher,
+        mut evt: Event,
         key: KeyEvent,
         n: RepeatCount,
         positive: bool,
@@ -1007,7 +1068,9 @@ impl InputState {
                 let paste = rdr.read_pasted_text()?;
                 Cmd::Insert(1, paste)
             },
-            _ => Cmd::Unknown,
+            _ => {
+                self.custom_seq_binding(rdr, wrt, &mut evt, n, positive)?.unwrap_or(Cmd::Unknown)
+            },
         })
     }
 
diff --git a/src/keys.rs b/src/keys.rs
index c147e49..6a250ae 100644
--- a/src/keys.rs
+++ b/src/keys.rs
@@ -187,6 +187,11 @@ mod tests {
         assert_eq!(E::ESC, E::new('\x1b', M::NONE));
     }
 
+    #[test]
+    fn from() {
+        assert_eq!(E(K::Tab, M::NONE), E::from('\t'));
+    }
+
     #[test]
     fn normalize() {
         assert_eq!(E::ctrl('A'), E::normalize(E(K::Char('\x01'), M::NONE)));
diff --git a/src/lib.rs b/src/lib.rs
index 343b70e..a79802c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,6 +17,7 @@
 //! ```
 #![warn(missing_docs)]
 
+mod binding;
 mod command;
 pub mod completion;
 pub mod config;
@@ -34,7 +35,6 @@ mod tty;
 mod undo;
 pub mod validate;
 
-use std::collections::HashMap;
 use std::fmt;
 use std::io::{self, Write};
 use std::path::Path;
@@ -42,10 +42,12 @@ use std::result;
 use std::sync::{Arc, Mutex, RwLock};
 
 use log::debug;
+use radix_trie::Trie;
 use unicode_width::UnicodeWidthStr;
 
 use crate::tty::{RawMode, Renderer, Term, Terminal};
 
+pub use crate::binding::{ConditionalEventHandler, Event, EventContext, EventHandler};
 use crate::completion::{longest_common_prefix, Candidate, Completer};
 pub use crate::config::{
     ColorMode, CompletionType, Config, EditMode, HistoryDuplicates, OutputStreamType,
@@ -54,7 +56,7 @@ use crate::edit::State;
 use crate::highlight::Highlighter;
 use crate::hint::Hinter;
 use crate::history::{Direction, History};
-pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
+pub use crate::keymap::{Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word};
 use crate::keymap::{InputState, Refresher};
 pub use crate::keys::{KeyCode, KeyEvent, Modifiers};
 use crate::kill_ring::KillRing;
@@ -635,7 +637,7 @@ pub struct Editor<H: Helper> {
     helper: Option<H>,
     kill_ring: Arc<Mutex<KillRing>>,
     config: Config,
-    custom_bindings: Arc<RwLock<HashMap<KeyEvent, Cmd>>>,
+    custom_bindings: Arc<RwLock<Trie<Event, EventHandler>>>,
 }
 
 #[allow(clippy::new_without_default)]
@@ -660,7 +662,7 @@ impl<H: Helper> Editor<H> {
             helper: None,
             kill_ring: Arc::new(Mutex::new(KillRing::new(60))),
             config,
-            custom_bindings: Arc::new(RwLock::new(HashMap::new())),
+            custom_bindings: Arc::new(RwLock::new(Trie::new())),
         }
     }
 
@@ -755,18 +757,22 @@ impl<H: Helper> Editor<H> {
     }
 
     /// Bind a sequence to a command.
-    pub fn bind_sequence(&mut self, key_seq: KeyEvent, cmd: Cmd) -> Option<Cmd> {
+    pub fn bind_sequence<E: Into<Event>, R: Into<EventHandler>>(
+        &mut self,
+        key_seq: E,
+        handler: R,
+    ) -> Option<EventHandler> {
         if let Ok(mut bindings) = self.custom_bindings.write() {
-            bindings.insert(KeyEvent::normalize(key_seq), cmd)
+            bindings.insert(Event::normalize(key_seq.into()), handler.into())
         } else {
             None
         }
     }
 
     /// Remove a binding for the given sequence.
-    pub fn unbind_sequence(&mut self, key_seq: KeyEvent) -> Option<Cmd> {
+    pub fn unbind_sequence<E: Into<Event>>(&mut self, key_seq: E) -> Option<EventHandler> {
         if let Ok(mut bindings) = self.custom_bindings.write() {
-            bindings.remove(&KeyEvent::normalize(key_seq))
+            bindings.remove(&Event::normalize(key_seq.into()))
         } else {
             None
         }
diff --git a/src/tty/windows.rs b/src/tty/windows.rs
index 01e3ec7..12de168 100644
--- a/src/tty/windows.rs
+++ b/src/tty/windows.rs
@@ -116,9 +116,7 @@ impl RawReader for ConsoleRawReader {
         let mut surrogate = 0;
         loop {
             // TODO GetNumberOfConsoleInputEvents
-            check(unsafe {
-                consoleapi::ReadConsoleInputW(self.handle, &mut rec, 1, &mut count)
-            })?;
+            check(unsafe { consoleapi::ReadConsoleInputW(self.handle, &mut rec, 1, &mut count) })?;
 
             if rec.EventType == wincon::WINDOW_BUFFER_SIZE_EVENT {
                 SIGWINCH.store(true, Ordering::SeqCst);
@@ -449,7 +447,9 @@ impl Renderer for ConsoleRenderer {
     }
 
     fn sigwinch(&self) -> bool {
-        SIGWINCH.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst).unwrap_or(false)
+        SIGWINCH
+            .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
+            .unwrap_or(false)
     }
 
     /// Try to get the number of columns in the current terminal,


Test patch:
diff --git a/src/test/mod.rs b/src/test/mod.rs
index a9d42bf..9b66b83 100644
--- a/src/test/mod.rs
+++ b/src/test/mod.rs
@@ -1,7 +1,8 @@
-use std::collections::HashMap;
 use std::sync::{Arc, RwLock};
 use std::vec::IntoIter;
 
+use radix_trie::Trie;
+
 use crate::completion::Completer;
 use crate::config::{Config, EditMode};
 use crate::edit::init_state;
@@ -58,7 +59,7 @@ fn complete_line() {
     let helper = Some(SimpleCompleter);
     let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history);
     let config = Config::default();
-    let mut input_state = InputState::new(&config, Arc::new(RwLock::new(HashMap::new())));
+    let mut input_state = InputState::new(&config, Arc::new(RwLock::new(Trie::new())));
     let keys = vec![E::ENTER];
     let mut rdr: IntoIter<KeyEvent> = keys.into_iter();
     let cmd = super::complete_line(&mut rdr, &mut s, &mut input_state, &Config::default()).unwrap();