Skip to content

Commit

Permalink
Merge pull request #435 from gwenn/key_mods
Browse files Browse the repository at this point in the history
Split KeyPress into separate Key and Modifier types.
  • Loading branch information
gwenn authored Oct 11, 2020
2 parents caa710c + 6dcfd61 commit 83999e5
Show file tree
Hide file tree
Showing 15 changed files with 951 additions and 929 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" }
members = ["rustyline-derive"]

[dependencies]
bitflags = "1.2"
cfg-if = "0.1.6"
dirs-next = { version = "1.0", optional = true }
fs2 = "0.4"
Expand Down
6 changes: 3 additions & 3 deletions examples/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::validate::{self, MatchingBracketValidator, Validator};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyEvent};
use rustyline_derive::Helper;

#[derive(Helper)]
Expand Down Expand Up @@ -97,8 +97,8 @@ fn main() -> rustyline::Result<()> {
};
let mut rl = Editor::with_config(config);
rl.set_helper(Some(h));
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyPress::Meta('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.");
}
Expand Down
293 changes: 150 additions & 143 deletions src/keymap.rs

Large diffs are not rendered by default.

213 changes: 144 additions & 69 deletions src/keys.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,119 @@
//! Key constants
/// Input key pressed and modifiers
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyEvent(pub KeyCode, pub Modifiers);

impl KeyEvent {
/// Constant value representing an unmodified press of `KeyCode::Backspace`.
pub(crate) const BACKSPACE: Self = Self(KeyCode::Backspace, Modifiers::NONE);
/// Constant value representing an unmodified press of `KeyCode::Enter`.
pub(crate) const ENTER: Self = Self(KeyCode::Enter, Modifiers::NONE);
/// Constant value representing an unmodified press of `KeyCode::Esc`.
pub(crate) const ESC: Self = Self(KeyCode::Esc, Modifiers::NONE);

/// Constructor from `char` and modifiers
pub fn new(c: char, mut mods: Modifiers) -> Self {
use {KeyCode as K, KeyEvent as E, Modifiers as M};

if !c.is_control() {
if !mods.is_empty() {
mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if
// `c` is uppercase
}
return E(K::Char(c), mods);
}
#[allow(clippy::match_same_arms)]
match c {
'\x00' => E(K::Char(' '), mods | M::CTRL),
'\x01' => E(K::Char('A'), mods | M::CTRL),
'\x02' => E(K::Char('B'), mods | M::CTRL),
'\x03' => E(K::Char('C'), mods | M::CTRL),
'\x04' => E(K::Char('D'), mods | M::CTRL),
'\x05' => E(K::Char('E'), mods | M::CTRL),
'\x06' => E(K::Char('F'), mods | M::CTRL),
'\x07' => E(K::Char('G'), mods | M::CTRL),
'\x08' => E(K::Backspace, mods), // '\b'
'\x09' => {
// '\t'
if mods.contains(M::SHIFT) {
mods.remove(M::SHIFT);
E(K::BackTab, mods)
} else {
E(K::Tab, mods)
}
}
'\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10)
'\x0b' => E(K::Char('K'), mods | M::CTRL),
'\x0c' => E(K::Char('L'), mods | M::CTRL),
'\x0d' => E(K::Enter, mods), // '\r' (13)
'\x0e' => E(K::Char('N'), mods | M::CTRL),
'\x0f' => E(K::Char('O'), mods | M::CTRL),
'\x10' => E(K::Char('P'), mods | M::CTRL),
'\x11' => E(K::Char('Q'), mods | M::CTRL),
'\x12' => E(K::Char('R'), mods | M::CTRL),
'\x13' => E(K::Char('S'), mods | M::CTRL),
'\x14' => E(K::Char('T'), mods | M::CTRL),
'\x15' => E(K::Char('U'), mods | M::CTRL),
'\x16' => E(K::Char('V'), mods | M::CTRL),
'\x17' => E(K::Char('W'), mods | M::CTRL),
'\x18' => E(K::Char('X'), mods | M::CTRL),
'\x19' => E(K::Char('Y'), mods | M::CTRL),
'\x1a' => E(K::Char('Z'), mods | M::CTRL),
'\x1b' => E(K::Esc, mods), // Ctrl-[
'\x1c' => E(K::Char('\\'), mods | M::CTRL),
'\x1d' => E(K::Char(']'), mods | M::CTRL),
'\x1e' => E(K::Char('^'), mods | M::CTRL),
'\x1f' => E(K::Char('_'), mods | M::CTRL),
'\x7f' => E(K::Backspace, mods), // Rubout
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
_ => E(K::Null, mods),
}
}

/// Constructor from `char` with Ctrl modifier
pub fn ctrl(c: char) -> Self {
Self::new(c, Modifiers::CTRL)
}

/// Constructor from `char` with Alt modifier
pub fn alt(c: char) -> Self {
Self::new(c, Modifiers::ALT)
}

/// ctrl-a => ctrl-A (uppercase)
/// shift-A => A (no SHIFT modifier)
/// shift-Tab => BackTab
pub fn normalize(e: Self) -> Self {
use {KeyCode as K, KeyEvent as E, Modifiers as M};

match e {
E(K::Char(c), m) if c.is_ascii_control() => Self::new(c, m),
E(K::Char(c), m) if c.is_ascii_lowercase() && m.contains(M::CTRL) => {
E(K::Char(c.to_ascii_uppercase()), m)
}
E(K::Char(c), m) if c.is_ascii_uppercase() && m.contains(M::SHIFT) => {
E(K::Char(c), m ^ M::SHIFT)
}
E(K::Tab, m) if m.contains(M::SHIFT) => E(K::BackTab, m ^ M::SHIFT),
_ => e,
}
}
}

