use std::fmt::Write;
use anyhow::Result;
use crossterm::event::{
KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crate::key::Key;
pub const CSI: &str = "\x1b[";
pub const SS3: &str = "\x1bO";
#[derive(Debug, Clone, Copy)]
pub struct KeyCodeEncodeModes {
pub enable_csi_u_key_encoding: bool,
pub application_cursor_keys: bool,
pub newline_mode: bool,
}
impl Default for KeyCodeEncodeModes {
fn default() -> Self {
KeyCodeEncodeModes {
enable_csi_u_key_encoding: false,
application_cursor_keys: false,
newline_mode: false,
}
}
}
pub fn encode_key(key: &Key, modes: KeyCodeEncodeModes) -> Result<String> {
use KeyCode::*;
#[cfg(windows)]
if let Some(encoded) = encode_key_win32(key, modes) {
return Ok(encoded);
}
let code = key.code().clone();
let mods = key.mods().clone();
let mut buf = String::new();
let code = normalize_shift_to_upper_case(code, &mods);
let mods = match code {
Char(c)
if (c.is_ascii_punctuation() || c.is_ascii_uppercase())
&& mods.contains(KeyModifiers::SHIFT) =>
{
mods.clone().difference(KeyModifiers::SHIFT)
}
_ => mods,
};
let code = match code {
Char('\x7f') => KeyCode::Backspace,
Char('\x08') => KeyCode::Delete,
c => c,
};
match code {
Char(c)
if is_ambiguous_ascii_ctrl(c)
&& mods.contains(KeyModifiers::CONTROL)
&& modes.enable_csi_u_key_encoding =>
{
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
Char(c)
if c.is_ascii_uppercase() && mods.contains(KeyModifiers::CONTROL) =>
{
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
Char(c)
if mods.contains(KeyModifiers::CONTROL) && ctrl_mapping(c).is_some() =>
{
let c = ctrl_mapping(c).unwrap();
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
}
Char(c)
if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
&& mods.contains(KeyModifiers::ALT) =>
{
buf.push(0x1b as char);
buf.push(c);
}
Enter | Esc | Backspace => {
let c = match code {
Enter => '\r',
Esc => '\x1b',
Backspace => '\x7f',
_ => unreachable!(),
};
if mods.contains(KeyModifiers::SHIFT)
|| mods.contains(KeyModifiers::CONTROL)
{
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
} else {
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
if modes.newline_mode && code == Enter {
buf.push(0x0a as char);
}
}
}
Tab => {
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
let mods = mods & !KeyModifiers::ALT;
if mods == KeyModifiers::CONTROL {
buf.push_str("\x1b[9;5u");
} else if mods == KeyModifiers::CONTROL | KeyModifiers::SHIFT {
buf.push_str("\x1b[1;5Z");
} else if mods == KeyModifiers::SHIFT {
buf.push_str("\x1b[Z");
} else {
buf.push('\t');
}
}
Char(c) => {
if mods.is_empty() {
buf.push(c);
} else {
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
}
Home | End | Up | Down | Right | Left => {
let (force_app, c) = match code {
Up => (false, 'A'),
Down => (false, 'B'),
Right => (false, 'C'),
Left => (false, 'D'),
Home => (false, 'H'),
End => (false, 'F'),
_ => unreachable!(),
};
let csi_or_ss3 = if force_app
|| (
modes.application_cursor_keys
) {
SS3
} else {
CSI
};
if mods.contains(KeyModifiers::ALT)
|| mods.contains(KeyModifiers::SHIFT)
|| mods.contains(KeyModifiers::CONTROL)
{
write!(buf, "{}1;{}{}", CSI, 1 + encode_modifiers(mods), c)?;
} else {
write!(buf, "{}{}", csi_or_ss3, c)?;
}
}
PageUp | PageDown | Insert | Delete => {
let c = match code {
Insert => 2,
Delete => 3,
PageUp => 5,
PageDown => 6,
_ => unreachable!(),
};
if mods.contains(KeyModifiers::ALT)
|| mods.contains(KeyModifiers::SHIFT)
|| mods.contains(KeyModifiers::CONTROL)
{
write!(buf, "\x1b[{};{}~", c, 1 + encode_modifiers(mods))?;
} else {
write!(buf, "\x1b[{}~", c)?;
}
}
F(n) => {
if mods.is_empty() && n < 5 {
write!(
buf,
"{}",
match n {
1 => "\x1bOP",
2 => "\x1bOQ",
3 => "\x1bOR",
4 => "\x1bOS",
_ => unreachable!("wat?"),
}
)?;
} else {
let intro = match n {
1 => "\x1b[11",
2 => "\x1b[12",
3 => "\x1b[13",
4 => "\x1b[14",
5 => "\x1b[15",
6 => "\x1b[17",
7 => "\x1b[18",
8 => "\x1b[19",
9 => "\x1b[20",
10 => "\x1b[21",
11 => "\x1b[23",
12 => "\x1b[24",
_ => panic!("unhandled fkey number {}", n),
};
let encoded_mods = encode_modifiers(mods);
if encoded_mods == 0 {
write!(buf, "{}~", intro)?;
} else {
write!(buf, "{};{}~", intro, 1 + encoded_mods)?;
}
}
}
BackTab | Null => todo!(),
};
Ok(buf)
}
fn encode_modifiers(mods: KeyModifiers) -> u8 {
let mut number = 0;
if mods.contains(KeyModifiers::SHIFT) {
number |= 1;
}
if mods.contains(KeyModifiers::ALT) {
number |= 2;
}
if mods.contains(KeyModifiers::CONTROL) {
number |= 4;
}
number
}
fn is_ambiguous_ascii_ctrl(c: char) -> bool {
match c {
'i' | 'I' | 'm' | 'M' | '[' | '{' | '@' => true,
_ => false,
}
}
fn ctrl_mapping(c: char) -> Option<char> {
Some(match c {
'@' | '`' | ' ' | '2' => '\x00',
'A' | 'a' => '\x01',
'B' | 'b' => '\x02',
'C' | 'c' => '\x03',
'D' | 'd' => '\x04',
'E' | 'e' => '\x05',
'F' | 'f' => '\x06',
'G' | 'g' => '\x07',
'H' | 'h' => '\x08',
'I' | 'i' => '\x09',
'J' | 'j' => '\x0a',
'K' | 'k' => '\x0b',
'L' | 'l' => '\x0c',
'M' | 'm' => '\x0d',
'N' | 'n' => '\x0e',
'O' | 'o' => '\x0f',
'P' | 'p' => '\x10',
'Q' | 'q' => '\x11',
'R' | 'r' => '\x12',
'S' | 's' => '\x13',
'T' | 't' => '\x14',
'U' | 'u' => '\x15',
'V' | 'v' => '\x16',
'W' | 'w' => '\x17',
'X' | 'x' => '\x18',
'Y' | 'y' => '\x19',
'Z' | 'z' => '\x1a',
'[' | '3' | '{' => '\x1b',
'\\' | '4' | '|' => '\x1c',
']' | '5' | '}' => '\x1d',
'^' | '6' | '~' => '\x1e',
'_' | '7' | '/' => '\x1f',
'8' | '?' => '\x7f', _ => return None,
})
}
fn csi_u_encode(
buf: &mut String,
c: char,
mods: KeyModifiers,
enable_csi_u_key_encoding: bool,
) -> Result<()> {
if enable_csi_u_key_encoding {
write!(buf, "\x1b[{};{}u", c as u32, 1 + encode_modifiers(mods))?;
} else {
let c = if mods.contains(KeyModifiers::CONTROL) && ctrl_mapping(c).is_some()
{
ctrl_mapping(c).unwrap()
} else {
c
};
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "{}", c)?;
}
Ok(())
}
pub fn normalize_shift_to_upper_case(
code: KeyCode,
modifiers: &KeyModifiers,
) -> KeyCode {
if modifiers.contains(KeyModifiers::SHIFT) {
match code {
KeyCode::Char(c) if c.is_ascii_lowercase() => KeyCode::Char(c),
_ => code,
}
} else {
code
}
}
#[cfg(windows)]
pub fn encode_key_win32(
key: &Key,
_modes: KeyCodeEncodeModes,
) -> Option<String> {
let mut control_key_state = 0;
if key.mods().contains(KeyModifiers::SHIFT) {
control_key_state |= winapi::um::wincon::SHIFT_PRESSED;
}
if key.mods().contains(KeyModifiers::ALT) {
control_key_state |= winapi::um::wincon::LEFT_ALT_PRESSED;
}
if key.mods().contains(KeyModifiers::CONTROL) {
control_key_state |= winapi::um::wincon::LEFT_CTRL_PRESSED;
}
let vkey = virtual_key_code(key.code())?;
let uni = match key.code() {
KeyCode::Char(c) => {
let c = *c;
let c = match c {
'\x7f' => '\x00',
'\x08' => {
if key.mods().contains(KeyModifiers::CONTROL) {
if key.mods().contains(KeyModifiers::ALT)
|| key.mods().contains(KeyModifiers::SHIFT)
{
'\x00'
} else {
'\x7f'
}
} else {
'\x08'
}
}
_ => c,
};
let c = if key.mods().contains(KeyModifiers::CONTROL) {
ctrl_mapping(c).unwrap_or(c)
} else {
c
};
c as u32
}
KeyCode::Backspace => 0x8,
KeyCode::Enter => 0xd,
KeyCode::Left => 0,
KeyCode::Right => 0,
KeyCode::Up => 0,
KeyCode::Down => 0,
KeyCode::Home => 0,
KeyCode::End => 0,
KeyCode::PageUp => 0,
KeyCode::PageDown => 0,
KeyCode::Tab => 0x9,
KeyCode::BackTab => 0,
KeyCode::Delete => 0x7f,
KeyCode::Insert => 0,
KeyCode::F(_) => 0,
KeyCode::Null => 0,
KeyCode::Esc => 0,
};
let scan_code = 0;
let key_down = 1;
let repeat_count = 1;
Some(format!(
"\u{1b}[{};{};{};{};{};{}_",
vkey, scan_code, uni, key_down, control_key_state, repeat_count
))
}
#[cfg(windows)]
fn virtual_key_code(code: &KeyCode) -> Option<i32> {
let code = match code {
KeyCode::Char(c) => match c {
'0'..='9' => *c as i32,
'a'..='z' => c.to_ascii_uppercase() as i32,
' ' => winapi::um::winuser::VK_SPACE,
'*' => winapi::um::winuser::VK_MULTIPLY,
'+' => winapi::um::winuser::VK_ADD,
',' => winapi::um::winuser::VK_SEPARATOR,
'-' => winapi::um::winuser::VK_SUBTRACT,
'.' => winapi::um::winuser::VK_DECIMAL,
'/' => winapi::um::winuser::VK_DIVIDE,
_ => return None,
},
KeyCode::Backspace => winapi::um::winuser::VK_BACK,
KeyCode::Enter => winapi::um::winuser::VK_RETURN,
KeyCode::Left => winapi::um::winuser::VK_LEFT,
KeyCode::Right => winapi::um::winuser::VK_RIGHT,
KeyCode::Up => winapi::um::winuser::VK_UP,
KeyCode::Down => winapi::um::winuser::VK_DOWN,
KeyCode::Home => winapi::um::winuser::VK_HOME,
KeyCode::End => winapi::um::winuser::VK_END,
KeyCode::PageUp => winapi::um::winuser::VK_PRIOR,
KeyCode::PageDown => winapi::um::winuser::VK_NEXT,
KeyCode::Tab => winapi::um::winuser::VK_TAB,
KeyCode::BackTab => return None,
KeyCode::Delete => winapi::um::winuser::VK_DELETE,
KeyCode::Insert => winapi::um::winuser::VK_INSERT,
KeyCode::F(n) => match n {
1..=24 => winapi::um::winuser::VK_F1 - 1 + *n as i32,
_ => return None,
},
KeyCode::Null => 0,
KeyCode::Esc => winapi::um::winuser::VK_ESCAPE,
};
Some(code)
}
pub fn print_key(key: &Key) -> String {
let mut buf = String::new();
if key.mods().contains(KeyModifiers::CONTROL) {
buf.push_str("C-");
}
if key.mods().contains(KeyModifiers::SHIFT) {
buf.push_str("S-");
}
if key.mods().contains(KeyModifiers::ALT) {
buf.push_str("M-");
}
match key.code() {
KeyCode::Backspace => buf.push_str("Backspace"),
KeyCode::Enter => buf.push_str("Enter"),
KeyCode::Left => buf.push_str("Left"),
KeyCode::Right => buf.push_str("Right"),
KeyCode::Up => buf.push_str("Up"),
KeyCode::Down => buf.push_str("Down"),
KeyCode::Home => buf.push_str("Home"),
KeyCode::End => buf.push_str("End"),
KeyCode::PageUp => buf.push_str("PgUp"),
KeyCode::PageDown => buf.push_str("PgDn"),
KeyCode::Tab => buf.push_str("Tab"),
KeyCode::BackTab => buf.push_str("BackTab"),
KeyCode::Delete => buf.push_str("Del"),
KeyCode::Insert => buf.push_str("Ins"),
KeyCode::F(n) => buf.push_str(&format!("F{}", n)),
KeyCode::Char(ch) => buf.push(*ch),
KeyCode::Null => buf.push_str("Null"),
KeyCode::Esc => buf.push_str("Esc"),
}
return buf;
}
pub fn encode_mouse_event(mev: MouseEvent) -> String {
let mut buf = String::new();
buf.push_str("\x1b[<");
match mev.kind {
MouseEventKind::Down(btn) | MouseEventKind::Up(btn) => match btn {
MouseButton::Left => buf.push('0'),
MouseButton::Right => buf.push('1'),
MouseButton::Middle => buf.push('2'),
},
MouseEventKind::Drag(btn) => match btn {
MouseButton::Left => buf.push_str("32"),
MouseButton::Right => buf.push_str("33"),
MouseButton::Middle => buf.push_str("34"),
},
MouseEventKind::Moved => {
return "".to_string();
}
MouseEventKind::ScrollDown => buf.push_str("65"),
MouseEventKind::ScrollUp => buf.push_str("64"),
}
buf.push(';');
buf.push_str((mev.column + 1).to_string().as_str());
buf.push(';');
buf.push_str((mev.row + 1).to_string().as_str());
buf.push(match mev.kind {
MouseEventKind::Down(_) => 'M',
MouseEventKind::Up(_) => 'm',
MouseEventKind::Drag(_) => 'M',
MouseEventKind::Moved => todo!(),
MouseEventKind::ScrollDown => 'M',
MouseEventKind::ScrollUp => 'M',
});
buf
}