use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use vt100::{MouseProtocolEncoding, MouseProtocolMode};
const ESC: u8 = 0x1B;
const CSI: &[u8] = b"\x1b[";
const SS3: &[u8] = b"\x1bO";
const CARRIAGE_RETURN: u8 = b'\r';
const BACKSPACE_BYTE: u8 = 0x7F;
const TAB_BYTE: u8 = b'\t';
const CONTROL_MASK: u8 = 0x1F;
const TILDE: u8 = b'~';
const SS3_F1: u8 = b'P';
const FIRST_FUNCTION_KEY: u8 = 1;
const SS3_FUNCTION_KEYS: u8 = 4;
const CSI_FUNCTION_PARAMS: [u8; 8] = [15, 17, 18, 19, 20, 21, 23, 24];
const NUL: u8 = 0x00;
const CTRL_QUESTION: u8 = 0x7F;
const CSI_KEY_PARAM: u8 = b'1';
const PARAM_SEPARATOR: u8 = b';';
const NO_MODIFIER: u8 = 1;
const LEFT_BUTTON_CODE: u8 = 0;
const MIDDLE_BUTTON_CODE: u8 = 1;
const RIGHT_BUTTON_CODE: u8 = 2;
const RELEASE_BUTTON_CODE: u8 = 3;
const MOTION_BIT: u8 = 32;
const NO_BUTTON_MOTION_CODE: u8 = RELEASE_BUTTON_CODE | MOTION_BIT;
const SCROLL_UP_CODE: u8 = 64;
const SCROLL_DOWN_CODE: u8 = 65;
const SCROLL_LEFT_CODE: u8 = 66;
const SCROLL_RIGHT_CODE: u8 = 67;
const SHIFT_MOUSE_MODIFIER: u8 = 4;
const ALT_MOUSE_MODIFIER: u8 = 8;
const CONTROL_MOUSE_MODIFIER: u8 = 16;
const LEGACY_MOUSE_OFFSET: u16 = 32;
const MAX_LEGACY_MOUSE_COORDINATE: u16 = 223;
const SGR_MOUSE_PREFIX: u8 = b'<';
const SGR_MOUSE_PRESS: u8 = b'M';
const SGR_MOUSE_RELEASE: u8 = b'm';
const CURSOR_UP_CSI: &[u8] = b"\x1b[A";
const CURSOR_DOWN_CSI: &[u8] = b"\x1b[B";
const CURSOR_UP_SS3: &[u8] = b"\x1bOA";
const CURSOR_DOWN_SS3: &[u8] = b"\x1bOB";
pub fn wheel_arrow(up: bool, application_cursor: bool) -> &'static [u8] {
match (up, application_cursor) {
(true, false) => CURSOR_UP_CSI,
(false, false) => CURSOR_DOWN_CSI,
(true, true) => CURSOR_UP_SS3,
(false, true) => CURSOR_DOWN_SS3,
}
}
pub fn encode_key(key: KeyEvent) -> Option<Vec<u8>> {
let modifiers = key.modifiers;
match key.code {
KeyCode::Char(c) => Some(encode_char(c, modifiers)),
KeyCode::Enter => Some(vec![CARRIAGE_RETURN]),
KeyCode::Backspace => Some(vec![BACKSPACE_BYTE]),
KeyCode::Tab => Some(vec![TAB_BYTE]),
KeyCode::BackTab => Some(csi_final(b'Z', KeyModifiers::NONE)),
KeyCode::Esc => Some(vec![ESC]),
KeyCode::Left => Some(csi_final(b'D', modifiers)),
KeyCode::Right => Some(csi_final(b'C', modifiers)),
KeyCode::Up => Some(csi_final(b'A', modifiers)),
KeyCode::Down => Some(csi_final(b'B', modifiers)),
KeyCode::Home => Some(csi_final(b'H', modifiers)),
KeyCode::End => Some(csi_final(b'F', modifiers)),
KeyCode::PageUp => Some(csi_tilde(5, modifiers)),
KeyCode::PageDown => Some(csi_tilde(6, modifiers)),
KeyCode::Insert => Some(csi_tilde(2, modifiers)),
KeyCode::Delete => Some(csi_tilde(3, modifiers)),
KeyCode::F(n) => encode_function_key(n, modifiers),
_ => None,
}
}
pub fn encode_mouse(
mouse: MouseEvent,
column: u16,
row: u16,
mode: MouseProtocolMode,
encoding: MouseProtocolEncoding,
) -> Option<Vec<u8>> {
let (button, release) = mouse_button_code(mouse.kind, mode)?;
let button = release_code(button, release, encoding)
.saturating_add(mouse_modifier_bits(mouse.modifiers));
let column = column.saturating_add(1);
let row = row.saturating_add(1);
match encoding {
MouseProtocolEncoding::Sgr => Some(sgr_mouse_report(button, column, row, release)),
MouseProtocolEncoding::Default => legacy_mouse_report(button, column, row),
MouseProtocolEncoding::Utf8 => utf8_mouse_report(button, column, row),
}
}
fn mouse_button_code(kind: MouseEventKind, mode: MouseProtocolMode) -> Option<(u8, bool)> {
match kind {
MouseEventKind::Down(button) if mode != MouseProtocolMode::None => {
Some((button_code(button)?, false))
},
MouseEventKind::Up(button) if reports_release(mode) => Some((button_code(button)?, true)),
MouseEventKind::Drag(button) if reports_motion(mode) => {
Some((button_code(button)?.saturating_add(MOTION_BIT), false))
},
MouseEventKind::Moved if mode == MouseProtocolMode::AnyMotion => {
Some((NO_BUTTON_MOTION_CODE, false))
},
MouseEventKind::ScrollUp if mode != MouseProtocolMode::None => {
Some((SCROLL_UP_CODE, false))
},
MouseEventKind::ScrollDown if mode != MouseProtocolMode::None => {
Some((SCROLL_DOWN_CODE, false))
},
MouseEventKind::ScrollLeft if mode != MouseProtocolMode::None => {
Some((SCROLL_LEFT_CODE, false))
},
MouseEventKind::ScrollRight if mode != MouseProtocolMode::None => {
Some((SCROLL_RIGHT_CODE, false))
},
_ => None,
}
}
fn release_code(button: u8, release: bool, encoding: MouseProtocolEncoding) -> u8 {
if release && encoding != MouseProtocolEncoding::Sgr {
RELEASE_BUTTON_CODE
} else {
button
}
}
fn button_code(button: MouseButton) -> Option<u8> {
match button {
MouseButton::Left => Some(LEFT_BUTTON_CODE),
MouseButton::Middle => Some(MIDDLE_BUTTON_CODE),
MouseButton::Right => Some(RIGHT_BUTTON_CODE),
}
}
fn reports_release(mode: MouseProtocolMode) -> bool {
matches!(
mode,
MouseProtocolMode::PressRelease
| MouseProtocolMode::ButtonMotion
| MouseProtocolMode::AnyMotion
)
}
fn reports_motion(mode: MouseProtocolMode) -> bool {
matches!(
mode,
MouseProtocolMode::ButtonMotion | MouseProtocolMode::AnyMotion
)
}
fn mouse_modifier_bits(modifiers: KeyModifiers) -> u8 {
let mut bits = 0;
if modifiers.contains(KeyModifiers::SHIFT) {
bits += SHIFT_MOUSE_MODIFIER;
}
if modifiers.contains(KeyModifiers::ALT) {
bits += ALT_MOUSE_MODIFIER;
}
if modifiers.contains(KeyModifiers::CONTROL) {
bits += CONTROL_MOUSE_MODIFIER;
}
bits
}
fn sgr_mouse_report(button: u8, column: u16, row: u16, release: bool) -> Vec<u8> {
let terminator = if release {
SGR_MOUSE_RELEASE
} else {
SGR_MOUSE_PRESS
};
let mut bytes = CSI.to_vec();
bytes.push(SGR_MOUSE_PREFIX);
bytes.extend(format!("{button};{column};{row}").bytes());
bytes.push(terminator);
bytes
}
fn legacy_mouse_report(button: u8, column: u16, row: u16) -> Option<Vec<u8>> {
if column > MAX_LEGACY_MOUSE_COORDINATE || row > MAX_LEGACY_MOUSE_COORDINATE {
return None;
}
Some(vec![
ESC,
b'[',
b'M',
button.saturating_add(LEGACY_MOUSE_OFFSET as u8),
(column + LEGACY_MOUSE_OFFSET) as u8,
(row + LEGACY_MOUSE_OFFSET) as u8,
])
}
fn utf8_mouse_report(button: u8, column: u16, row: u16) -> Option<Vec<u8>> {
let mut bytes = vec![ESC, b'[', b'M'];
for value in [
u16::from(button) + LEGACY_MOUSE_OFFSET,
column + LEGACY_MOUSE_OFFSET,
row + LEGACY_MOUSE_OFFSET,
] {
bytes.extend(char::from_u32(u32::from(value))?.to_string().bytes());
}
Some(bytes)
}
fn encode_char(c: char, modifiers: KeyModifiers) -> Vec<u8> {
let mut bytes = if modifiers.contains(KeyModifiers::CONTROL) {
control_byte(c).map_or_else(|| c.to_string().into_bytes(), |byte| vec![byte])
} else {
c.to_string().into_bytes()
};
if modifiers.contains(KeyModifiers::ALT) && !bytes.is_empty() {
bytes.insert(0, ESC);
}
bytes
}
fn control_byte(c: char) -> Option<u8> {
match c {
' ' | '@' => Some(NUL),
'?' => Some(CTRL_QUESTION),
'['..='_' | 'a'..='z' => Some(c as u8 & CONTROL_MASK),
'A'..='Z' => Some(c as u8 & CONTROL_MASK),
_ => None,
}
}
fn encode_function_key(n: u8, modifiers: KeyModifiers) -> Option<Vec<u8>> {
if (FIRST_FUNCTION_KEY..FIRST_FUNCTION_KEY + SS3_FUNCTION_KEYS).contains(&n) {
let final_byte = SS3_F1 + (n - FIRST_FUNCTION_KEY);
if modifier_code(modifiers) > NO_MODIFIER {
return Some(csi_final(final_byte, modifiers));
}
let mut bytes = SS3.to_vec();
bytes.push(final_byte);
return Some(bytes);
}
let index = n.checked_sub(FIRST_FUNCTION_KEY + SS3_FUNCTION_KEYS)?;
let param = *CSI_FUNCTION_PARAMS.get(usize::from(index))?;
Some(csi_tilde(param, modifiers))
}
fn csi_final(final_byte: u8, modifiers: KeyModifiers) -> Vec<u8> {
let code = modifier_code(modifiers);
let mut bytes = CSI.to_vec();
if code > NO_MODIFIER {
bytes.push(CSI_KEY_PARAM);
bytes.push(PARAM_SEPARATOR);
bytes.extend_from_slice(code.to_string().as_bytes());
}
bytes.push(final_byte);
bytes
}
fn csi_tilde(number: u8, modifiers: KeyModifiers) -> Vec<u8> {
let code = modifier_code(modifiers);
let mut bytes = CSI.to_vec();
bytes.extend_from_slice(number.to_string().as_bytes());
if code > NO_MODIFIER {
bytes.push(PARAM_SEPARATOR);
bytes.extend_from_slice(code.to_string().as_bytes());
}
bytes.push(TILDE);
bytes
}
fn modifier_code(modifiers: KeyModifiers) -> u8 {
let mut code = NO_MODIFIER;
if modifiers.contains(KeyModifiers::SHIFT) {
code += 1;
}
if modifiers.contains(KeyModifiers::ALT) {
code += 2;
}
if modifiers.contains(KeyModifiers::CONTROL) {
code += 4;
}
code
}
#[cfg(test)]
mod tests {
use super::*;
fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, modifiers)
}
#[test]
fn plain_char_is_utf8() {
assert_eq!(
encode_key(key(KeyCode::Char('a'), KeyModifiers::NONE)),
Some(b"a".to_vec())
);
}
#[test]
fn ctrl_letter_is_a_control_byte() {
assert_eq!(
encode_key(key(KeyCode::Char('c'), KeyModifiers::CONTROL)),
Some(vec![0x03])
);
}
#[test]
fn ctrl_non_letters_map_to_control_bytes() {
assert_eq!(
encode_key(key(KeyCode::Char(' '), KeyModifiers::CONTROL)),
Some(vec![0x00])
);
assert_eq!(
encode_key(key(KeyCode::Char('['), KeyModifiers::CONTROL)),
Some(vec![0x1B])
);
assert_eq!(
encode_key(key(KeyCode::Char('_'), KeyModifiers::CONTROL)),
Some(vec![0x1F])
);
assert_eq!(
encode_key(key(KeyCode::Char('?'), KeyModifiers::CONTROL)),
Some(vec![0x7F])
);
}
#[test]
fn enter_is_carriage_return() {
assert_eq!(
encode_key(key(KeyCode::Enter, KeyModifiers::NONE)),
Some(vec![b'\r'])
);
}
#[test]
fn up_arrow_is_a_csi_sequence() {
assert_eq!(
encode_key(key(KeyCode::Up, KeyModifiers::NONE)),
Some(b"\x1b[A".to_vec())
);
}
#[test]
fn ctrl_up_carries_a_modifier_parameter() {
assert_eq!(
encode_key(key(KeyCode::Up, KeyModifiers::CONTROL)),
Some(b"\x1b[1;5A".to_vec())
);
}
#[test]
fn page_up_is_a_tilde_sequence() {
assert_eq!(
encode_key(key(KeyCode::PageUp, KeyModifiers::NONE)),
Some(b"\x1b[5~".to_vec())
);
assert_eq!(
encode_key(key(KeyCode::PageUp, KeyModifiers::CONTROL)),
Some(b"\x1b[5;5~".to_vec())
);
}
#[test]
fn alt_char_is_escape_prefixed() {
assert_eq!(
encode_key(key(KeyCode::Char('b'), KeyModifiers::ALT)),
Some(vec![0x1B, b'b'])
);
}
#[test]
fn function_keys_use_ss3_and_csi_sequences() {
assert_eq!(
encode_key(key(KeyCode::F(1), KeyModifiers::NONE)),
Some(b"\x1bOP".to_vec())
);
assert_eq!(
encode_key(key(KeyCode::F(4), KeyModifiers::NONE)),
Some(b"\x1bOS".to_vec())
);
assert_eq!(
encode_key(key(KeyCode::F(5), KeyModifiers::NONE)),
Some(b"\x1b[15~".to_vec())
);
assert_eq!(
encode_key(key(KeyCode::F(12), KeyModifiers::NONE)),
Some(b"\x1b[24~".to_vec())
);
}
#[test]
fn modified_function_keys_use_the_csi_form() {
assert_eq!(
encode_key(key(KeyCode::F(1), KeyModifiers::CONTROL)),
Some(b"\x1b[1;5P".to_vec())
);
assert_eq!(
encode_key(key(KeyCode::F(5), KeyModifiers::SHIFT)),
Some(b"\x1b[15;2~".to_vec())
);
}
#[test]
fn unsupported_function_key_is_ignored() {
assert_eq!(encode_key(key(KeyCode::F(13), KeyModifiers::NONE)), None);
}
#[test]
fn sgr_mouse_press_uses_terminal_coordinates() {
let mouse = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 0,
row: 0,
modifiers: KeyModifiers::CONTROL,
};
assert_eq!(
encode_mouse(
mouse,
4,
2,
MouseProtocolMode::ButtonMotion,
MouseProtocolEncoding::Sgr,
),
Some(b"\x1b[<16;5;3M".to_vec())
);
}
#[test]
fn mouse_motion_respects_the_child_requested_mode() {
let mouse = MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column: 0,
row: 0,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
encode_mouse(
mouse,
0,
0,
MouseProtocolMode::PressRelease,
MouseProtocolEncoding::Sgr,
),
None
);
assert_eq!(
encode_mouse(
mouse,
0,
0,
MouseProtocolMode::ButtonMotion,
MouseProtocolEncoding::Sgr,
),
Some(b"\x1b[<32;1;1M".to_vec())
);
}
#[test]
fn sgr_mouse_release_keeps_the_button_code() {
let mouse = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Right),
column: 0,
row: 0,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
encode_mouse(
mouse,
1,
3,
MouseProtocolMode::PressRelease,
MouseProtocolEncoding::Sgr,
),
Some(b"\x1b[<2;2;4m".to_vec())
);
}
#[test]
fn legacy_mouse_reports_reject_large_coordinates() {
let mouse = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 0,
row: 0,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
encode_mouse(
mouse,
MAX_LEGACY_MOUSE_COORDINATE,
0,
MouseProtocolMode::Press,
MouseProtocolEncoding::Default,
),
None
);
}
}