impl From<char> for KeyEvent {
fn from(c: char) -> Self {
Self::new(c, Modifiers::NONE)
}
}

/// Input key pressed
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum KeyPress {
pub enum KeyCode {
/// Unsupported escape sequence (on unix platform)
UnknownEscSeq,
/// ⌫ or `KeyPress::Ctrl('H')`
/// ⌫ or Ctrl-H
Backspace,
/// ⇤ (usually Shift-Tab)
BackTab,
Expand All @@ -16,25 +123,15 @@ pub enum KeyPress {
BracketedPasteEnd,
/// Single char
Char(char),
/// Ctrl-↓
ControlDown,
/// Ctrl-←
ControlLeft,
/// Ctrl-→
ControlRight,
/// Ctrl-↑
ControlUp,
/// Ctrl-char
Ctrl(char),
/// ⌦
Delete,
/// ↓ arrow key
Down,
/// ⇲
End,
/// ↵ or `KeyPress::Ctrl('M')`
/// ↵ or Ctrl-M
Enter,
/// Escape or `KeyPress::Ctrl('[')`
/// Escape or Ctrl-[
Esc,
/// Function key
F(u8),
Expand All @@ -44,79 +141,57 @@ pub enum KeyPress {
Insert,
/// ← arrow key
Left,
/// Escape-char or Alt-char
Meta(char),
/// `KeyPress::Char('\0')`
/// \0
Null,
/// ⇟
PageDown,
/// ⇞
PageUp,
/// → arrow key
Right,
/// Shift-↓
ShiftDown,
/// Shift-←
ShiftLeft,
/// Shift-→
ShiftRight,
/// Shift-↑
ShiftUp,
/// ⇥ or `KeyPress::Ctrl('I')`
/// ⇥ or Ctrl-I
Tab,
/// ↑ arrow key
Up,
}

#[cfg(any(windows, unix))]
pub fn char_to_key_press(c: char) -> KeyPress {
if !c.is_control() {
return KeyPress::Char(c);
}
#[allow(clippy::match_same_arms)]
match c {
'\x00' => KeyPress::Ctrl(' '),
'\x01' => KeyPress::Ctrl('A'),
'\x02' => KeyPress::Ctrl('B'),
'\x03' => KeyPress::Ctrl('C'),
'\x04' => KeyPress::Ctrl('D'),
'\x05' => KeyPress::Ctrl('E'),
'\x06' => KeyPress::Ctrl('F'),
'\x07' => KeyPress::Ctrl('G'),
'\x08' => KeyPress::Backspace, // '\b'
'\x09' => KeyPress::Tab, // '\t'
'\x0a' => KeyPress::Ctrl('J'), // '\n' (10)
'\x0b' => KeyPress::Ctrl('K'),
'\x0c' => KeyPress::Ctrl('L'),
'\x0d' => KeyPress::Enter, // '\r' (13)
'\x0e' => KeyPress::Ctrl('N'),
'\x0f' => KeyPress::Ctrl('O'),
'\x10' => KeyPress::Ctrl('P'),
'\x12' => KeyPress::Ctrl('R'),
'\x13' => KeyPress::Ctrl('S'),
'\x14' => KeyPress::Ctrl('T'),
'\x15' => KeyPress::Ctrl('U'),
'\x16' => KeyPress::Ctrl('V'),
'\x17' => KeyPress::Ctrl('W'),
'\x18' => KeyPress::Ctrl('X'),
'\x19' => KeyPress::Ctrl('Y'),
'\x1a' => KeyPress::Ctrl('Z'),
'\x1b' => KeyPress::Esc, // Ctrl-[
'\x1c' => KeyPress::Ctrl('\\'),
'\x1d' => KeyPress::Ctrl(']'),
'\x1e' => KeyPress::Ctrl('^'),
'\x1f' => KeyPress::Ctrl('_'),
'\x7f' => KeyPress::Backspace, // Rubout
_ => KeyPress::Null,
bitflags::bitflags! {
/// The set of modifier keys that were triggered along with a key press.
pub struct Modifiers: u8 {
/// Control modifier
const CTRL = 1<<3;
/// Escape or Alt modifier
const ALT = 1<<2;
/// Shift modifier
const SHIFT = 1<<1;

/// No modifier
const NONE = 0;
/// Ctrl + Shift
const CTRL_SHIFT = Self::CTRL.bits | Self::SHIFT.bits;
/// Alt + Shift
const ALT_SHIFT = Self::ALT.bits | Self::SHIFT.bits;
/// Ctrl + Alt
const CTRL_ALT = Self::CTRL.bits | Self::ALT.bits;
/// Ctrl + Alt + Shift
const CTRL_ALT_SHIFT = Self::CTRL.bits | Self::ALT.bits | Self::SHIFT.bits;
}
}

#[cfg(test)]
mod tests {
use super::{char_to_key_press, KeyPress};
use super::{KeyCode as K, KeyEvent as E, Modifiers as M};

#[test]
fn new() {
assert_eq!(E::ESC, E::new('\x1b', M::NONE));
}

#[test]
fn char_to_key() {
assert_eq!(KeyPress::Esc, char_to_key_press('\x1b'));
fn normalize() {
assert_eq!(E::ctrl('A'), E::normalize(E(K::Char('\x01'), M::NONE)));
assert_eq!(E::ctrl('A'), E::normalize(E::ctrl('a')));
assert_eq!(E::from('A'), E::normalize(E(K::Char('A'), M::SHIFT)));
assert_eq!(E(K::BackTab, M::NONE), E::normalize(E(K::Tab, M::SHIFT)));
}
}
12 changes: 6 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use crate::hint::Hinter;
use crate::history::{Direction, History};
pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
use crate::keymap::{InputState, Refresher};
pub use crate::keys::KeyPress;
pub use crate::keys::{KeyCode, KeyEvent, Modifiers};
use crate::kill_ring::{KillRing, Mode};
use crate::line_buffer::WordAction;
use crate::validate::Validator;
Expand Down Expand Up @@ -804,7 +804,7 @@ pub struct Editor<H: Helper> {
helper: Option<H>,
kill_ring: Arc<Mutex<KillRing>>,
config: Config,
custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>,
custom_bindings: Arc<RwLock<HashMap<KeyEvent, Cmd>>>,
}

#[allow(clippy::new_without_default)]
Expand Down Expand Up @@ -923,18 +923,18 @@ impl<H: Helper> Editor<H> {
}

/// Bind a sequence to a command.
pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option<Cmd> {
pub fn bind_sequence(&mut self, key_seq: KeyEvent, cmd: Cmd) -> Option<Cmd> {
if let Ok(mut bindings) = self.custom_bindings.write() {
bindings.insert(key_seq, cmd)
bindings.insert(KeyEvent::normalize(key_seq), cmd)
} else {
None
}
}

/// Remove a binding for the given sequence.
pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option<Cmd> {
pub fn unbind_sequence(&mut self, key_seq: KeyEvent) -> Option<Cmd> {
if let Ok(mut bindings) = self.custom_bindings.write() {
bindings.remove(&key_seq)
bindings.remove(&KeyEvent::normalize(key_seq))
} else {
None
}
Expand Down
Loading

0 comments on commit 83999e5

Please sign in to comment.