use std::fmt::Write;
use anyhow::Result;
use crate::{
key::{Key, KeyCode, KeyMods},
mouse::{MouseButton, MouseEvent, MouseEventKind},
};
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,
}
#[allow(clippy::derivable_impls)]
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();
let mods = key.mods();
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(KeyMods::SHIFT) =>
{
mods.difference(KeyMods::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(KeyMods::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(KeyMods::CONTROL) => {
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
Char(c) if mods.contains(KeyMods::CONTROL) && ctrl_mapping(c).is_some() => {
let c = ctrl_mapping(c).unwrap();
if mods.contains(KeyMods::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
}
Char(c)
if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
&& mods.contains(KeyMods::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(KeyMods::SHIFT) || mods.contains(KeyMods::CONTROL) {
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
} else {
if mods.contains(KeyMods::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
if modes.newline_mode && code == Enter {
buf.push(0x0a as char);
}
}
}
Tab => {
if mods.contains(KeyMods::ALT) {
buf.push(0x1b as char);
}
let mods = mods & !KeyMods::ALT;
if mods == KeyMods::CONTROL {
buf.push_str("\x1b[9;5u");
} else if mods == KeyMods::CONTROL | KeyMods::SHIFT {
buf.push_str("\x1b[1;5Z");
} else if mods == KeyMods::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(KeyMods::ALT)
|| mods.contains(KeyMods::SHIFT)
|| mods.contains(KeyMods::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(KeyMods::ALT)
|| mods.contains(KeyMods::SHIFT)
|| mods.contains(KeyMods::CONTROL)
{
write!(buf, "\x1b[{};{}~", c, 1 + encode_modifiers(mods))?;
} else {
write!(buf, "\x1b[{}~", c)?;
}
}
F(n) => {
if mods.is_empty() && n < 5 {
let s = match n {
1 => "\x1bOP",
2 => "\x1bOQ",
3 => "\x1bOR",
4 => "\x1bOS",
_ => unreachable!("wat?"),
};
write!(buf, "{}", s)?;
} 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)?;
}
}
}
Null => (),
CapsLock => (),
ScrollLock => (),
NumLock => (),
PrintScreen => (),
Pause => (),
Menu => (),
KeypadBegin => (),
Media(_) => (),
Modifier(_) => (),
};
Ok(buf)
}
fn encode_modifiers(mods: KeyMods) -> u8 {
let mut number = 0;
if mods.contains(KeyMods::SHIFT) {
number |= 1;
}
if mods.contains(KeyMods::ALT) {
number |= 2;
}
if mods.contains(KeyMods::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: KeyMods,
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(KeyMods::CONTROL) && ctrl_mapping(c).is_some() {
ctrl_mapping(c).unwrap()
} else {
c
};
if mods.contains(KeyMods::ALT) {
buf.push(0x1b as char);
}
write!(buf, "{}", c)?;
}
Ok(())
}
pub fn normalize_shift_to_upper_case(
code: KeyCode,
modifiers: &KeyMods,
) -> KeyCode {
if modifiers.contains(KeyMods::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(KeyMods::SHIFT) {
control_key_state |= windows::Win32::System::Console::SHIFT_PRESSED;
}
if key.mods().contains(KeyMods::ALT) {
control_key_state |= windows::Win32::System::Console::LEFT_ALT_PRESSED;
}
if key.mods().contains(KeyMods::CONTROL) {
control_key_state |= windows::Win32::System::Console::LEFT_CTRL_PRESSED;
}
let vkey = virtual_key_code(&key.code())?;
let uni = match key.code() {
KeyCode::Char(c) => {
let c = match c {
'\x7f' => '\x00',
'\x08' => {
if key.mods().contains(KeyMods::CONTROL) {
if key.mods().contains(KeyMods::ALT)
|| key.mods().contains(KeyMods::SHIFT)
{
'\x00'
} else {
'\x7f'
}
} else {
'\x08'
}
}
_ => c,
};
let c = if key.mods().contains(KeyMods::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::Delete => 0x7f,
KeyCode::Insert => 0,
KeyCode::F(_) => 0,
KeyCode::Null => 0,
KeyCode::Esc => 0,
KeyCode::CapsLock => 0,
KeyCode::ScrollLock => 0,
KeyCode::NumLock => 0,
KeyCode::PrintScreen => 0,
KeyCode::Pause => 0,
KeyCode::Menu => 0,
KeyCode::KeypadBegin => 0,
KeyCode::Media(_) => 0,
KeyCode::Modifier(_) => 0,
};
let scan_code = 0;
let key_down = 1;
let repeat_count = 1;
Some(format!(
"\u{1b}[{};{};{};{};{};{}_",
vkey.0, scan_code, uni, key_down, control_key_state, repeat_count
))
}
#[cfg(windows)]
fn virtual_key_code(
code: &KeyCode,
) -> Option<windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY> {
use windows::Win32::UI::Input::KeyboardAndMouse::*;
let code = match code {
KeyCode::Char(c) => match c {
'0'..='9' => VIRTUAL_KEY(*c as u16),
'a'..='z' => VIRTUAL_KEY(c.to_ascii_uppercase() as u16),
' ' => VK_SPACE,
'*' => VK_MULTIPLY,
'+' => VK_ADD,
',' => VK_SEPARATOR,
'-' => VK_SUBTRACT,
'.' => VK_DECIMAL,
'/' => VK_DIVIDE,
_ => return None,
},
KeyCode::Backspace => VK_BACK,
KeyCode::Enter => VK_RETURN,
KeyCode::Left => VK_LEFT,
KeyCode::Right => VK_RIGHT,
KeyCode::Up => VK_UP,
KeyCode::Down => VK_DOWN,
KeyCode::Home => VK_HOME,
KeyCode::End => VK_END,
KeyCode::PageUp => VK_PRIOR,
KeyCode::PageDown => VK_NEXT,
KeyCode::Tab => VK_TAB,
KeyCode::Delete => VK_DELETE,
KeyCode::Insert => VK_INSERT,
KeyCode::F(n) => match n {
1..=24 => VIRTUAL_KEY(VK_F1.0 - 1 + *n as u16),
_ => return None,
},
KeyCode::Null => VIRTUAL_KEY(0),
KeyCode::Esc => VK_ESCAPE,
KeyCode::CapsLock => VIRTUAL_KEY(0),
KeyCode::ScrollLock => VIRTUAL_KEY(0),
KeyCode::NumLock => VIRTUAL_KEY(0),
KeyCode::PrintScreen => VIRTUAL_KEY(0),
KeyCode::Pause => VIRTUAL_KEY(0),
KeyCode::Menu => VIRTUAL_KEY(0),
KeyCode::KeypadBegin => VIRTUAL_KEY(0),
KeyCode::Media(_) => VIRTUAL_KEY(0),
KeyCode::Modifier(_) => VIRTUAL_KEY(0),
};
Some(code)
}
pub fn print_key(key: &Key) -> String {
let mut buf = String::new();
if key.mods().contains(KeyMods::CONTROL) {
buf.push_str("C-");
}
if key.mods().contains(KeyMods::SHIFT) {
buf.push_str("S-");
}
if key.mods().contains(KeyMods::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::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"),
KeyCode::CapsLock => todo!(),
KeyCode::ScrollLock => todo!(),
KeyCode::NumLock => todo!(),
KeyCode::PrintScreen => todo!(),
KeyCode::Pause => todo!(),
KeyCode::Menu => todo!(),
KeyCode::KeypadBegin => todo!(),
KeyCode::Media(_) => todo!(),
KeyCode::Modifier(_) => todo!(),
}
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 => buf.push_str("35"),
MouseEventKind::ScrollUp => buf.push_str("64"),
MouseEventKind::ScrollDown => buf.push_str("65"),
MouseEventKind::ScrollLeft => buf.push_str("66"),
MouseEventKind::ScrollRight => buf.push_str("67"),
}
buf.push(';');
buf.push_str((mev.x + 1).to_string().as_str());
buf.push(';');
buf.push_str((mev.y + 1).to_string().as_str());
buf.push(match mev.kind {
MouseEventKind::Down(_) => 'M',
MouseEventKind::Up(_) => 'm',
MouseEventKind::Drag(_) => 'M',
MouseEventKind::Moved => 'M',
MouseEventKind::ScrollDown => 'M',
MouseEventKind::ScrollUp => 'M',
MouseEventKind::ScrollLeft => 'M',
MouseEventKind::ScrollRight => 'M',
});
buf
}