#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
Char(char),
Enter,
Esc,
Tab,
BackTab,
Backspace,
Delete,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
F(u8),
Ctrl(char),
Alt(char),
}
impl Key {
#[must_use]
#[track_caller]
pub fn encode(self) -> Vec<u8> {
match self {
Key::Char(c) => {
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf).as_bytes().to_vec()
}
Key::Enter => vec![b'\r'],
Key::Esc => vec![0x1b],
Key::Tab => vec![b'\t'],
Key::BackTab => b"\x1b[Z".to_vec(),
Key::Backspace => vec![0x7f],
Key::Delete => b"\x1b[3~".to_vec(),
Key::Up => b"\x1b[A".to_vec(),
Key::Down => b"\x1b[B".to_vec(),
Key::Right => b"\x1b[C".to_vec(),
Key::Left => b"\x1b[D".to_vec(),
Key::Home => b"\x1b[H".to_vec(),
Key::End => b"\x1b[F".to_vec(),
Key::PageUp => b"\x1b[5~".to_vec(),
Key::PageDown => b"\x1b[6~".to_vec(),
Key::F(n) => match n {
1 => b"\x1bOP".to_vec(),
2 => b"\x1bOQ".to_vec(),
3 => b"\x1bOR".to_vec(),
4 => b"\x1bOS".to_vec(),
5 => b"\x1b[15~".to_vec(),
6 => b"\x1b[17~".to_vec(),
7 => b"\x1b[18~".to_vec(),
8 => b"\x1b[19~".to_vec(),
9 => b"\x1b[20~".to_vec(),
10 => b"\x1b[21~".to_vec(),
11 => b"\x1b[23~".to_vec(),
12 => b"\x1b[24~".to_vec(),
_ => panic!("Key::F({n}): only F1-F12 exist"),
},
Key::Ctrl(c) => vec![ctrl_byte(c)],
Key::Alt(c) => {
let mut out = vec![0x1b];
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
out
}
}
}
}
#[track_caller]
fn ctrl_byte(c: char) -> u8 {
match c {
'a'..='z' => (c as u8) & 0x1f,
'A'..='Z' | '@' | '[' | '\\' | ']' | '^' | '_' => (c as u8) & 0x1f,
' ' => 0x00,
'?' => 0x7f,
_ => panic!("Key::Ctrl({c:?}): no control-code mapping for this character"),
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for super::Key {}
impl Sealed for super::Chord {}
}
pub trait Input: sealed::Sealed {
#[doc(hidden)]
fn encode_modal(&self, application_cursor: bool) -> Vec<u8>;
}
impl Input for Key {
fn encode_modal(&self, application_cursor: bool) -> Vec<u8> {
if application_cursor {
let ss3 = match self {
Key::Up => Some(b'A'),
Key::Down => Some(b'B'),
Key::Right => Some(b'C'),
Key::Left => Some(b'D'),
Key::Home => Some(b'H'),
Key::End => Some(b'F'),
_ => None,
};
if let Some(final_byte) = ss3 {
return vec![0x1b, b'O', final_byte];
}
}
self.encode()
}
}
impl Input for Chord {
fn encode_modal(&self, _application_cursor: bool) -> Vec<u8> {
self.encode()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Chord {
key: Key,
ctrl: bool,
alt: bool,
shift: bool,
}
impl Key {
#[must_use]
#[track_caller]
pub fn ctrl(self) -> Chord {
Chord::over(self).ctrl()
}
#[must_use]
#[track_caller]
pub fn alt(self) -> Chord {
Chord::over(self).alt()
}
#[must_use]
#[track_caller]
pub fn shift(self) -> Chord {
Chord::over(self).shift()
}
}
impl Chord {
#[track_caller]
fn over(key: Key) -> Self {
assert!(
chord_base(key).is_some(),
"{key:?} has no CSI-modifier chord form; for characters use \
Key::Ctrl(c) / Key::Alt(c)"
);
Self {
key,
ctrl: false,
alt: false,
shift: false,
}
}
#[must_use]
pub fn ctrl(mut self) -> Self {
self.ctrl = true;
self
}
#[must_use]
pub fn alt(mut self) -> Self {
self.alt = true;
self
}
#[must_use]
pub fn shift(mut self) -> Self {
self.shift = true;
self
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let base = chord_base(self.key).expect("validated at construction");
let modifier = 1 + u8::from(self.shift) + 2 * u8::from(self.alt) + 4 * u8::from(self.ctrl);
match base {
ChordBase::Letter(letter) => format!("\x1b[1;{modifier}{letter}").into_bytes(),
ChordBase::Tilde(number) => format!("\x1b[{number};{modifier}~").into_bytes(),
}
}
}
enum ChordBase {
Letter(char),
Tilde(u8),
}
fn chord_base(key: Key) -> Option<ChordBase> {
Some(match key {
Key::Up => ChordBase::Letter('A'),
Key::Down => ChordBase::Letter('B'),
Key::Right => ChordBase::Letter('C'),
Key::Left => ChordBase::Letter('D'),
Key::Home => ChordBase::Letter('H'),
Key::End => ChordBase::Letter('F'),
Key::F(n @ 1..=4) => ChordBase::Letter(['P', 'Q', 'R', 'S'][usize::from(n) - 1]),
Key::Delete => ChordBase::Tilde(3),
Key::PageUp => ChordBase::Tilde(5),
Key::PageDown => ChordBase::Tilde(6),
Key::F(5) => ChordBase::Tilde(15),
Key::F(n @ 6..=10) => ChordBase::Tilde(11 + n), Key::F(11) => ChordBase::Tilde(23),
Key::F(12) => ChordBase::Tilde(24),
_ => return None,
})
}
pub(crate) fn mouse_sgr(button: u8, col: u16, row: u16, press: bool) -> Vec<u8> {
let suffix = if press { 'M' } else { 'm' };
format!("\x1b[<{button};{};{}{suffix}", col + 1, row + 1).into_bytes()
}
pub(crate) fn mouse_legacy(button: u8, col: u16, row: u16) -> Vec<u8> {
let mut out = b"\x1b[M".to_vec();
out.push(32 + button);
out.push(32 + 1 + u8::try_from(col).expect("caller validated"));
out.push(32 + 1 + u8::try_from(row).expect("caller validated"));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xterm_encodings() {
let table: &[(Key, &[u8])] = &[
(Key::Char('j'), b"j"),
(Key::Char('é'), "é".as_bytes()),
(Key::Enter, b"\r"),
(Key::Esc, b"\x1b"),
(Key::Tab, b"\t"),
(Key::BackTab, b"\x1b[Z"),
(Key::Backspace, b"\x7f"),
(Key::Delete, b"\x1b[3~"),
(Key::Up, b"\x1b[A"),
(Key::Down, b"\x1b[B"),
(Key::Right, b"\x1b[C"),
(Key::Left, b"\x1b[D"),
(Key::Home, b"\x1b[H"),
(Key::End, b"\x1b[F"),
(Key::PageUp, b"\x1b[5~"),
(Key::PageDown, b"\x1b[6~"),
(Key::F(1), b"\x1bOP"),
(Key::F(4), b"\x1bOS"),
(Key::F(5), b"\x1b[15~"),
(Key::F(10), b"\x1b[21~"),
(Key::F(12), b"\x1b[24~"),
(Key::Ctrl('c'), b"\x03"),
(Key::Ctrl('C'), b"\x03"),
(Key::Ctrl('['), b"\x1b"),
(Key::Ctrl(' '), b"\x00"),
(Key::Ctrl('?'), b"\x7f"),
(Key::Alt('x'), b"\x1bx"),
];
for (key, bytes) in table {
assert_eq!(key.encode(), *bytes, "wrong encoding for {key:?}");
}
}
#[test]
fn cursor_keys_switch_to_ss3_under_application_cursor_mode() {
assert_eq!(Key::Up.encode_modal(true), b"\x1bOA");
assert_eq!(Key::End.encode_modal(true), b"\x1bOF");
assert_eq!(Key::Up.encode_modal(false), b"\x1b[A");
assert_eq!(Key::F(5).encode_modal(true), Key::F(5).encode());
assert_eq!(Key::Delete.encode_modal(true), Key::Delete.encode());
assert_eq!(Key::Up.ctrl().encode_modal(true), b"\x1b[1;5A");
}
#[test]
fn chord_encodings() {
let table: &[(Chord, &[u8])] = &[
(Key::Right.ctrl(), b"\x1b[1;5C"),
(Key::Up.shift(), b"\x1b[1;2A"),
(Key::Left.alt(), b"\x1b[1;3D"),
(Key::Home.ctrl(), b"\x1b[1;5H"),
(Key::End.ctrl().shift(), b"\x1b[1;6F"),
(Key::PageDown.alt(), b"\x1b[6;3~"),
(Key::Delete.ctrl(), b"\x1b[3;5~"),
(Key::F(1).ctrl(), b"\x1b[1;5P"),
(Key::F(5).ctrl().shift(), b"\x1b[15;6~"),
(Key::F(10).alt(), b"\x1b[21;3~"),
(Key::F(12).ctrl().alt().shift(), b"\x1b[24;8~"),
];
for (chord, bytes) in table {
assert_eq!(chord.encode(), *bytes, "wrong encoding for {chord:?}");
}
}
#[test]
#[should_panic(expected = "no CSI-modifier chord form")]
fn char_chords_panic_toward_key_ctrl() {
let _ = Key::Char('a').ctrl();
}
#[test]
fn mouse_encodings() {
assert_eq!(mouse_sgr(0, 9, 6, true), b"\x1b[<0;10;7M");
assert_eq!(mouse_sgr(0, 9, 6, false), b"\x1b[<0;10;7m");
assert_eq!(mouse_sgr(64, 0, 0, true), b"\x1b[<64;1;1M");
assert_eq!(mouse_legacy(0, 0, 0), b"\x1b[M\x20\x21\x21");
assert_eq!(mouse_legacy(3, 9, 6,), b"\x1b[M\x23\x2a\x27");
}
#[test]
#[should_panic(expected = "only F1-F12")]
fn f13_panics() {
let _ = Key::F(13).encode();
}
#[test]
#[should_panic(expected = "no control-code mapping")]
fn ctrl_digit_panics() {
let _ = Key::Ctrl('1').encode();
}
}