use serde::{Deserialize, Serialize};
use crate::types::{TerminalError, MAX_INPUT_BYTES};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Modifier {
#[serde(alias = "control")]
Ctrl,
#[serde(alias = "option")]
Alt,
Shift,
#[serde(alias = "command", alias = "cmd", alias = "meta", alias = "windows")]
Super,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NamedKey {
#[serde(alias = "return")]
Enter,
#[serde(alias = "esc")]
Escape,
Tab,
Backspace,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Insert,
Delete,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum KeyCode {
Named {
name: NamedKey,
},
Character {
value: char,
},
Function {
number: u8,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum InputEvent {
Text {
text: String,
},
Key {
key: KeyCode,
#[serde(default)]
modifiers: Vec<Modifier>,
},
}
pub(crate) fn encode_events(events: &[InputEvent]) -> Result<Vec<u8>, TerminalError> {
let mut output = Vec::new();
for event in events {
let bytes = match event {
InputEvent::Text { text } => text.as_bytes().to_vec(),
InputEvent::Key { key, modifiers } => encode_key(key, modifiers)?,
};
if output.len().saturating_add(bytes.len()) > MAX_INPUT_BYTES {
return Err(TerminalError::InvalidArgument(format!(
"encoded input exceeds {MAX_INPUT_BYTES} bytes"
)));
}
output.extend_from_slice(&bytes);
}
Ok(output)
}
fn encode_key(key: &KeyCode, modifiers: &[Modifier]) -> Result<Vec<u8>, TerminalError> {
validate_modifiers(modifiers)?;
match key {
KeyCode::Character { value } => encode_character(*value, modifiers),
KeyCode::Function { number } => {
if !(1..=12).contains(number) {
return Err(TerminalError::InvalidArgument(
"function key number must be between 1 and 12".to_string(),
));
}
encode_function(*number, modifiers)
}
KeyCode::Named { name } => encode_named(*name, modifiers),
}
}
fn validate_modifiers(modifiers: &[Modifier]) -> Result<(), TerminalError> {
for (index, modifier) in modifiers.iter().enumerate() {
if modifiers[..index].contains(modifier) {
return Err(TerminalError::InvalidArgument(format!(
"duplicate modifier {modifier:?}"
)));
}
}
Ok(())
}
fn encode_character(value: char, modifiers: &[Modifier]) -> Result<Vec<u8>, TerminalError> {
let shift = modifiers.contains(&Modifier::Shift);
let alt = modifiers.contains(&Modifier::Alt);
let ctrl = modifiers.contains(&Modifier::Ctrl);
let super_key = modifiers.contains(&Modifier::Super);
if super_key {
return Err(TerminalError::InvalidArgument(
"super-modified characters have no portable terminal encoding".to_string(),
));
}
let value = if shift && value.is_ascii_lowercase() {
value.to_ascii_uppercase()
} else {
value
};
let encoded = if ctrl {
let byte = match value.to_ascii_uppercase() {
'@' | ' ' => 0,
'A'..='Z' => (value.to_ascii_uppercase() as u8) - b'@',
'[' => 27,
'\\' => 28,
']' => 29,
'^' => 30,
'_' => 31,
'?' => 127,
_ => {
return Err(TerminalError::InvalidArgument(format!(
"character {value:?} has no portable control-key encoding"
)));
}
};
vec![byte]
} else {
value.to_string().into_bytes()
};
if alt {
let mut with_escape = Vec::with_capacity(encoded.len() + 1);
with_escape.push(0x1b);
with_escape.extend(encoded);
Ok(with_escape)
} else {
Ok(encoded)
}
}
fn encode_named(name: NamedKey, modifiers: &[Modifier]) -> Result<Vec<u8>, TerminalError> {
let modifier = xterm_modifier(modifiers);
let unsupported_simple = modifiers.contains(&Modifier::Super);
let bytes = match name {
NamedKey::Up => csi_final('A', modifier),
NamedKey::Down => csi_final('B', modifier),
NamedKey::Right => csi_final('C', modifier),
NamedKey::Left => csi_final('D', modifier),
NamedKey::Home => csi_final('H', modifier),
NamedKey::End => csi_final('F', modifier),
NamedKey::Insert => csi_tilde(2, modifier),
NamedKey::Delete => csi_tilde(3, modifier),
NamedKey::PageUp => csi_tilde(5, modifier),
NamedKey::PageDown => csi_tilde(6, modifier),
NamedKey::Tab if modifiers == [Modifier::Shift] => b"\x1b[Z".to_vec(),
NamedKey::Enter | NamedKey::Escape | NamedKey::Tab | NamedKey::Backspace
if modifiers.is_empty() =>
{
match name {
NamedKey::Enter => b"\r".to_vec(),
NamedKey::Escape => b"\x1b".to_vec(),
NamedKey::Tab => b"\t".to_vec(),
NamedKey::Backspace => b"\x7f".to_vec(),
_ => unreachable!(),
}
}
NamedKey::Enter | NamedKey::Escape | NamedKey::Tab | NamedKey::Backspace => {
if unsupported_simple {
return Err(TerminalError::InvalidArgument(format!(
"super-modified {name:?} has no portable terminal encoding"
)));
}
let base = match name {
NamedKey::Enter => b'\r',
NamedKey::Escape => 0x1b,
NamedKey::Tab => b'\t',
NamedKey::Backspace => 0x7f,
_ => unreachable!(),
};
if modifiers == [Modifier::Alt] {
vec![0x1b, base]
} else {
return Err(TerminalError::InvalidArgument(format!(
"modifier combination is unsupported for {name:?}"
)));
}
}
};
Ok(bytes)
}
fn encode_function(number: u8, modifiers: &[Modifier]) -> Result<Vec<u8>, TerminalError> {
let modifier = xterm_modifier(modifiers);
let code = match number {
1 => return Ok(csi_final('P', modifier)),
2 => return Ok(csi_final('Q', modifier)),
3 => return Ok(csi_final('R', modifier)),
4 => return Ok(csi_final('S', modifier)),
5 => 15,
6 => 17,
7 => 18,
8 => 19,
9 => 20,
10 => 21,
11 => 23,
12 => 24,
_ => unreachable!(),
};
Ok(csi_tilde(code, modifier))
}
fn xterm_modifier(modifiers: &[Modifier]) -> u8 {
1 + u8::from(modifiers.contains(&Modifier::Shift))
+ 2 * u8::from(modifiers.contains(&Modifier::Alt))
+ 4 * u8::from(modifiers.contains(&Modifier::Ctrl))
+ 8 * u8::from(modifiers.contains(&Modifier::Super))
}
fn csi_final(final_byte: char, modifier: u8) -> Vec<u8> {
if modifier == 1 {
format!("\x1b[{final_byte}").into_bytes()
} else {
format!("\x1b[1;{modifier}{final_byte}").into_bytes()
}
}
fn csi_tilde(code: u8, modifier: u8) -> Vec<u8> {
if modifier == 1 {
format!("\x1b[{code}~").into_bytes()
} else {
format!("\x1b[{code};{modifier}~").into_bytes()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_control_and_modified_navigation() {
assert_eq!(
encode_key(&KeyCode::Character { value: 'w' }, &[Modifier::Ctrl]).unwrap(),
vec![0x17]
);
assert_eq!(
encode_key(
&KeyCode::Named {
name: NamedKey::Left
},
&[Modifier::Ctrl, Modifier::Shift]
)
.unwrap(),
b"\x1b[1;6D"
);
}
#[test]
fn validates_the_whole_input_batch_before_use() {
let err = encode_events(&[
InputEvent::Text { text: "ok".into() },
InputEvent::Key {
key: KeyCode::Function { number: 13 },
modifiers: vec![],
},
])
.unwrap_err();
assert!(err.to_string().contains("between 1 and 12"));
}
}