use std::fmt;
#[cfg(all(unix, feature = "termion"))]
mod via_termion;
#[cfg(all(unix, feature = "termion"))]
pub use via_termion::from_termion;
#[cfg(feature = "termwiz")]
mod via_termwiz;
#[cfg(feature = "termwiz")]
pub use via_termwiz::{from_termwiz, from_termwiz_kitty};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Modifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
}
impl Modifiers {
pub const NONE: Modifiers = Modifiers {
shift: false,
ctrl: false,
alt: false,
};
pub const CTRL: Modifiers = Modifiers {
shift: false,
ctrl: true,
alt: false,
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum KeyCode {
Char(char),
Enter,
Tab,
BackTab,
Backspace,
Delete,
Insert,
Escape,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
F(u8),
Menu,
Modifier(ModifierKey),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ModifierKey {
Shift,
Ctrl,
Alt,
Super,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Releases {
#[default]
None,
ExceptText,
All,
}
impl Releases {
pub fn reported(self, key: &Key) -> bool {
match self {
Releases::None => false,
Releases::All => true,
Releases::ExceptText => {
let text = match key.code {
KeyCode::Char(_) => !key.modifiers.ctrl && !key.modifiers.alt,
KeyCode::Enter | KeyCode::Tab | KeyCode::Backspace => true,
KeyCode::Modifier(_) => true,
_ => false,
};
!text
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Key {
pub code: KeyCode,
pub modifiers: Modifiers,
exact: bool,
}
impl PartialEq for Key {
fn eq(&self, other: &Key) -> bool {
self.code == other.code && self.modifiers == other.modifiers
}
}
impl Eq for Key {}
impl std::hash::Hash for Key {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.code.hash(state);
self.modifiers.hash(state);
}
}
impl Key {
pub const fn new(code: KeyCode) -> Key {
Key::with(code, Modifiers::NONE)
}
pub const fn with(code: KeyCode, modifiers: Modifiers) -> Key {
Key {
code,
modifiers,
exact: false,
}
}
pub const fn ctrl(c: char) -> Key {
Key::with(KeyCode::Char(c), Modifiers::CTRL)
}
pub const fn char(c: char) -> Key {
Key::new(KeyCode::Char(c))
}
pub fn parse(text: &str) -> Option<Key> {
if text == "+" {
return Some(Key::char('+'));
}
let mut modifiers = Modifiers::NONE;
let mut parts: Vec<&str> = text.split('+').collect();
if text.ends_with("++") {
parts.truncate(parts.len() - 2);
parts.push("+");
}
let (name, held) = parts.split_last()?;
for modifier in held {
match modifier.to_ascii_lowercase().as_str() {
"ctrl" | "control" => modifiers.ctrl = true,
"alt" | "meta" => modifiers.alt = true,
"shift" => modifiers.shift = true,
_ => return None,
}
}
let mut chars = name.chars();
let code = match (chars.next(), chars.next()) {
(Some(c), None) if modifiers.shift => {
if !c.is_alphabetic() {
return None;
}
modifiers.shift = false;
KeyCode::Char(c.to_uppercase().next().unwrap_or(c))
}
(Some(c), None) if modifiers.ctrl => KeyCode::Char(c.to_ascii_lowercase()),
(Some(c), None) => KeyCode::Char(c),
_ => match name.to_ascii_lowercase().as_str() {
"enter" | "return" => KeyCode::Enter,
"tab" if modifiers.shift => {
modifiers.shift = false;
KeyCode::BackTab
}
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"backspace" => KeyCode::Backspace,
"delete" | "del" => KeyCode::Delete,
"insert" => KeyCode::Insert,
"menu" => KeyCode::Menu,
"shift" => KeyCode::Modifier(ModifierKey::Shift),
"ctrl" | "control" => KeyCode::Modifier(ModifierKey::Ctrl),
"alt" | "meta" => KeyCode::Modifier(ModifierKey::Alt),
"super" | "cmd" | "win" => KeyCode::Modifier(ModifierKey::Super),
"escape" | "esc" => KeyCode::Escape,
"space" => KeyCode::Char(' '),
"comma" => KeyCode::Char(','),
other if punctuation(other).is_some() && modifiers.shift => return None,
other if punctuation(other).is_some() => KeyCode::Char(punctuation(other)?),
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
other => match other.strip_prefix('f')?.parse().ok()? {
n @ 1..=24 => KeyCode::F(n),
_ => return None,
},
},
};
Some(Key::with(code, modifiers))
}
#[must_use]
pub const fn exact(self) -> Key {
Key {
exact: true,
..self
}
}
pub const fn is_exact(&self) -> bool {
self.exact
}
#[must_use]
pub fn legacy(self) -> Key {
let mut key = Key {
exact: false,
..self
};
let KeyCode::Char(c) = key.code else {
return key;
};
if !key.modifiers.ctrl {
return key;
}
key.code = match c.to_ascii_lowercase() {
'i' => KeyCode::Tab,
'm' => KeyCode::Enter,
'[' => KeyCode::Escape,
'\\' => KeyCode::Char('4'),
']' => KeyCode::Char('5'),
'^' => KeyCode::Char('6'),
'_' => KeyCode::Char('7'),
'@' => KeyCode::Char(' '),
c => KeyCode::Char(c),
};
if matches!(key.code, KeyCode::Tab | KeyCode::Enter | KeyCode::Escape) {
key.modifiers.ctrl = false;
}
key
}
pub fn matches(&self, binding: &Key) -> bool {
if self == binding {
return true;
}
if self.exact {
return false;
}
if binding.legacy() == *self {
return true;
}
binding.code == KeyCode::Backspace
&& !binding.modifiers.ctrl
&& self.code == KeyCode::Char('h')
&& self.modifiers
== Modifiers {
ctrl: true,
..binding.modifiers
}
}
pub fn matches_any<'a>(&self, bindings: impl IntoIterator<Item = &'a Key>) -> bool {
bindings.into_iter().any(|binding| self.matches(binding))
}
pub fn pick<'a>(&self, sets: impl IntoIterator<Item = &'a [Key]>) -> Option<usize> {
let sets: Vec<&[Key]> = sets.into_iter().collect();
sets.iter()
.position(|keys| keys.contains(self))
.or_else(|| sets.iter().position(|keys| self.matches_any(*keys)))
}
pub fn is_interrupt(&self) -> bool {
self.modifiers.ctrl && self.code == KeyCode::Char('c')
}
}
pub fn punctuation(name: &str) -> Option<char> {
Some(match name {
"equals" => '=',
"minus" | "dash" => '-',
"plus" => '+',
"underscore" => '_',
"slash" => '/',
"backslash" => '\\',
"period" | "dot" => '.',
"comma" => ',',
"semicolon" => ';',
"colon" => ':',
"quote" | "apostrophe" => '\'',
"doublequote" => '"',
"backtick" | "grave" => '`',
"tilde" => '~',
"bracketleft" => '[',
"bracketright" => ']',
"braceleft" => '{',
"braceright" => '}',
"parenleft" => '(',
"parenright" => ')',
"lt" | "less" => '<',
"gt" | "greater" => '>',
"question" => '?',
"exclam" | "bang" => '!',
"at" => '@',
"hash" => '#',
"dollar" => '$',
"percent" => '%',
"caret" => '^',
"ampersand" => '&',
"asterisk" | "star" => '*',
"pipe" => '|',
_ => return None,
})
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.modifiers.ctrl {
f.write_str("ctrl+")?;
}
if self.modifiers.alt {
f.write_str("alt+")?;
}
let capital =
matches!(self.code, KeyCode::Char(c) if self.modifiers.ctrl && c.is_ascii_uppercase());
if self.modifiers.shift || self.code == KeyCode::BackTab || capital {
f.write_str("shift+")?;
}
match self.code {
KeyCode::BackTab => f.write_str("tab"),
KeyCode::Char(' ') => f.write_str("space"),
KeyCode::Char(c) if capital => write!(f, "{}", c.to_ascii_lowercase()),
KeyCode::Char(c) => write!(f, "{c}"),
KeyCode::F(n) => write!(f, "f{n}"),
KeyCode::Modifier(key) => f.write_str(match key {
ModifierKey::Shift => "shift",
ModifierKey::Ctrl => "ctrl",
ModifierKey::Alt => "alt",
ModifierKey::Super => "super",
}),
code => f.write_str(&format!("{code:?}").to_ascii_lowercase()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Button {
Left,
Right,
Middle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MouseKind {
Down(Button),
Up(Button),
Drag(Button),
Moved,
ScrollUp,
ScrollDown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Mouse {
pub kind: MouseKind,
pub column: u16,
pub row: u16,
pub modifiers: Modifiers,
pub pixel: Option<Subcell>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Subcell {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Mouse {
pub const fn new(kind: MouseKind, column: u16, row: u16) -> Mouse {
Mouse {
kind,
column,
row,
modifiers: Modifiers::NONE,
pixel: None,
}
}
pub fn from_pixels(kind: MouseKind, x: u32, y: u32, cell: (u16, u16)) -> Option<Mouse> {
let (width, height) = cell;
if width == 0 || height == 0 {
return None;
}
let (w, h) = (u32::from(width), u32::from(height));
let cells = |at: u32, size: u32| u16::try_from(at / size).unwrap_or(u16::MAX);
Some(Mouse {
pixel: Some(Subcell {
x: (x % w) as u16,
y: (y % h) as u16,
width,
height,
}),
..Mouse::new(kind, cells(x, w), cells(y, h))
})
}
pub fn precise(&self) -> (f32, f32) {
match self.pixel {
Some(p) if p.width > 0 && p.height > 0 => (
f32::from(self.column) + f32::from(p.x) / f32::from(p.width),
f32::from(self.row) + f32::from(p.y) / f32::from(p.height),
),
_ => (f32::from(self.column) + 0.5, f32::from(self.row) + 0.5),
}
}
pub fn reread_as_pixels(self, cell: (u16, u16)) -> Option<Mouse> {
Mouse::from_pixels(self.kind, u32::from(self.column), u32::from(self.row), cell).map(
|read| Mouse {
modifiers: self.modifiers,
..read
},
)
}
pub fn is_click(&self) -> bool {
self.kind == MouseKind::Down(Button::Left)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
Key(Key),
KeyUp(Key),
Mouse(Mouse),
Resize {
columns: u16,
rows: u16,
},
Paste(String),
Tick,
Returned(Option<i32>),
Link(String),
}
impl Event {
pub fn key(&self) -> Option<Key> {
match self {
Event::Key(key) => Some(*key),
_ => None,
}
}
}
impl Event {
pub fn mouse(&self) -> Option<Mouse> {
match self {
Event::Mouse(mouse) => Some(*mouse),
_ => None,
}
}
}
impl From<Key> for Event {
fn from(key: Key) -> Event {
Event::Key(key)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HeldButton(Option<Button>);
impl HeldButton {
pub fn get(self) -> Option<Button> {
self.0
}
}
#[cfg(any(unix, feature = "termwiz"))]
fn char_key(c: char, mut modifiers: Modifiers) -> Key {
let code = match c {
'\r' | '\n' => KeyCode::Enter,
'\t' => KeyCode::Tab,
'\x1b' => KeyCode::Escape,
'\x7f' => KeyCode::Backspace,
'\0' => {
modifiers.ctrl = true;
KeyCode::Char(' ')
}
'\x01'..='\x1a' => {
modifiers.ctrl = true;
KeyCode::Char((c as u8 - 1 + b'a') as char)
}
'\x1c'..='\x1f' => {
modifiers.ctrl = true;
KeyCode::Char((c as u8 - 0x1c + b'4') as char)
}
c => KeyCode::Char(c),
};
Key::with(code, modifiers)
}
#[cfg(unix)]
pub(crate) fn from_legacy_bytes(bytes: &[u8]) -> Vec<Event> {
let text = String::from_utf8_lossy(bytes);
let chars: Vec<char> = text.chars().collect();
let mut events = Vec::new();
let mut at = 0;
while at < chars.len() {
let c = chars[at];
if c != '\x1b' {
events.push(Event::Key(char_key(c, Modifiers::NONE)));
at += 1;
continue;
}
match chars.get(at + 1) {
None | Some('\x1b') => {
events.push(Event::Key(Key::new(KeyCode::Escape)));
at += 1;
}
Some('[') => {
let start = at + 2;
let mut end = start;
while chars
.get(end)
.is_some_and(|c| ('\x20'..='\x3f').contains(c))
{
end += 1;
}
let Some(&last) = chars.get(end) else {
break;
};
let parameters: String = chars[start..end].iter().collect();
if let Some(key) = csi_key(¶meters, last) {
events.push(Event::Key(key));
}
at = end + 1;
}
Some('O') if at + 2 < chars.len() => {
if let Some(key) = csi_key("", chars[at + 2]) {
events.push(Event::Key(key));
}
at += 3;
}
Some(&c) => {
let alt = Modifiers {
alt: true,
..Modifiers::NONE
};
events.push(Event::Key(char_key(c, alt)));
at += 2;
}
}
}
events
}
#[cfg(unix)]
fn csi_key(parameters: &str, last: char) -> Option<Key> {
let mut parts = parameters.split(';');
let first = parts.next().unwrap_or("");
let modifiers = match parts.next().map(str::parse::<u8>) {
Some(Ok(code)) => {
let bits = code.saturating_sub(1);
Modifiers {
shift: bits & 1 != 0,
alt: bits & 2 != 0,
ctrl: bits & 4 != 0,
}
}
Some(Err(_)) => return None,
None => Modifiers::NONE,
};
let code = match last {
'A' => KeyCode::Up,
'B' => KeyCode::Down,
'C' => KeyCode::Right,
'D' => KeyCode::Left,
'H' => KeyCode::Home,
'F' => KeyCode::End,
'P'..='S' => KeyCode::F(1 + (last as u8 - b'P')),
'Z' => return Some(Key::new(KeyCode::BackTab)),
'~' => match first.parse::<u8>().ok()? {
1 | 7 => KeyCode::Home,
2 => KeyCode::Insert,
3 => KeyCode::Delete,
4 | 8 => KeyCode::End,
5 => KeyCode::PageUp,
6 => KeyCode::PageDown,
n @ 11..=15 => KeyCode::F(n - 10),
n @ 17..=21 => KeyCode::F(n - 11),
n @ 23..=24 => KeyCode::F(n - 12),
_ => return None,
},
_ => return None,
};
Some(Key::with(code, modifiers))
}
#[cfg(not(target_arch = "wasm32"))]
fn modifiers(state: crossterm::event::KeyModifiers) -> Modifiers {
use crossterm::event::KeyModifiers as M;
Modifiers {
shift: state.contains(M::SHIFT),
ctrl: state.contains(M::CONTROL),
alt: state.contains(M::ALT),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn button(button: crossterm::event::MouseButton) -> Button {
use crossterm::event::MouseButton as B;
match button {
B::Left => Button::Left,
B::Right => Button::Right,
B::Middle => Button::Middle,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_crossterm(event: crossterm::event::Event) -> Option<Event> {
translate(event, false)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_crossterm_kitty(event: crossterm::event::Event) -> Option<Event> {
translate(event, true)
}
#[cfg(not(target_arch = "wasm32"))]
fn translate(event: crossterm::event::Event, exact: bool) -> Option<Event> {
use crossterm::event::{
Event as E, KeyCode as K, KeyEventKind, KeyModifiers as M, MouseEventKind as MK,
};
Some(match event {
E::Key(key) if matches!(key.code, crossterm::event::KeyCode::Modifier(_)) => {
use crossterm::event::{KeyCode as K, ModifierKeyCode as MK};
let K::Modifier(which) = key.code else {
return None;
};
let code = KeyCode::Modifier(match which {
MK::LeftShift | MK::RightShift => ModifierKey::Shift,
MK::LeftControl | MK::RightControl => ModifierKey::Ctrl,
MK::LeftAlt | MK::RightAlt | MK::LeftMeta | MK::RightMeta => ModifierKey::Alt,
MK::LeftSuper | MK::RightSuper => ModifierKey::Super,
_ => return None,
});
let mut read = Key::new(code);
read.exact = exact;
if key.kind == KeyEventKind::Release {
Event::KeyUp(read)
} else {
Event::Key(read)
}
}
E::Key(key) => {
if key.modifiers.intersects(M::SUPER | M::HYPER) {
return None;
}
let mut held = modifiers(key.modifiers);
held.alt |= key.modifiers.contains(M::META);
let code = match key.code {
K::Char(c) => {
if held.shift && c.is_ascii_lowercase() {
held.shift = false;
KeyCode::Char(c.to_ascii_uppercase())
} else {
if c.is_uppercase() {
held.shift = false;
}
KeyCode::Char(c)
}
}
K::Enter => KeyCode::Enter,
K::Tab => KeyCode::Tab,
K::BackTab => {
held.shift = false;
KeyCode::BackTab
}
K::Backspace => KeyCode::Backspace,
K::Delete => KeyCode::Delete,
K::Insert => KeyCode::Insert,
K::Esc => KeyCode::Escape,
K::Up => KeyCode::Up,
K::Down => KeyCode::Down,
K::Left => KeyCode::Left,
K::Right => KeyCode::Right,
K::Home => KeyCode::Home,
K::End => KeyCode::End,
K::PageUp => KeyCode::PageUp,
K::PageDown => KeyCode::PageDown,
K::F(n) => KeyCode::F(n),
K::Menu => KeyCode::Menu,
_ => return None,
};
let mut read = Key::with(code, held);
read.exact = exact;
if key.kind == KeyEventKind::Release {
Event::KeyUp(read)
} else {
Event::Key(read)
}
}
E::Mouse(mouse) => Event::Mouse(Mouse {
kind: match mouse.kind {
MK::Down(b) => MouseKind::Down(button(b)),
MK::Up(b) => MouseKind::Up(button(b)),
MK::Drag(b) => MouseKind::Drag(button(b)),
MK::Moved => MouseKind::Moved,
MK::ScrollUp => MouseKind::ScrollUp,
MK::ScrollDown => MouseKind::ScrollDown,
_ => return None,
},
column: mouse.column,
row: mouse.row,
modifiers: modifiers(mouse.modifiers),
pixel: None,
}),
E::Resize(columns, rows) => Event::Resize { columns, rows },
E::Paste(text) => Event::Paste(text),
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn reads_legacy_bytes() {
let keys = |bytes: &[u8]| -> Vec<String> {
from_legacy_bytes(bytes)
.into_iter()
.map(|event| event.key().expect("a key").to_string())
.collect()
};
assert_eq!(
keys(b"qA \r\t\x7f\x03"),
["q", "A", "space", "enter", "tab", "backspace", "ctrl+c"]
);
assert_eq!(
keys(b"\x1b[A\x1b[1;5C\x1bOP\x1b[5~\x1b[15~\x1b[Z"),
["up", "ctrl+right", "f1", "pageup", "f5", "shift+tab"]
);
assert_eq!(keys("\x1bxé\x1b".as_bytes()), ["alt+x", "é", "escape"]);
assert_eq!(keys(b"a\x1b[99q b\x1b[1;"), ["a", "space", "b"]);
}
#[test]
fn parses_key_names() {
assert_eq!(Key::parse("enter"), Some(Key::new(KeyCode::Enter)));
assert_eq!(Key::parse("Ctrl+C"), Some(Key::ctrl('c')));
assert!(Key::parse("ctrl+c").unwrap().is_interrupt());
assert_eq!(Key::parse("shift+tab"), Some(Key::new(KeyCode::BackTab)));
assert_eq!(Key::parse("f12"), Some(Key::new(KeyCode::F(12))));
assert_eq!(Key::parse("space"), Some(Key::char(' ')));
assert_eq!(Key::parse("comma"), Some(Key::char(',')));
assert_eq!(Key::parse("ctrl+comma"), Some(Key::ctrl(',')));
assert_eq!(Key::parse("x"), Some(Key::char('x')));
assert_eq!(Key::parse("ctrl++").unwrap().code, KeyCode::Char('+'));
assert_eq!(Key::parse("+"), Some(Key::char('+')));
assert_eq!(Key::parse("hyper+x"), None);
assert_eq!(Key::parse("fx"), None);
}
#[test]
fn punctuation_keys_have_names() {
for (name, c) in [
("equals", '='),
("minus", '-'),
("plus", '+'),
("underscore", '_'),
("slash", '/'),
("backslash", '\\'),
("period", '.'),
("dot", '.'),
("semicolon", ';'),
("colon", ':'),
("quote", '\''),
("backtick", '`'),
("tilde", '~'),
("bracketleft", '['),
("bracketright", ']'),
("lt", '<'),
("gt", '>'),
("question", '?'),
] {
assert_eq!(Key::parse(name), Some(Key::char(c)), "{name}");
assert_eq!(Key::parse(name).unwrap().to_string(), c.to_string());
}
assert_eq!(Key::parse("Ctrl+Equals"), Some(Key::ctrl('=')));
let alt = Modifiers {
alt: true,
..Modifiers::NONE
};
assert_eq!(
Key::parse("alt+slash"),
Some(Key::with(KeyCode::Char('/'), alt))
);
assert_eq!(Key::parse("shift+minus"), None);
}
#[test]
fn the_menu_key_has_a_name() {
assert_eq!(Key::parse("menu"), Some(Key::new(KeyCode::Menu)));
assert_eq!(Key::parse("Menu").unwrap().to_string(), "menu");
assert_eq!(Key::parse("shift+menu").unwrap().to_string(), "shift+menu");
}
fn key(name: &str) -> Key {
Key::parse(name).expect("a key name")
}
#[test]
fn names_are_the_keys_terminals_send() {
assert_eq!(Key::parse("shift+a"), Some(Key::char('A')));
assert_eq!(Key::parse("alt+shift+a"), Key::parse("alt+A"));
assert_eq!(
Key::parse("ctrl+shift+a"),
Some(Key::with(KeyCode::Char('A'), Modifiers::CTRL))
);
assert_eq!(Key::parse("ctrl+A"), Some(Key::ctrl('a')));
assert_eq!(Key::parse("shift+1"), None);
assert_eq!(Key::parse("ctrl+i"), Some(Key::ctrl('i')));
assert_eq!(Key::parse("ctrl+m"), Some(Key::ctrl('m')));
assert_eq!(Key::parse("ctrl+["), Some(Key::ctrl('[')));
assert_eq!(Key::parse("ctrl+]"), Some(Key::ctrl(']')));
assert_eq!(Key::parse("ctrl+@"), Some(Key::ctrl('@')));
assert_eq!(Key::parse("ctrl+h"), Some(Key::ctrl('h')));
assert!(key("tab").matches(&key("ctrl+i")));
assert!(key("enter").matches(&key("ctrl+m")));
assert!(key("esc").matches(&key("ctrl+[")));
assert!(Key::ctrl('5').matches(&key("ctrl+]")));
assert!(key("ctrl+space").matches(&key("ctrl+@")));
assert!(key("ctrl+a").matches(&key("ctrl+shift+a")));
assert_eq!(Key::parse("f24"), Some(Key::new(KeyCode::F(24))));
assert_eq!(Key::parse("f0"), None);
assert_eq!(Key::parse("f99"), None);
}
#[test]
fn legacy_keys_match_every_key_they_could_be() {
for (name, sent) in [
("ctrl+i", "tab"),
("ctrl+m", "enter"),
("ctrl+[", "esc"),
("ctrl+\\", "ctrl+4"),
("ctrl+]", "ctrl+5"),
("ctrl+^", "ctrl+6"),
("ctrl+_", "ctrl+7"),
("ctrl+@", "ctrl+space"),
("alt+ctrl+i", "alt+tab"),
("ctrl+shift+a", "ctrl+a"),
("ctrl+h", "ctrl+h"),
("tab", "tab"),
("f5", "f5"),
] {
assert_eq!(key(name).legacy(), key(sent), "{name}");
assert!(key(sent).matches(&key(name)), "{sent} fires {name}");
assert!(key(sent).matches(&key(sent)), "{sent} fires {sent}");
}
assert!(key("ctrl+h").matches(&key("backspace")));
assert!(key("alt+ctrl+h").matches(&key("alt+backspace")));
assert!(!key("backspace").matches(&key("ctrl+h")));
assert!(!key("ctrl+i").matches(&key("tab")));
assert!(!key("tab").matches(&key("ctrl+j")));
assert!(!key("ctrl+a").matches(&key("ctrl+b")));
assert!(key("tab").matches_any(&keys(&["x", "ctrl+i"])));
}
fn keys(names: &[&str]) -> Vec<Key> {
names.iter().map(|name| key(name)).collect()
}
#[test]
fn exact_keys_match_only_themselves() {
assert!(key("tab").exact().matches(&key("tab")));
assert!(!key("tab").exact().matches(&key("ctrl+i")));
assert!(key("ctrl+i").exact().matches(&key("ctrl+i")));
assert!(!key("ctrl+i").exact().matches(&key("tab")));
assert!(!key("enter").exact().matches(&key("ctrl+m")));
assert!(!key("esc").exact().matches(&key("ctrl+[")));
assert!(!key("ctrl+h").exact().matches(&key("backspace")));
assert!(!key("ctrl+a").exact().matches(&key("ctrl+shift+a")));
assert_eq!(key("tab").exact(), key("tab"));
assert!(key("tab").exact().is_exact());
assert!(!key("tab").exact().legacy().is_exact());
let mut set = std::collections::HashSet::new();
set.insert(key("q").exact());
assert!(set.contains(&key("q")));
}
#[test]
fn an_exact_name_wins_over_a_key_it_could_be() {
let bindings = [keys(&["ctrl+i"]), keys(&["x", "tab"])];
let sets = || bindings.iter().map(Vec::as_slice);
assert_eq!(key("tab").pick(sets()), Some(1));
assert_eq!(key("tab").exact().pick(sets()), Some(1));
assert_eq!(key("ctrl+i").pick(sets()), Some(0));
let only = [keys(&["ctrl+i"])];
assert_eq!(key("tab").pick(only.iter().map(Vec::as_slice)), Some(0));
assert_eq!(
key("tab").exact().pick(only.iter().map(Vec::as_slice)),
None
);
}
#[test]
fn keys_display_as_they_parse() {
for name in [
"ctrl+c",
"enter",
"pagedown",
"space",
"f3",
"alt+x",
"ctrl+i",
"ctrl+[",
"ctrl+shift+a",
"shift+tab",
"A",
] {
assert_eq!(key(name).to_string(), name);
}
}
#[test]
fn translates_crossterm_events() {
use crossterm::event::{Event as E, KeyCode as K, KeyEvent, KeyModifiers as M};
let press = E::Key(KeyEvent::new(K::Char('c'), M::CONTROL));
assert_eq!(from_crossterm(press), Some(Event::Key(Key::ctrl('c'))));
let upper = E::Key(KeyEvent::new(K::Char('A'), M::SHIFT));
assert_eq!(from_crossterm(upper), Some(Event::Key(Key::char('A'))));
assert_eq!(
from_crossterm(E::Resize(80, 24)),
Some(Event::Resize {
columns: 80,
rows: 24
})
);
}
#[test]
fn releases_and_repeats_from_crossterm() {
use crossterm::event::{
Event as E, KeyCode as K, KeyEvent, KeyEventKind, KeyModifiers as M,
};
let with = |code, modifiers, kind| {
let mut event = KeyEvent::new(code, modifiers);
event.kind = kind;
E::Key(event)
};
let release = with(K::Char('q'), M::NONE, KeyEventKind::Release);
assert_eq!(from_crossterm(release), Some(Event::KeyUp(Key::char('q'))));
let repeat = with(K::Down, M::NONE, KeyEventKind::Repeat);
assert_eq!(
from_crossterm(repeat),
Some(Event::Key(Key::new(KeyCode::Down)))
);
let up = with(K::Tab, M::NONE, KeyEventKind::Release);
let Some(Event::KeyUp(tab)) = from_crossterm_kitty(up) else {
panic!("a release");
};
assert!(tab.is_exact());
}
#[test]
fn kitty_keys_from_crossterm_are_exact() {
use crossterm::event::{Event as E, KeyCode as K, KeyEvent, KeyModifiers as M};
let read =
|code, modifiers| match from_crossterm_kitty(E::Key(KeyEvent::new(code, modifiers))) {
Some(Event::Key(key)) => Some(key),
_ => None,
};
let ctrl_i = read(K::Char('i'), M::CONTROL).unwrap();
assert!(ctrl_i.is_exact() && ctrl_i.matches(&key("ctrl+i")));
let tab = read(K::Tab, M::NONE).unwrap();
assert!(tab.matches(&key("tab")) && !tab.matches(&key("ctrl+i")));
assert_eq!(
read(K::Char('a'), M::CONTROL | M::SHIFT),
Key::parse("ctrl+shift+a")
);
assert_eq!(read(K::Char('a'), M::SUPER), None);
let Some(Event::Key(legacy_tab)) = from_crossterm(E::Key(KeyEvent::new(K::Tab, M::NONE)))
else {
panic!("a key");
};
assert!(!legacy_tab.is_exact() && legacy_tab.matches(&key("ctrl+i")));
}
}