1use super::kitty::KITTY_MODIFIERS;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum EventType {
28 Press,
29 Repeat,
30 Release,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Key {
36 pub name: String,
39 pub ctrl: bool,
40 pub meta: bool,
41 pub shift: bool,
42 pub sequence: String,
44 pub raw: Option<String>,
46 pub code: Option<String>,
48 pub super_key: bool,
49 pub hyper: bool,
50 pub caps_lock: bool,
51 pub num_lock: bool,
52 pub event_type: Option<EventType>,
54 pub is_kitty_protocol: bool,
56 pub text: Option<String>,
59 pub is_printable: Option<bool>,
62}
63
64impl Key {
65 fn legacy(sequence: String) -> Self {
68 Key {
69 name: String::new(),
70 ctrl: false,
71 meta: false,
72 shift: false,
73 raw: Some(sequence.clone()),
74 sequence,
75 code: None,
76 super_key: false,
77 hyper: false,
78 caps_lock: false,
79 num_lock: false,
80 event_type: None,
81 is_kitty_protocol: false,
82 text: None,
83 is_printable: None,
84 }
85 }
86}
87
88fn key_name(code: &str) -> Option<&'static str> {
92 Some(match code {
93 "OP" => "f1",
95 "OQ" => "f2",
96 "OR" => "f3",
97 "OS" => "f4",
98 "[P" => "f1",
100 "[Q" => "f2",
101 "[R" => "f3",
102 "[S" => "f4",
103 "[11~" => "f1",
105 "[12~" => "f2",
106 "[13~" => "f3",
107 "[14~" => "f4",
108 "[[A" => "f1",
110 "[[B" => "f2",
111 "[[C" => "f3",
112 "[[D" => "f4",
113 "[[E" => "f5",
114 "[15~" => "f5",
116 "[17~" => "f6",
117 "[18~" => "f7",
118 "[19~" => "f8",
119 "[20~" => "f9",
120 "[21~" => "f10",
121 "[23~" => "f11",
122 "[24~" => "f12",
123 "[A" => "up",
125 "[B" => "down",
126 "[C" => "right",
127 "[D" => "left",
128 "[E" => "clear",
129 "[F" => "end",
130 "[H" => "home",
131 "OA" => "up",
133 "OB" => "down",
134 "OC" => "right",
135 "OD" => "left",
136 "OE" => "clear",
137 "OF" => "end",
138 "OH" => "home",
139 "[1~" => "home",
141 "[2~" => "insert",
142 "[3~" => "delete",
143 "[4~" => "end",
144 "[5~" => "pageup",
145 "[6~" => "pagedown",
146 "[[5~" => "pageup",
148 "[[6~" => "pagedown",
149 "[7~" => "home",
151 "[8~" => "end",
152 "[a" => "up",
154 "[b" => "down",
155 "[c" => "right",
156 "[d" => "left",
157 "[e" => "clear",
158
159 "[2$" => "insert",
160 "[3$" => "delete",
161 "[5$" => "pageup",
162 "[6$" => "pagedown",
163 "[7$" => "home",
164 "[8$" => "end",
165
166 "Oa" => "up",
167 "Ob" => "down",
168 "Oc" => "right",
169 "Od" => "left",
170 "Oe" => "clear",
171
172 "[2^" => "insert",
173 "[3^" => "delete",
174 "[5^" => "pageup",
175 "[6^" => "pagedown",
176 "[7^" => "home",
177 "[8^" => "end",
178 "[Z" => "tab",
180 _ => return None,
181 })
182}
183
184fn is_shift_key(code: &str) -> bool {
185 matches!(
186 code,
187 "[a" | "[b" | "[c" | "[d" | "[e" | "[2$" | "[3$" | "[5$" | "[6$" | "[7$" | "[8$" | "[Z"
188 )
189}
190
191fn is_ctrl_key(code: &str) -> bool {
192 matches!(
193 code,
194 "Oa" | "Ob" | "Oc" | "Od" | "Oe" | "[2^" | "[3^" | "[5^" | "[6^" | "[7^" | "[8^"
195 )
196}
197
198fn kitty_special_letter_key(terminator: char) -> Option<&'static str> {
201 Some(match terminator {
202 'A' => "up",
203 'B' => "down",
204 'C' => "right",
205 'D' => "left",
206 'E' => "clear",
207 'F' => "end",
208 'H' => "home",
209 'P' => "f1",
210 'Q' => "f2",
211 'R' => "f3",
212 'S' => "f4",
213 _ => return None,
214 })
215}
216
217fn kitty_special_number_key(number: u32) -> Option<&'static str> {
218 Some(match number {
219 2 => "insert",
220 3 => "delete",
221 5 => "pageup",
222 6 => "pagedown",
223 7 => "home",
224 8 => "end",
225 11 => "f1",
226 12 => "f2",
227 13 => "f3",
228 14 => "f4",
229 15 => "f5",
230 17 => "f6",
231 18 => "f7",
232 19 => "f8",
233 20 => "f9",
234 21 => "f10",
235 23 => "f11",
236 24 => "f12",
237 _ => return None,
238 })
239}
240
241fn kitty_codepoint_name(cp: u32) -> Option<&'static str> {
244 Some(match cp {
245 27 => "escape",
246 9 => "tab",
248 127 => "backspace",
249 8 => "backspace",
250 57358 => "capslock",
251 57359 => "scrolllock",
252 57360 => "numlock",
253 57361 => "printscreen",
254 57362 => "pause",
255 57363 => "menu",
256 57376 => "f13",
257 57377 => "f14",
258 57378 => "f15",
259 57379 => "f16",
260 57380 => "f17",
261 57381 => "f18",
262 57382 => "f19",
263 57383 => "f20",
264 57384 => "f21",
265 57385 => "f22",
266 57386 => "f23",
267 57387 => "f24",
268 57388 => "f25",
269 57389 => "f26",
270 57390 => "f27",
271 57391 => "f28",
272 57392 => "f29",
273 57393 => "f30",
274 57394 => "f31",
275 57395 => "f32",
276 57396 => "f33",
277 57397 => "f34",
278 57398 => "f35",
279 57399 => "kp0",
280 57400 => "kp1",
281 57401 => "kp2",
282 57402 => "kp3",
283 57403 => "kp4",
284 57404 => "kp5",
285 57405 => "kp6",
286 57406 => "kp7",
287 57407 => "kp8",
288 57408 => "kp9",
289 57409 => "kpdecimal",
290 57410 => "kpdivide",
291 57411 => "kpmultiply",
292 57412 => "kpsubtract",
293 57413 => "kpadd",
294 57414 => "kpenter",
295 57415 => "kpequal",
296 57416 => "kpseparator",
297 57417 => "kpleft",
298 57418 => "kpright",
299 57419 => "kpup",
300 57420 => "kpdown",
301 57421 => "kppageup",
302 57422 => "kppagedown",
303 57423 => "kphome",
304 57424 => "kpend",
305 57425 => "kpinsert",
306 57426 => "kpdelete",
307 57427 => "kpbegin",
308 57428 => "mediaplay",
309 57429 => "mediapause",
310 57430 => "mediaplaypause",
311 57431 => "mediareverse",
312 57432 => "mediastop",
313 57433 => "mediafastforward",
314 57434 => "mediarewind",
315 57435 => "mediatracknext",
316 57436 => "mediatrackprevious",
317 57437 => "mediarecord",
318 57438 => "lowervolume",
319 57439 => "raisevolume",
320 57440 => "mutevolume",
321 57441 => "leftshift",
322 57442 => "leftcontrol",
323 57443 => "leftalt",
324 57444 => "leftsuper",
325 57445 => "lefthyper",
326 57446 => "leftmeta",
327 57447 => "rightshift",
328 57448 => "rightcontrol",
329 57449 => "rightalt",
330 57450 => "rightsuper",
331 57451 => "righthyper",
332 57452 => "rightmeta",
333 57453 => "isoLevel3Shift",
334 57454 => "isoLevel5Shift",
335 _ => return None,
336 })
337}
338
339fn is_valid_codepoint(cp: u32) -> bool {
341 cp <= 0x10_ffff && !(0xd8_00..=0xdf_ff).contains(&cp)
342}
343
344fn safe_from_codepoint(cp: u32) -> String {
346 match char::from_u32(cp) {
347 Some(c) if is_valid_codepoint(cp) => c.to_string(),
348 _ => "?".to_string(),
349 }
350}
351
352fn resolve_event_type(value: u32) -> EventType {
353 match value {
354 3 => EventType::Release,
355 2 => EventType::Repeat,
356 _ => EventType::Press,
357 }
358}
359
360struct KittyModifiers {
362 ctrl: bool,
363 shift: bool,
364 meta: bool,
365 super_key: bool,
366 hyper: bool,
367 caps_lock: bool,
368 num_lock: bool,
369}
370
371fn parse_kitty_modifiers(modifiers: u32) -> KittyModifiers {
372 KittyModifiers {
373 ctrl: modifiers & KITTY_MODIFIERS.ctrl != 0,
374 shift: modifiers & KITTY_MODIFIERS.shift != 0,
375 meta: modifiers & (KITTY_MODIFIERS.meta | KITTY_MODIFIERS.alt) != 0,
376 super_key: modifiers & KITTY_MODIFIERS.super_key != 0,
377 hyper: modifiers & KITTY_MODIFIERS.hyper != 0,
378 caps_lock: modifiers & KITTY_MODIFIERS.caps_lock != 0,
379 num_lock: modifiers & KITTY_MODIFIERS.num_lock != 0,
380 }
381}
382
383#[allow(clippy::result_unit_err)]
390fn parse_kitty_keypress(s: &str) -> Option<Result<Key, ()>> {
391 let body = s.strip_prefix("\u{1b}[")?.strip_suffix('u')?;
393
394 let mut groups = body.split(';');
396 let codepoint_str = groups.next()?;
397 let codepoint: u32 = parse_all_digits(codepoint_str)?;
398
399 let (modifiers_raw, event_type_raw) = match groups.next() {
400 Some(seg) => {
401 let mut parts = seg.split(':');
403 let m = parse_all_digits(parts.next()?)?;
404 let e = match parts.next() {
405 Some(e_str) => Some(parse_all_digits(e_str)?),
406 None => None,
407 };
408 if parts.next().is_some() {
409 return None;
410 }
411 (Some(m), e)
412 }
413 None => (None, None),
414 };
415
416 let text_field = match groups.next() {
417 Some(t) => {
418 if t.is_empty() || !t.chars().all(|c| c.is_ascii_digit() || c == ':') {
420 return None;
421 }
422 Some(t)
423 }
424 None => None,
425 };
426 if groups.next().is_some() {
427 return None;
428 }
429
430 let modifiers = modifiers_raw.map_or(0, |m| m.saturating_sub(1));
431 let event_type = event_type_raw.unwrap_or(1);
432
433 if !is_valid_codepoint(codepoint) {
434 return Some(Err(()));
435 }
436
437 let mut text: Option<String> = match text_field {
439 Some(field) => {
440 let mut out = String::new();
441 for cp_str in field.split(':') {
442 let cp = parse_all_digits(cp_str)?;
443 out.push_str(&safe_from_codepoint(cp));
444 }
445 Some(out)
446 }
447 None => None,
448 };
449
450 let (name, is_printable) = if codepoint == 32 {
452 ("space".to_string(), true)
453 } else if codepoint == 13 {
454 ("return".to_string(), true)
455 } else if let Some(n) = kitty_codepoint_name(codepoint) {
456 (n.to_string(), false)
457 } else if (1..=26).contains(&codepoint) {
458 (
460 char::from_u32(codepoint + 96)
461 .map(String::from)
462 .unwrap_or_default(),
463 false,
464 )
465 } else {
466 (safe_from_codepoint(codepoint).to_lowercase(), true)
467 };
468
469 if is_printable && text.is_none() {
471 text = Some(safe_from_codepoint(codepoint));
472 }
473
474 let m = parse_kitty_modifiers(modifiers);
475 Some(Ok(Key {
476 name,
477 ctrl: m.ctrl,
478 meta: m.meta,
479 shift: m.shift,
480 super_key: m.super_key,
481 hyper: m.hyper,
482 caps_lock: m.caps_lock,
483 num_lock: m.num_lock,
484 event_type: Some(resolve_event_type(event_type)),
485 sequence: s.to_string(),
486 raw: Some(s.to_string()),
487 code: None,
488 is_kitty_protocol: true,
489 is_printable: Some(is_printable),
490 text,
491 }))
492}
493
494fn parse_kitty_special_key(s: &str) -> Option<Key> {
497 let body = s.strip_prefix("\u{1b}[")?;
499 let terminator = body.chars().last()?;
500 if !(terminator.is_ascii_alphabetic() || terminator == '~') {
501 return None;
502 }
503 let body = &body[..body.len() - terminator.len_utf8()];
504
505 let mut parts = body.split(';');
506 let number: u32 = parse_all_digits(parts.next()?)?;
507 let rest = parts.next()?;
508 if parts.next().is_some() {
509 return None;
510 }
511 let mut mod_event = rest.split(':');
512 let modifiers_raw: u32 = parse_all_digits(mod_event.next()?)?;
513 let event_type: u32 = parse_all_digits(mod_event.next()?)?;
514 if mod_event.next().is_some() {
515 return None;
516 }
517
518 let modifiers = modifiers_raw.saturating_sub(1);
519
520 let name = if terminator == '~' {
521 kitty_special_number_key(number)?
522 } else {
523 kitty_special_letter_key(terminator)?
524 };
525
526 let m = parse_kitty_modifiers(modifiers);
527 Some(Key {
528 name: name.to_string(),
529 ctrl: m.ctrl,
530 meta: m.meta,
531 shift: m.shift,
532 super_key: m.super_key,
533 hyper: m.hyper,
534 caps_lock: m.caps_lock,
535 num_lock: m.num_lock,
536 event_type: Some(resolve_event_type(event_type)),
537 sequence: s.to_string(),
538 raw: Some(s.to_string()),
539 code: None,
540 is_kitty_protocol: true,
541 is_printable: Some(false),
542 text: None,
543 })
544}
545
546fn parse_all_digits(s: &str) -> Option<u32> {
550 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
551 return None;
552 }
553 s.parse::<u32>().ok()
554}
555
556pub fn parse_keypress(bytes: &[u8]) -> Key {
559 let decoded: String = if bytes.len() == 1 && bytes[0] > 127 {
561 let mut s = String::from('\u{1b}');
562 s.push_str(&String::from_utf8_lossy(&[bytes[0] - 128]));
563 s
564 } else {
565 String::from_utf8_lossy(bytes).into_owned()
566 };
567 parse_keypress_str(&decoded)
568}
569
570fn parse_keypress_str(s: &str) -> Key {
572 match parse_kitty_keypress(s) {
574 Some(Ok(key)) => return key,
575 Some(Err(())) => {
576 return Key {
579 name: String::new(),
580 ctrl: false,
581 meta: false,
582 shift: false,
583 sequence: s.to_string(),
584 raw: Some(s.to_string()),
585 code: None,
586 super_key: false,
587 hyper: false,
588 caps_lock: false,
589 num_lock: false,
590 event_type: None,
591 is_kitty_protocol: true,
592 is_printable: Some(false),
593 text: None,
594 };
595 }
596 None => {}
597 }
598
599 if let Some(key) = parse_kitty_special_key(s) {
600 return key;
601 }
602
603 let mut key = Key::legacy(s.to_string());
604
605 let chars: Vec<char> = s.chars().collect();
609
610 if s == "\r" || s == "\u{1b}\r" {
611 key.raw = None;
613 key.name = "return".to_string();
614 key.meta = chars.len() == 2;
615 } else if s == "\n" {
616 key.name = "enter".to_string();
618 } else if s == "\t" {
619 key.name = "tab".to_string();
620 } else if s == "\u{8}" || s == "\u{1b}\u{8}" {
621 key.name = "backspace".to_string();
623 key.meta = chars.first() == Some(&'\u{1b}');
624 } else if s == "\u{7f}" || s == "\u{1b}\u{7f}" {
625 key.name = "backspace".to_string();
627 key.meta = chars.first() == Some(&'\u{1b}');
628 } else if s == "\u{1b}" || s == "\u{1b}\u{1b}" {
629 key.name = "escape".to_string();
631 key.meta = chars.len() == 2;
632 } else if s == " " || s == "\u{1b} " {
633 key.name = "space".to_string();
634 key.meta = chars.len() == 2;
635 } else if chars.len() == 1 && chars[0] <= '\u{1a}' {
636 let c = chars[0] as u32;
638 key.name = char::from_u32(c + ('a' as u32) - 1)
639 .map(String::from)
640 .unwrap_or_default();
641 key.ctrl = true;
642 } else if chars.len() == 1 && chars[0].is_ascii_digit() {
643 key.name = "number".to_string();
645 } else if chars.len() == 1 && chars[0].is_ascii_lowercase() {
646 key.name = chars[0].to_string();
648 } else if chars.len() == 1 && chars[0].is_ascii_uppercase() {
649 key.name = chars[0].to_ascii_lowercase().to_string();
651 key.shift = true;
652 } else if let Some((name, meta, shift)) = match_meta_key_code(s) {
653 key.name = name;
655 key.meta = meta;
656 key.shift = shift;
657 } else if let Some(parsed) = match_fn_key(&chars) {
658 if chars.first() == Some(&'\u{1b}') && chars.get(1) == Some(&'\u{1b}') {
659 key.meta = true;
660 }
661 let modifier = parsed.modifier;
662 key.ctrl = modifier & 4 != 0;
663 key.meta = key.meta || (modifier & 10 != 0);
664 key.shift = modifier & 1 != 0;
665 key.code = Some(parsed.code.clone());
666 key.name = key_name(&parsed.code).unwrap_or("").to_string();
667 key.shift = is_shift_key(&parsed.code) || key.shift;
668 key.ctrl = is_ctrl_key(&parsed.code) || key.ctrl;
669 }
670
671 key
672}
673
674fn match_meta_key_code(s: &str) -> Option<(String, bool, bool)> {
676 let inner = s.strip_prefix('\u{1b}')?;
677 let mut it = inner.chars();
678 let c = it.next()?;
679 if it.next().is_some() {
680 return None;
681 }
682 if !c.is_ascii_alphanumeric() {
683 return None;
684 }
685 let name = c.to_ascii_lowercase().to_string();
686 let shift = c.is_ascii_uppercase();
687 Some((name, true, shift))
688}
689
690struct FnKeyMatch {
691 code: String,
692 modifier: i64,
697}
698
699fn match_fn_key(chars: &[char]) -> Option<FnKeyMatch> {
704 let mut i = 0;
705 if chars.get(i) != Some(&'\u{1b}') {
707 return None;
708 }
709 while chars.get(i) == Some(&'\u{1b}') {
710 i += 1;
711 }
712
713 let prefix: String;
715 if chars.get(i) == Some(&'[') {
716 if chars.get(i + 1) == Some(&'[') {
717 prefix = "[[".to_string();
718 i += 2;
719 } else {
720 prefix = "[".to_string();
721 i += 1;
722 }
723 } else if chars.get(i) == Some(&'O') {
724 prefix = "O".to_string();
725 i += 1;
726 } else if chars.get(i) == Some(&'N') {
727 prefix = "N".to_string();
728 i += 1;
729 } else {
730 return None;
731 }
732
733 let rest = &chars[i..];
736
737 if let Some(m) = match_fn_branch_a(&prefix, rest) {
739 return Some(m);
740 }
741 match_fn_branch_b(&prefix, rest)
742}
743
744fn match_fn_branch_a(prefix: &str, rest: &[char]) -> Option<FnKeyMatch> {
746 let mut j = 0;
747 let num_start = j;
749 while rest.get(j).is_some_and(|c| c.is_ascii_digit()) {
750 j += 1;
751 }
752 if j == num_start {
753 return None;
754 }
755 let p2: String = rest[num_start..j].iter().collect();
756
757 let mut p3: Option<String> = None;
759 if rest.get(j) == Some(&';') {
760 let mut k = j + 1;
761 let s = k;
762 while rest.get(k).is_some_and(|c| c.is_ascii_digit()) {
763 k += 1;
764 }
765 if k > s {
766 p3 = Some(rest[s..k].iter().collect());
767 j = k;
768 }
769 }
772
773 let term = rest.get(j)?;
775 if !matches!(term, '~' | '^' | '$') {
776 return None;
777 }
778
779 let code = format!("{prefix}{p2}{term}");
780 let modifier = i64::from(p3.as_deref().and_then(parse_all_digits).unwrap_or(1)) - 1;
781 Some(FnKeyMatch { code, modifier })
782}
783
784fn match_fn_branch_b(prefix: &str, rest: &[char]) -> Option<FnKeyMatch> {
786 let mut j = 0;
787 if rest.get(j) == Some(&'1') && rest.get(j + 1) == Some(&';') {
789 j += 2;
790 }
791 let s = j;
793 while rest.get(j).is_some_and(|c| c.is_ascii_digit()) {
794 j += 1;
795 }
796 let p5: Option<String> = if j > s {
797 Some(rest[s..j].iter().collect())
798 } else {
799 None
800 };
801 let letter = rest.get(j)?;
803 if !letter.is_ascii_alphabetic() {
804 return None;
805 }
806
807 let code = format!("{prefix}{letter}");
810 let modifier = i64::from(p5.as_deref().and_then(parse_all_digits).unwrap_or(1)) - 1;
811 Some(FnKeyMatch { code, modifier })
812}
813
814#[cfg(test)]
815mod tests;