use super::kitty::KITTY_MODIFIERS;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventType {
Press,
Repeat,
Release,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Key {
pub name: String,
pub ctrl: bool,
pub meta: bool,
pub shift: bool,
pub sequence: String,
pub raw: Option<String>,
pub code: Option<String>,
pub super_key: bool,
pub hyper: bool,
pub caps_lock: bool,
pub num_lock: bool,
pub event_type: Option<EventType>,
pub is_kitty_protocol: bool,
pub text: Option<String>,
pub is_printable: Option<bool>,
}
impl Key {
fn legacy(sequence: String) -> Self {
Key {
name: String::new(),
ctrl: false,
meta: false,
shift: false,
raw: Some(sequence.clone()),
sequence,
code: None,
super_key: false,
hyper: false,
caps_lock: false,
num_lock: false,
event_type: None,
is_kitty_protocol: false,
text: None,
is_printable: None,
}
}
}
fn key_name(code: &str) -> Option<&'static str> {
Some(match code {
"OP" => "f1",
"OQ" => "f2",
"OR" => "f3",
"OS" => "f4",
"[P" => "f1",
"[Q" => "f2",
"[R" => "f3",
"[S" => "f4",
"[11~" => "f1",
"[12~" => "f2",
"[13~" => "f3",
"[14~" => "f4",
"[[A" => "f1",
"[[B" => "f2",
"[[C" => "f3",
"[[D" => "f4",
"[[E" => "f5",
"[15~" => "f5",
"[17~" => "f6",
"[18~" => "f7",
"[19~" => "f8",
"[20~" => "f9",
"[21~" => "f10",
"[23~" => "f11",
"[24~" => "f12",
"[A" => "up",
"[B" => "down",
"[C" => "right",
"[D" => "left",
"[E" => "clear",
"[F" => "end",
"[H" => "home",
"OA" => "up",
"OB" => "down",
"OC" => "right",
"OD" => "left",
"OE" => "clear",
"OF" => "end",
"OH" => "home",
"[1~" => "home",
"[2~" => "insert",
"[3~" => "delete",
"[4~" => "end",
"[5~" => "pageup",
"[6~" => "pagedown",
"[[5~" => "pageup",
"[[6~" => "pagedown",
"[7~" => "home",
"[8~" => "end",
"[a" => "up",
"[b" => "down",
"[c" => "right",
"[d" => "left",
"[e" => "clear",
"[2$" => "insert",
"[3$" => "delete",
"[5$" => "pageup",
"[6$" => "pagedown",
"[7$" => "home",
"[8$" => "end",
"Oa" => "up",
"Ob" => "down",
"Oc" => "right",
"Od" => "left",
"Oe" => "clear",
"[2^" => "insert",
"[3^" => "delete",
"[5^" => "pageup",
"[6^" => "pagedown",
"[7^" => "home",
"[8^" => "end",
"[Z" => "tab",
_ => return None,
})
}
fn is_shift_key(code: &str) -> bool {
matches!(
code,
"[a" | "[b" | "[c" | "[d" | "[e" | "[2$" | "[3$" | "[5$" | "[6$" | "[7$" | "[8$" | "[Z"
)
}
fn is_ctrl_key(code: &str) -> bool {
matches!(
code,
"Oa" | "Ob" | "Oc" | "Od" | "Oe" | "[2^" | "[3^" | "[5^" | "[6^" | "[7^" | "[8^"
)
}
fn kitty_special_letter_key(terminator: char) -> Option<&'static str> {
Some(match terminator {
'A' => "up",
'B' => "down",
'C' => "right",
'D' => "left",
'E' => "clear",
'F' => "end",
'H' => "home",
'P' => "f1",
'Q' => "f2",
'R' => "f3",
'S' => "f4",
_ => return None,
})
}
fn kitty_special_number_key(number: u32) -> Option<&'static str> {
Some(match number {
2 => "insert",
3 => "delete",
5 => "pageup",
6 => "pagedown",
7 => "home",
8 => "end",
11 => "f1",
12 => "f2",
13 => "f3",
14 => "f4",
15 => "f5",
17 => "f6",
18 => "f7",
19 => "f8",
20 => "f9",
21 => "f10",
23 => "f11",
24 => "f12",
_ => return None,
})
}
fn kitty_codepoint_name(cp: u32) -> Option<&'static str> {
Some(match cp {
27 => "escape",
9 => "tab",
127 => "backspace",
8 => "backspace",
57358 => "capslock",
57359 => "scrolllock",
57360 => "numlock",
57361 => "printscreen",
57362 => "pause",
57363 => "menu",
57376 => "f13",
57377 => "f14",
57378 => "f15",
57379 => "f16",
57380 => "f17",
57381 => "f18",
57382 => "f19",
57383 => "f20",
57384 => "f21",
57385 => "f22",
57386 => "f23",
57387 => "f24",
57388 => "f25",
57389 => "f26",
57390 => "f27",
57391 => "f28",
57392 => "f29",
57393 => "f30",
57394 => "f31",
57395 => "f32",
57396 => "f33",
57397 => "f34",
57398 => "f35",
57399 => "kp0",
57400 => "kp1",
57401 => "kp2",
57402 => "kp3",
57403 => "kp4",
57404 => "kp5",
57405 => "kp6",
57406 => "kp7",
57407 => "kp8",
57408 => "kp9",
57409 => "kpdecimal",
57410 => "kpdivide",
57411 => "kpmultiply",
57412 => "kpsubtract",
57413 => "kpadd",
57414 => "kpenter",
57415 => "kpequal",
57416 => "kpseparator",
57417 => "kpleft",
57418 => "kpright",
57419 => "kpup",
57420 => "kpdown",
57421 => "kppageup",
57422 => "kppagedown",
57423 => "kphome",
57424 => "kpend",
57425 => "kpinsert",
57426 => "kpdelete",
57427 => "kpbegin",
57428 => "mediaplay",
57429 => "mediapause",
57430 => "mediaplaypause",
57431 => "mediareverse",
57432 => "mediastop",
57433 => "mediafastforward",
57434 => "mediarewind",
57435 => "mediatracknext",
57436 => "mediatrackprevious",
57437 => "mediarecord",
57438 => "lowervolume",
57439 => "raisevolume",
57440 => "mutevolume",
57441 => "leftshift",
57442 => "leftcontrol",
57443 => "leftalt",
57444 => "leftsuper",
57445 => "lefthyper",
57446 => "leftmeta",
57447 => "rightshift",
57448 => "rightcontrol",
57449 => "rightalt",
57450 => "rightsuper",
57451 => "righthyper",
57452 => "rightmeta",
57453 => "isoLevel3Shift",
57454 => "isoLevel5Shift",
_ => return None,
})
}
fn is_valid_codepoint(cp: u32) -> bool {
cp <= 0x10_ffff && !(0xd8_00..=0xdf_ff).contains(&cp)
}
fn safe_from_codepoint(cp: u32) -> String {
match char::from_u32(cp) {
Some(c) if is_valid_codepoint(cp) => c.to_string(),
_ => "?".to_string(),
}
}
fn resolve_event_type(value: u32) -> EventType {
match value {
3 => EventType::Release,
2 => EventType::Repeat,
_ => EventType::Press,
}
}
struct KittyModifiers {
ctrl: bool,
shift: bool,
meta: bool,
super_key: bool,
hyper: bool,
caps_lock: bool,
num_lock: bool,
}
fn parse_kitty_modifiers(modifiers: u32) -> KittyModifiers {
KittyModifiers {
ctrl: modifiers & KITTY_MODIFIERS.ctrl != 0,
shift: modifiers & KITTY_MODIFIERS.shift != 0,
meta: modifiers & (KITTY_MODIFIERS.meta | KITTY_MODIFIERS.alt) != 0,
super_key: modifiers & KITTY_MODIFIERS.super_key != 0,
hyper: modifiers & KITTY_MODIFIERS.hyper != 0,
caps_lock: modifiers & KITTY_MODIFIERS.caps_lock != 0,
num_lock: modifiers & KITTY_MODIFIERS.num_lock != 0,
}
}
#[allow(clippy::result_unit_err)]
fn parse_kitty_keypress(s: &str) -> Option<Result<Key, ()>> {
let body = s.strip_prefix("\u{1b}[")?.strip_suffix('u')?;
let mut groups = body.split(';');
let codepoint_str = groups.next()?;
let codepoint: u32 = parse_all_digits(codepoint_str)?;
let (modifiers_raw, event_type_raw) = match groups.next() {
Some(seg) => {
let mut parts = seg.split(':');
let m = parse_all_digits(parts.next()?)?;
let e = match parts.next() {
Some(e_str) => Some(parse_all_digits(e_str)?),
None => None,
};
if parts.next().is_some() {
return None;
}
(Some(m), e)
}
None => (None, None),
};
let text_field = match groups.next() {
Some(t) => {
if t.is_empty() || !t.chars().all(|c| c.is_ascii_digit() || c == ':') {
return None;
}
Some(t)
}
None => None,
};
if groups.next().is_some() {
return None;
}
let modifiers = modifiers_raw.map_or(0, |m| m.saturating_sub(1));
let event_type = event_type_raw.unwrap_or(1);
if !is_valid_codepoint(codepoint) {
return Some(Err(()));
}
let mut text: Option<String> = match text_field {
Some(field) => {
let mut out = String::new();
for cp_str in field.split(':') {
let cp = parse_all_digits(cp_str)?;
out.push_str(&safe_from_codepoint(cp));
}
Some(out)
}
None => None,
};
let (name, is_printable) = if codepoint == 32 {
("space".to_string(), true)
} else if codepoint == 13 {
("return".to_string(), true)
} else if let Some(n) = kitty_codepoint_name(codepoint) {
(n.to_string(), false)
} else if (1..=26).contains(&codepoint) {
(
char::from_u32(codepoint + 96)
.map(String::from)
.unwrap_or_default(),
false,
)
} else {
(safe_from_codepoint(codepoint).to_lowercase(), true)
};
if is_printable && text.is_none() {
text = Some(safe_from_codepoint(codepoint));
}
let m = parse_kitty_modifiers(modifiers);
Some(Ok(Key {
name,
ctrl: m.ctrl,
meta: m.meta,
shift: m.shift,
super_key: m.super_key,
hyper: m.hyper,
caps_lock: m.caps_lock,
num_lock: m.num_lock,
event_type: Some(resolve_event_type(event_type)),
sequence: s.to_string(),
raw: Some(s.to_string()),
code: None,
is_kitty_protocol: true,
is_printable: Some(is_printable),
text,
}))
}
fn parse_kitty_special_key(s: &str) -> Option<Key> {
let body = s.strip_prefix("\u{1b}[")?;
let terminator = body.chars().last()?;
if !(terminator.is_ascii_alphabetic() || terminator == '~') {
return None;
}
let body = &body[..body.len() - terminator.len_utf8()];
let mut parts = body.split(';');
let number: u32 = parse_all_digits(parts.next()?)?;
let rest = parts.next()?;
if parts.next().is_some() {
return None;
}
let mut mod_event = rest.split(':');
let modifiers_raw: u32 = parse_all_digits(mod_event.next()?)?;
let event_type: u32 = parse_all_digits(mod_event.next()?)?;
if mod_event.next().is_some() {
return None;
}
let modifiers = modifiers_raw.saturating_sub(1);
let name = if terminator == '~' {
kitty_special_number_key(number)?
} else {
kitty_special_letter_key(terminator)?
};
let m = parse_kitty_modifiers(modifiers);
Some(Key {
name: name.to_string(),
ctrl: m.ctrl,
meta: m.meta,
shift: m.shift,
super_key: m.super_key,
hyper: m.hyper,
caps_lock: m.caps_lock,
num_lock: m.num_lock,
event_type: Some(resolve_event_type(event_type)),
sequence: s.to_string(),
raw: Some(s.to_string()),
code: None,
is_kitty_protocol: true,
is_printable: Some(false),
text: None,
})
}
fn parse_all_digits(s: &str) -> Option<u32> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse::<u32>().ok()
}
pub fn parse_keypress(bytes: &[u8]) -> Key {
let decoded: String = if bytes.len() == 1 && bytes[0] > 127 {
let mut s = String::from('\u{1b}');
s.push_str(&String::from_utf8_lossy(&[bytes[0] - 128]));
s
} else {
String::from_utf8_lossy(bytes).into_owned()
};
parse_keypress_str(&decoded)
}
fn parse_keypress_str(s: &str) -> Key {
match parse_kitty_keypress(s) {
Some(Ok(key)) => return key,
Some(Err(())) => {
return Key {
name: String::new(),
ctrl: false,
meta: false,
shift: false,
sequence: s.to_string(),
raw: Some(s.to_string()),
code: None,
super_key: false,
hyper: false,
caps_lock: false,
num_lock: false,
event_type: None,
is_kitty_protocol: true,
is_printable: Some(false),
text: None,
};
}
None => {}
}
if let Some(key) = parse_kitty_special_key(s) {
return key;
}
let mut key = Key::legacy(s.to_string());
let chars: Vec<char> = s.chars().collect();
if s == "\r" || s == "\u{1b}\r" {
key.raw = None;
key.name = "return".to_string();
key.meta = chars.len() == 2;
} else if s == "\n" {
key.name = "enter".to_string();
} else if s == "\t" {
key.name = "tab".to_string();
} else if s == "\u{8}" || s == "\u{1b}\u{8}" {
key.name = "backspace".to_string();
key.meta = chars.first() == Some(&'\u{1b}');
} else if s == "\u{7f}" || s == "\u{1b}\u{7f}" {
key.name = "backspace".to_string();
key.meta = chars.first() == Some(&'\u{1b}');
} else if s == "\u{1b}" || s == "\u{1b}\u{1b}" {
key.name = "escape".to_string();
key.meta = chars.len() == 2;
} else if s == " " || s == "\u{1b} " {
key.name = "space".to_string();
key.meta = chars.len() == 2;
} else if chars.len() == 1 && chars[0] <= '\u{1a}' {
let c = chars[0] as u32;
key.name = char::from_u32(c + ('a' as u32) - 1)
.map(String::from)
.unwrap_or_default();
key.ctrl = true;
} else if chars.len() == 1 && chars[0].is_ascii_digit() {
key.name = "number".to_string();
} else if chars.len() == 1 && chars[0].is_ascii_lowercase() {
key.name = chars[0].to_string();
} else if chars.len() == 1 && chars[0].is_ascii_uppercase() {
key.name = chars[0].to_ascii_lowercase().to_string();
key.shift = true;
} else if let Some((name, meta, shift)) = match_meta_key_code(s) {
key.name = name;
key.meta = meta;
key.shift = shift;
} else if let Some(parsed) = match_fn_key(&chars) {
if chars.first() == Some(&'\u{1b}') && chars.get(1) == Some(&'\u{1b}') {
key.meta = true;
}
let modifier = parsed.modifier;
key.ctrl = modifier & 4 != 0;
key.meta = key.meta || (modifier & 10 != 0);
key.shift = modifier & 1 != 0;
key.code = Some(parsed.code.clone());
key.name = key_name(&parsed.code).unwrap_or("").to_string();
key.shift = is_shift_key(&parsed.code) || key.shift;
key.ctrl = is_ctrl_key(&parsed.code) || key.ctrl;
}
key
}
fn match_meta_key_code(s: &str) -> Option<(String, bool, bool)> {
let inner = s.strip_prefix('\u{1b}')?;
let mut it = inner.chars();
let c = it.next()?;
if it.next().is_some() {
return None;
}
if !c.is_ascii_alphanumeric() {
return None;
}
let name = c.to_ascii_lowercase().to_string();
let shift = c.is_ascii_uppercase();
Some((name, true, shift))
}
struct FnKeyMatch {
code: String,
modifier: i64,
}
fn match_fn_key(chars: &[char]) -> Option<FnKeyMatch> {
let mut i = 0;
if chars.get(i) != Some(&'\u{1b}') {
return None;
}
while chars.get(i) == Some(&'\u{1b}') {
i += 1;
}
let prefix: String;
if chars.get(i) == Some(&'[') {
if chars.get(i + 1) == Some(&'[') {
prefix = "[[".to_string();
i += 2;
} else {
prefix = "[".to_string();
i += 1;
}
} else if chars.get(i) == Some(&'O') {
prefix = "O".to_string();
i += 1;
} else if chars.get(i) == Some(&'N') {
prefix = "N".to_string();
i += 1;
} else {
return None;
}
let rest = &chars[i..];
if let Some(m) = match_fn_branch_a(&prefix, rest) {
return Some(m);
}
match_fn_branch_b(&prefix, rest)
}
fn match_fn_branch_a(prefix: &str, rest: &[char]) -> Option<FnKeyMatch> {
let mut j = 0;
let num_start = j;
while rest.get(j).is_some_and(|c| c.is_ascii_digit()) {
j += 1;
}
if j == num_start {
return None;
}
let p2: String = rest[num_start..j].iter().collect();
let mut p3: Option<String> = None;
if rest.get(j) == Some(&';') {
let mut k = j + 1;
let s = k;
while rest.get(k).is_some_and(|c| c.is_ascii_digit()) {
k += 1;
}
if k > s {
p3 = Some(rest[s..k].iter().collect());
j = k;
}
}
let term = rest.get(j)?;
if !matches!(term, '~' | '^' | '$') {
return None;
}
let code = format!("{prefix}{p2}{term}");
let modifier = i64::from(p3.as_deref().and_then(parse_all_digits).unwrap_or(1)) - 1;
Some(FnKeyMatch { code, modifier })
}
fn match_fn_branch_b(prefix: &str, rest: &[char]) -> Option<FnKeyMatch> {
let mut j = 0;
if rest.get(j) == Some(&'1') && rest.get(j + 1) == Some(&';') {
j += 2;
}
let s = j;
while rest.get(j).is_some_and(|c| c.is_ascii_digit()) {
j += 1;
}
let p5: Option<String> = if j > s {
Some(rest[s..j].iter().collect())
} else {
None
};
let letter = rest.get(j)?;
if !letter.is_ascii_alphabetic() {
return None;
}
let code = format!("{prefix}{letter}");
let modifier = i64::from(p5.as_deref().and_then(parse_all_digits).unwrap_or(1)) - 1;
Some(FnKeyMatch { code, modifier })
}
#[cfg(test)]
mod tests;