use crate::command::{Context, ScripttyCommand};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
pub struct KeyPress {
pub bytes: Vec<u8>,
}
impl KeyPress {
pub const NAME: &'static str = "key";
}
fn modifier_code(ctrl: bool, alt: bool, shift: bool) -> Option<u8> {
let n = (shift as u8) | ((alt as u8) << 1) | ((ctrl as u8) << 2);
if n == 0 { None } else { Some(n + 1) }
}
fn arrow_seq(final_byte: u8, ctrl: bool, alt: bool, shift: bool) -> Vec<u8> {
match modifier_code(ctrl, alt, shift) {
None => vec![0x1b, b'[', final_byte],
Some(m) => {
let mut v = format!("\x1b[1;{}", m).into_bytes();
v.push(final_byte);
v
}
}
}
fn tilde_seq(n: u8, ctrl: bool, alt: bool, shift: bool) -> Vec<u8> {
match modifier_code(ctrl, alt, shift) {
None => format!("\x1b[{}~", n).into_bytes(),
Some(m) => format!("\x1b[{};{}~", n, m).into_bytes(),
}
}
fn f1f4_seq(final_byte: u8, ctrl: bool, alt: bool, shift: bool) -> Vec<u8> {
match modifier_code(ctrl, alt, shift) {
None => vec![0x1b, b'O', final_byte],
Some(m) => {
let mut v = format!("\x1b[1;{}", m).into_bytes();
v.push(final_byte);
v
}
}
}
fn key_to_bytes(key: &str, ctrl: bool, alt: bool, shift: bool) -> Result<Vec<u8>> {
match key {
"Enter" => {
if alt {
Ok(vec![0x1b, b'\r'])
} else {
Ok(vec![b'\r'])
}
}
"Backspace" => {
if ctrl {
Ok(vec![0x08])
} else if alt {
Ok(vec![0x1b, 0x7f])
} else {
Ok(vec![0x7f])
}
}
"Tab" => {
if shift {
Ok(vec![0x1b, b'[', b'Z'])
} else if alt {
Ok(vec![0x1b, b'\t'])
} else {
Ok(vec![b'\t'])
}
}
"Escape" | "Esc" => {
if alt {
Ok(vec![0x1b, 0x1b])
} else {
Ok(vec![0x1b])
}
}
"Space" => {
if ctrl {
Ok(vec![0x00])
} else if alt {
Ok(vec![0x1b, b' '])
} else {
Ok(vec![b' '])
}
}
"Up" => Ok(arrow_seq(b'A', ctrl, alt, shift)),
"Down" => Ok(arrow_seq(b'B', ctrl, alt, shift)),
"Right" => Ok(arrow_seq(b'C', ctrl, alt, shift)),
"Left" => Ok(arrow_seq(b'D', ctrl, alt, shift)),
"Home" => Ok(arrow_seq(b'H', ctrl, alt, shift)),
"End" => Ok(arrow_seq(b'F', ctrl, alt, shift)),
"Insert" => Ok(tilde_seq(2, ctrl, alt, shift)),
"Delete" | "Del" => Ok(tilde_seq(3, ctrl, alt, shift)),
"PageUp" => Ok(tilde_seq(5, ctrl, alt, shift)),
"PageDown" => Ok(tilde_seq(6, ctrl, alt, shift)),
"F1" => Ok(f1f4_seq(b'P', ctrl, alt, shift)),
"F2" => Ok(f1f4_seq(b'Q', ctrl, alt, shift)),
"F3" => Ok(f1f4_seq(b'R', ctrl, alt, shift)),
"F4" => Ok(f1f4_seq(b'S', ctrl, alt, shift)),
"F5" => Ok(tilde_seq(15, ctrl, alt, shift)),
"F6" => Ok(tilde_seq(17, ctrl, alt, shift)),
"F7" => Ok(tilde_seq(18, ctrl, alt, shift)),
"F8" => Ok(tilde_seq(19, ctrl, alt, shift)),
"F9" => Ok(tilde_seq(20, ctrl, alt, shift)),
"F10" => Ok(tilde_seq(21, ctrl, alt, shift)),
"F11" => Ok(tilde_seq(23, ctrl, alt, shift)),
"F12" => Ok(tilde_seq(24, ctrl, alt, shift)),
_ => {
let mut chars = key.chars();
if let Some(ch) = chars.next()
&& chars.next().is_none()
&& ch.is_ascii()
{
let byte = ch as u8;
if ctrl && ch.is_ascii_alphabetic() {
return Ok(vec![ch.to_ascii_lowercase() as u8 - b'a' + 1]);
} else if alt {
return Ok(vec![0x1b, byte]);
} else {
return Ok(vec![byte]);
}
}
Err(anyhow!("Unknown key: {}", key))
}
}
}
#[async_trait(?Send)]
impl ScripttyCommand for KeyPress {
fn name(&self) -> &'static str {
Self::NAME
}
fn parse(args: &str) -> Result<Self> {
let mut token = args.trim();
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
loop {
if let Some(rest) = token.strip_prefix("Ctrl+") {
ctrl = true;
token = rest;
} else if let Some(rest) = token.strip_prefix("Alt+") {
alt = true;
token = rest;
} else if let Some(rest) = token.strip_prefix("Shift+") {
shift = true;
token = rest;
} else {
break;
}
}
if token.is_empty() {
return Err(anyhow!("key command requires a key name"));
}
let bytes = key_to_bytes(token, ctrl, alt, shift)?;
Ok(Self { bytes })
}
async fn execute(&self, ctx: &mut Context) -> Result<()> {
ctx.write_to_pty(&self.bytes)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::ScripttyCommand;
fn parse(s: &str) -> KeyPress {
KeyPress::parse(s).unwrap()
}
#[test]
fn test_enter() {
assert_eq!(parse("Enter").bytes, b"\r");
}
#[test]
fn test_ctrl_c() {
assert_eq!(parse("Ctrl+C").bytes, b"\x03");
}
#[test]
fn test_ctrl_d() {
assert_eq!(parse("Ctrl+D").bytes, b"\x04");
}
#[test]
fn test_shift_tab() {
assert_eq!(parse("Shift+Tab").bytes, b"\x1b[Z");
}
#[test]
fn test_up() {
assert_eq!(parse("Up").bytes, b"\x1b[A");
}
#[test]
fn test_ctrl_up() {
assert_eq!(parse("Ctrl+Up").bytes, b"\x1b[1;5A");
}
#[test]
fn test_alt_left() {
assert_eq!(parse("Alt+Left").bytes, b"\x1b[1;3D");
}
#[test]
fn test_page_up() {
assert_eq!(parse("PageUp").bytes, b"\x1b[5~");
}
#[test]
fn test_ctrl_page_down() {
assert_eq!(parse("Ctrl+PageDown").bytes, b"\x1b[6;5~");
}
#[test]
fn test_f1() {
assert_eq!(parse("F1").bytes, b"\x1bOP");
}
#[test]
fn test_ctrl_f5() {
assert_eq!(parse("Ctrl+F5").bytes, b"\x1b[15;5~");
}
#[test]
fn test_alt_enter() {
assert_eq!(parse("Alt+Enter").bytes, b"\x1b\r");
}
#[test]
fn test_backspace() {
assert_eq!(parse("Backspace").bytes, b"\x7f");
}
#[test]
fn test_delete() {
assert_eq!(parse("Delete").bytes, b"\x1b[3~");
}
#[test]
fn test_home() {
assert_eq!(parse("Home").bytes, b"\x1b[H");
}
#[test]
fn test_ctrl_home() {
assert_eq!(parse("Ctrl+Home").bytes, b"\x1b[1;5H");
}
#[test]
fn test_ctrl_alt_left() {
assert_eq!(parse("Ctrl+Alt+Left").bytes, b"\x1b[1;7D");
}
#[test]
fn test_unknown_key() {
assert!(KeyPress::parse("UnknownKey").is_err());
}
#[test]
fn test_empty_key() {
assert!(KeyPress::parse("").is_err());
}
}