1use std::time::{Duration, Instant};
4
5use omp_core::Str;
6use xutf::{IntoUnicodeNormalized, Text};
7
8use crate::rich::cell_width;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
14pub enum Key {
15 Up,
17 Down,
19 Left,
21 Right,
23 Tab,
25 BackTab,
27 Enter,
29 Space,
31 Esc,
33 Backspace,
35 Delete,
37 Insert,
39 Home,
41 End,
43 PageUp,
45 PageDown,
47 Function(u8),
49 Ctrl(char),
52 Alt(char),
58 CtrlAlt(char),
61 ShiftEnter,
63 WordLeft,
65 WordRight,
67 WordDelete,
69 Paste,
72 PasteRaw,
76 Char(char),
78}
79
80const INPUT_TIMEOUT: Duration = Duration::from_millis(75);
81const PARTIAL_HOLD_TIMEOUT: Duration = Duration::from_millis(150);
82const PASTE_INACTIVITY_TIMEOUT: Duration = Duration::from_millis(1000);
83const KITTY_DEDUP_TIMEOUT: Duration = Duration::from_millis(25);
84const MAX_CSI_BYTES: usize = 4096;
85const MAX_STRING_SEQ_BYTES: usize = 16 * 1024 * 1024;
86const MAX_PASTE_BYTES: usize = 64 * 1024 * 1024;
87const PASTE_END: &[u8] = b"\x1b[201~";
88
89#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
91pub enum TerminalResponse {
92 DeviceAttributes(Str),
94 ModeReport {
96 mode: u16,
98 status: u8,
100 },
101 DeviceStatus(Str),
103 KittyKeyboardFlags(u8),
105 Osc(Str),
107 KittyGraphics(Str),
109 DeviceControlString(Str),
111 OscColor {
113 index: u8,
115 r: u16,
117 g: u16,
119 b: u16,
121 },
122 AppearanceChanged(u8),
124 InBandResize {
126 rows: u16,
128 cols: u16,
130 x_px: u16,
132 y_px: u16,
134 },
135 ApplicationProgramCommand(Str),
137}
138#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
140pub enum MouseButton {
141 Left,
143 Middle,
145 Right,
147 WheelUp,
149 WheelDown,
151 WheelLeft,
153 WheelRight,
155 None,
157}
158
159#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
161pub struct Mods {
162 pub shift: bool,
164 pub alt: bool,
166 pub ctrl: bool,
168 pub super_key: bool,
170 pub hyper: bool,
172 pub meta: bool,
174}
175
176#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
178pub struct MouseReport {
179 pub kind: Mouse,
181 pub col: u16,
183 pub row: u16,
185 pub button: MouseButton,
187 pub mods: Mods,
189 pub pressed: bool,
191}
192
193#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
195pub enum InputEvent {
196 Key(Key),
198 Mouse(MouseReport),
200 Paste(Str),
202 Focus(bool),
204 Response(TerminalResponse),
206}
207
208#[derive(Default)]
214pub struct InputDecoder {
215 keymap: Keymap,
216 buffer: Vec<u8>,
217 incomplete_since: Option<Instant>,
218 kitty_keyboard_active: bool,
219 pending_kitty_print: Option<(u32, Instant)>,
220 paste: Vec<u8>,
221 paste_active: bool,
222 paste_last_input: Option<Instant>,
223 paste_scan_from: usize,
224}
225
226impl InputDecoder {
227 pub fn new() -> Self {
229 Self {
230 keymap: Keymap::default(),
231 buffer: Vec::new(),
232 incomplete_since: None,
233 kitty_keyboard_active: false,
234 pending_kitty_print: None,
235 paste: Vec::new(),
236 paste_active: false,
237 paste_last_input: None,
238 paste_scan_from: 0,
239 }
240 }
241
242 pub const fn keymap(&self) -> &Keymap {
244 &self.keymap
245 }
246
247 pub const fn keymap_mut(&mut self) -> &mut Keymap {
251 &mut self.keymap
252 }
253
254 pub const fn set_kitty_keyboard(&mut self, active: bool) {
259 self.kitty_keyboard_active = active;
260 }
261
262 pub fn feed(&mut self, bytes: &[u8], now: Instant, out: &mut Vec<InputEvent>) {
265 self.expire_paste(now, out);
266 self.expire_partial(now, out);
267 if self.paste_active {
268 self.paste.extend_from_slice(bytes);
269 self.paste_last_input = Some(now);
270 self.process_paste(now, out);
271 return;
272 }
273 self.buffer.extend_from_slice(bytes);
274 self.process_buffer(now, out);
275 }
276
277 pub fn tick(&mut self, now: Instant, out: &mut Vec<InputEvent>) {
279 self.expire_paste(now, out);
280 self.expire_partial(now, out);
281 }
282
283 pub fn deadline(&self) -> Option<Instant> {
287 [
288 self.incomplete_since.map(|at| at + INPUT_TIMEOUT),
289 self
290 .paste_last_input
291 .map(|at| at + PASTE_INACTIVITY_TIMEOUT),
292 self
293 .pending_kitty_print
294 .map(|(_, at)| at + KITTY_DEDUP_TIMEOUT),
295 ]
296 .into_iter()
297 .flatten()
298 .min()
299 }
300
301 fn process_buffer(&mut self, now: Instant, out: &mut Vec<InputEvent>) {
302 loop {
303 if self.buffer.is_empty() {
304 self.incomplete_since = None;
305 return;
306 }
307 if self.buffer.starts_with(b"\x1b\x1b[<") {
308 self.emit(Decoded::Chord(Chord::plain(Key::Esc)), now, out);
309 self.buffer.drain(..1);
310 continue;
311 }
312 let resolution = resolve_frame(&self.buffer);
313 match resolution {
314 FrameResolution::Incomplete => {
315 self.incomplete_since.get_or_insert(now);
316 return;
317 },
318 FrameResolution::Overflow(length) => {
319 if !emit_unterminated_response(&self.buffer[..length], out) {
320 emit_raw(&self.buffer[..length], &self.keymap, out);
321 }
322 self.pending_kitty_print = None;
323 self.buffer.drain(..length);
324 self.incomplete_since = None;
325 },
326 FrameResolution::Complete(length) => {
327 let decoded = decode_frame(&self.buffer[..length]);
328 self.buffer.drain(..length);
329 self.incomplete_since = None;
330 if matches!(decoded, Decoded::PasteStart) {
331 self.paste_active = true;
332 self.paste_last_input = Some(now);
333 self.paste_scan_from = 0;
334 self.paste.clear();
335 self.paste.append(&mut self.buffer);
336 self.process_paste(now, out);
337 if self.paste_active {
338 return;
339 }
340 } else {
341 self.emit(decoded, now, out);
342 }
343 },
344 }
345 }
346 }
347
348 fn process_paste(&mut self, now: Instant, out: &mut Vec<InputEvent>) {
349 let start = self.paste_scan_from.saturating_sub(PASTE_END.len() - 1);
350 let end = self.paste[start..]
351 .windows(PASTE_END.len())
352 .position(|window| window == PASTE_END)
353 .map(|offset| start + offset);
354 if let Some(end) = end {
355 let remaining = self.paste.split_off(end + PASTE_END.len());
356 self.paste.truncate(end);
357 self.finish_paste(out);
358 self.buffer = remaining;
359 self.process_buffer(now, out);
360 return;
361 }
362 self.paste_scan_from = self.paste.len();
363 if self.paste.len() > MAX_PASTE_BYTES {
364 self.finish_paste(out);
365 }
366 }
367
368 fn finish_paste(&mut self, out: &mut Vec<InputEvent>) {
369 let bytes = std::mem::take(&mut self.paste);
370 let decoded = decode_reencoded_paste_controls(&bytes);
371 out.push(InputEvent::Paste(Str::from(sanitize_paste(&decoded))));
372 self.paste_active = false;
373 self.paste_last_input = None;
374 self.paste_scan_from = 0;
375 self.pending_kitty_print = None;
376 }
377
378 fn expire_paste(&mut self, now: Instant, out: &mut Vec<InputEvent>) {
379 if self.paste_active
380 && self
381 .paste_last_input
382 .is_some_and(|last| now.saturating_duration_since(last) >= PASTE_INACTIVITY_TIMEOUT)
383 {
384 self.finish_paste(out);
385 }
386 }
387
388 fn expire_partial(&mut self, now: Instant, out: &mut Vec<InputEvent>) {
389 let Some(since) = self.incomplete_since else {
390 return;
391 };
392 let extended = self.kitty_keyboard_active || is_sgr_mouse_partial(&self.buffer);
393 let timeout = if extended {
394 INPUT_TIMEOUT + PARTIAL_HOLD_TIMEOUT
395 } else {
396 INPUT_TIMEOUT
397 };
398 if now.saturating_duration_since(since) < timeout {
399 return;
400 }
401 let buffered = std::mem::take(&mut self.buffer);
402 self.incomplete_since = None;
403 self.pending_kitty_print = None;
404 if !emit_unterminated_response(&buffered, out) {
405 if buffered == b"\x1b\x1b" {
406 emit_chord(&self.keymap, Chord::plain(Key::Esc), out);
407 emit_chord(&self.keymap, Chord::plain(Key::Esc), out);
408 } else {
409 emit_raw(&buffered, &self.keymap, out);
410 }
411 }
412 }
413
414 fn emit(&mut self, decoded: Decoded, now: Instant, out: &mut Vec<InputEvent>) {
415 let (event, chord, kitty_printable) = match decoded {
416 Decoded::Event(event) => (Some(event), None, false),
417 Decoded::Chord(chord) => (None, Some(chord), false),
418 Decoded::KittyChord(chord) => (None, Some(chord), true),
419 Decoded::PasteStart | Decoded::None => return,
420 };
421 let printable = chord.and_then(chord_printable_codepoint);
422 if printable.is_some_and(|printable| {
423 self.pending_kitty_print.is_some_and(|(codepoint, at)| {
424 printable == codepoint && now.saturating_duration_since(at) <= KITTY_DEDUP_TIMEOUT
425 })
426 }) {
427 self.pending_kitty_print = None;
428 return;
429 }
430 self.pending_kitty_print = if kitty_printable {
431 printable.map(|codepoint| (codepoint, now))
432 } else {
433 None
434 };
435 if let Some(InputEvent::Response(TerminalResponse::KittyKeyboardFlags(flags))) = event {
436 self.kitty_keyboard_active = flags != 0;
437 out.push(InputEvent::Response(TerminalResponse::KittyKeyboardFlags(flags)));
438 } else if let Some(event) = event {
439 out.push(event);
440 } else if let Some(chord) = chord {
441 emit_chord(&self.keymap, chord, out);
442 }
443 }
444}
445
446#[derive(Clone, Debug)]
447enum Decoded {
448 Event(InputEvent),
449 Chord(Chord),
450 KittyChord(Chord),
451 PasteStart,
452 None,
453}
454
455enum FrameResolution {
456 Complete(usize),
457 Incomplete,
458 Overflow(usize),
459}
460
461fn resolve_frame(bytes: &[u8]) -> FrameResolution {
462 if bytes[0] != 0x1b {
463 let width = utf8_width(bytes[0]);
464 return if width > bytes.len() {
465 FrameResolution::Incomplete
466 } else {
467 FrameResolution::Complete(width.max(1))
468 };
469 }
470 if bytes.len() == 1 {
471 return FrameResolution::Incomplete;
472 }
473 if bytes[1] == 0x1b {
474 if bytes.len() == 2 {
475 return FrameResolution::Incomplete;
476 }
477 if matches!(bytes[2], b'[' | b'O') {
478 return match resolve_escape(&bytes[1..]) {
479 FrameResolution::Complete(length) => FrameResolution::Complete(length + 1),
480 FrameResolution::Overflow(length) => FrameResolution::Overflow(length + 1),
481 FrameResolution::Incomplete => FrameResolution::Incomplete,
482 };
483 }
484 return FrameResolution::Complete(1);
485 }
486 resolve_escape(bytes)
487}
488
489fn resolve_escape(bytes: &[u8]) -> FrameResolution {
490 match bytes[1] {
491 b'[' => {
492 if bytes.len() < 3 {
493 return FrameResolution::Incomplete;
494 }
495 if bytes[2] == b'M' {
496 return if bytes.len() >= 6 {
497 FrameResolution::Complete(6)
498 } else {
499 FrameResolution::Incomplete
500 };
501 }
502 let limit = bytes.len().min(MAX_CSI_BYTES);
503 let sgr = bytes[2] == b'<';
504 for index in 2..limit {
505 if (0x40..=0x7e).contains(&bytes[index]) {
506 if !sgr {
507 return FrameResolution::Complete(index + 1);
508 }
509 if matches!(bytes[index], b'M' | b'm') && valid_sgr_body(&bytes[2..=index]) {
510 return FrameResolution::Complete(index + 1);
511 }
512 }
513 }
514 if bytes.len() >= MAX_CSI_BYTES {
515 FrameResolution::Overflow(MAX_CSI_BYTES)
516 } else {
517 FrameResolution::Incomplete
518 }
519 },
520 b']' | b'P' | b'_' => {
521 let limit = bytes.len().min(MAX_STRING_SEQ_BYTES);
522 for index in 2..limit {
523 if bytes[1] == b']' && bytes[index] == 0x07 {
524 return FrameResolution::Complete(index + 1);
525 }
526 if bytes[index] == 0x1b && index + 1 < limit && bytes[index + 1] == b'\\' {
527 return FrameResolution::Complete(index + 2);
528 }
529 }
530 if bytes.len() >= MAX_STRING_SEQ_BYTES {
531 FrameResolution::Overflow(MAX_STRING_SEQ_BYTES)
532 } else {
533 FrameResolution::Incomplete
534 }
535 },
536 b'O' => {
537 if bytes.len() >= 3 {
538 FrameResolution::Complete(3)
539 } else {
540 FrameResolution::Incomplete
541 }
542 },
543 _ => {
544 let width = utf8_width(bytes[1]);
545 if bytes.len() > width {
546 FrameResolution::Complete(width + 1)
547 } else {
548 FrameResolution::Incomplete
549 }
550 },
551 }
552}
553
554fn decode_frame(bytes: &[u8]) -> Decoded {
555 if bytes[0] != 0x1b {
556 return decode_plain(bytes, false).map_or(Decoded::None, Decoded::Chord);
557 }
558 if bytes == b"\x1b" {
559 return Decoded::Chord(Chord::plain(Key::Esc));
560 }
561 let (sequence, meta) = if bytes.starts_with(b"\x1b\x1b") {
562 (&bytes[1..], true)
563 } else {
564 (bytes, false)
565 };
566 match sequence.get(1) {
567 Some(b'[') => {
568 decode_csi(&sequence[2..sequence.len() - 1], sequence[sequence.len() - 1], meta)
569 },
570 Some(b'O') => {
571 let key = match sequence[2] {
572 b'A' => Some(Key::Up),
573 b'B' => Some(Key::Down),
574 b'C' => Some(Key::Right),
575 b'D' => Some(Key::Left),
576 b'H' => Some(Key::Home),
577 b'F' => Some(Key::End),
578 b'P'..=b'S' => Some(Key::Function(sequence[2] - b'P' + 1)),
579 _ => None,
580 };
581 key.map_or(Decoded::None, |key| {
582 Decoded::Chord(Chord::with_modifiers(key, u32::from(meta) * 2))
583 })
584 },
585 Some(b']') => {
586 let end = if sequence.ends_with(b"\x1b\\") {
587 sequence.len() - 2
588 } else {
589 sequence.len() - 1
590 };
591 let payload = &sequence[2..end];
592 if let Some((index, r, g, b)) = parse_osc_color(payload) {
593 Decoded::Event(InputEvent::Response(TerminalResponse::OscColor { index, r, g, b }))
594 } else {
595 Decoded::Event(InputEvent::Response(TerminalResponse::Osc(decode_text(payload))))
596 }
597 },
598 Some(b'P') => {
599 let end = sequence.len().saturating_sub(2);
600 Decoded::Event(InputEvent::Response(TerminalResponse::DeviceControlString(decode_text(
601 &sequence[2..end],
602 ))))
603 },
604 Some(b'_') => {
605 let end = sequence.len().saturating_sub(2);
606 let payload = &sequence[2..end];
607 if let Some(payload) = payload.strip_prefix(b"G") {
608 Decoded::Event(InputEvent::Response(TerminalResponse::KittyGraphics(decode_text(
609 payload,
610 ))))
611 } else {
612 Decoded::Event(InputEvent::Response(TerminalResponse::ApplicationProgramCommand(
613 decode_text(payload),
614 )))
615 }
616 },
617 _ => decode_plain(&sequence[1..], true).map_or(Decoded::None, Decoded::Chord),
618 }
619}
620
621fn decode_csi(body: &[u8], final_byte: u8, meta: bool) -> Decoded {
622 if final_byte == b'c' && matches!(body.first(), Some(b'?' | b'>')) {
623 return Decoded::Event(InputEvent::Response(TerminalResponse::DeviceAttributes(
624 decode_text(body),
625 )));
626 }
627 if final_byte == b'y'
628 && let Some(fields) = body
629 .strip_prefix(b"?")
630 .and_then(|body| body.strip_suffix(b"$"))
631 {
632 let mut fields = fields.split(|byte| *byte == b';');
633 if let (Some(mode), Some(status)) =
634 (fields.next().and_then(parse_decimal_u16), fields.next().and_then(parse_decimal_u8))
635 {
636 return Decoded::Event(InputEvent::Response(TerminalResponse::ModeReport {
637 mode,
638 status,
639 }));
640 }
641 }
642 if final_byte == b'u'
643 && let Some(flags) = body.strip_prefix(b"?").and_then(parse_decimal_u8)
644 {
645 return Decoded::Event(InputEvent::Response(TerminalResponse::KittyKeyboardFlags(flags)));
646 }
647 if final_byte == b'n'
648 && let Some(appearance) = parse_appearance_response(body)
649 {
650 return Decoded::Event(InputEvent::Response(TerminalResponse::AppearanceChanged(appearance)));
651 }
652 if final_byte == b't'
653 && let Some((rows, cols, x_px, y_px)) = parse_in_band_resize(body)
654 {
655 return Decoded::Event(InputEvent::Response(TerminalResponse::InBandResize {
656 rows,
657 cols,
658 x_px,
659 y_px,
660 }));
661 }
662 if matches!(final_byte, b'n' | b'R') {
663 return Decoded::Event(InputEvent::Response(TerminalResponse::DeviceStatus(decode_text(
664 body,
665 ))));
666 }
667 if body.is_empty() && matches!(final_byte, b'I' | b'O') {
668 return Decoded::Event(InputEvent::Focus(final_byte == b'I'));
669 }
670 if final_byte == b'~' && body == b"200" {
671 return Decoded::PasteStart;
672 }
673 if body.starts_with(b"<") && matches!(final_byte, b'M' | b'm') {
674 return decode_sgr_mouse(body, final_byte);
675 }
676 if final_byte == b'u' {
677 return decode_kitty_key(body, meta);
678 }
679 if final_byte == b'~' {
680 return decode_tilde_key(body, meta);
681 }
682 let mut fields = body.split(|byte| *byte == b';');
683 let first = fields.next().unwrap_or_default();
684 let modifiers = if first == b"1" {
685 fields.next().and_then(parse_modifier).unwrap_or(0)
686 } else {
687 0
688 } | if meta { 2 } else { 0 };
689 let key = match final_byte {
690 b'A' => Some(Key::Up),
691 b'B' => Some(Key::Down),
692 b'C' => Some(Key::Right),
693 b'D' => Some(Key::Left),
694 b'H' => Some(Key::Home),
695 b'F' => Some(Key::End),
696 b'Z' => Some(Key::Tab),
697 _ => None,
698 };
699 let modifiers = modifiers | u32::from(final_byte == b'Z');
700 key.map_or(Decoded::None, |key| Decoded::Chord(Chord::with_modifiers(key, modifiers)))
701}
702
703fn decode_kitty_key(body: &[u8], meta: bool) -> Decoded {
704 let mut fields = body.split(|byte| *byte == b';');
705 let codepoints = fields.next().unwrap_or_default();
706 let mut codepoints = codepoints.split(|byte| *byte == b':');
707 let primary = codepoints.next().and_then(parse_decimal).unwrap_or(0);
708 let shifted = codepoints.next().and_then(parse_decimal);
709 let modifier_field = fields.next().unwrap_or(b"1");
710 let mut modifier_parts = modifier_field.split(|byte| *byte == b':');
711 let mut modifiers = modifier_parts.next().and_then(parse_modifier).unwrap_or(0);
712 if meta {
713 modifiers |= 0b0000_0010;
714 }
715 let event_type = modifier_parts
716 .next()
717 .and_then(parse_decimal_u8)
718 .unwrap_or(1);
719 if event_type == 3 {
720 return Decoded::None;
721 }
722 let codepoint = if modifiers & 0b0000_0001 != 0 {
723 shifted.unwrap_or(primary)
724 } else {
725 primary
726 };
727 let Some(chord) = chord_from_codepoint(codepoint, modifiers) else {
728 return Decoded::None;
729 };
730 let printable = modifiers == 0 && codepoint >= 32;
731 if printable {
732 Decoded::KittyChord(chord)
733 } else {
734 Decoded::Chord(chord)
735 }
736}
737
738fn chord_printable_codepoint(chord: Chord) -> Option<u32> {
739 match chord.key {
740 Key::Char(character) => Some(u32::from(character)),
741 Key::Space => Some(32),
742 _ => None,
743 }
744}
745
746fn decode_tilde_key(body: &[u8], meta: bool) -> Decoded {
747 let mut fields = body.split(|byte| *byte == b';');
748 let first = fields.next().unwrap_or_default();
749 let second = fields.next();
750 let third = fields.next();
751 if first == b"27"
752 && let (Some(modifiers), Some(codepoint), None) = (second, third, fields.next())
753 {
754 let modifiers = parse_modifier(modifiers).unwrap_or(0) | if meta { 2 } else { 0 };
755 let codepoint = parse_decimal(codepoint).unwrap_or(0);
756 return chord_from_codepoint(codepoint, modifiers).map_or(Decoded::None, Decoded::Chord);
757 }
758 let number = parse_decimal_u8(first).unwrap_or(0);
759 let modifiers = second.and_then(parse_modifier).unwrap_or(0) | if meta { 2 } else { 0 };
760 let key = match number {
761 1 | 7 => Some(Key::Home),
762 2 => Some(Key::Insert),
763 3 => Some(Key::Delete),
764 4 | 8 => Some(Key::End),
765 5 => Some(Key::PageUp),
766 6 => Some(Key::PageDown),
767 11..=15 => Some(Key::Function(number - 10)),
768 17..=21 => Some(Key::Function(number - 11)),
769 23 | 24 => Some(Key::Function(number - 12)),
770 _ => None,
771 };
772 key.map(|key| Chord::with_modifiers(key, modifiers))
773 .map_or(Decoded::None, Decoded::Chord)
774}
775
776fn decode_sgr_mouse(body: &[u8], final_byte: u8) -> Decoded {
777 let mut fields = body[1..].split(|byte| *byte == b';');
778 let Some(bits) = fields.next().and_then(parse_decimal_u16) else {
779 return Decoded::None;
780 };
781 let Some(column) = fields.next().and_then(parse_decimal_u16) else {
782 return Decoded::None;
783 };
784 let Some(row) = fields.next().and_then(parse_decimal_u16) else {
785 return Decoded::None;
786 };
787 let button = if bits & 0b0100_0000 != 0 {
788 match bits & 0b0000_0011 {
789 0 => MouseButton::WheelUp,
790 1 => MouseButton::WheelDown,
791 2 => MouseButton::WheelLeft,
792 _ => MouseButton::WheelRight,
793 }
794 } else {
795 match bits & 0b0000_0011 {
796 0 => MouseButton::Left,
797 1 => MouseButton::Middle,
798 2 => MouseButton::Right,
799 _ => MouseButton::None,
800 }
801 };
802 let kind = match button {
803 MouseButton::WheelUp => Mouse::WheelUp,
804 MouseButton::WheelDown => Mouse::WheelDown,
805 MouseButton::WheelLeft => Mouse::WheelLeft,
806 MouseButton::WheelRight => Mouse::WheelRight,
807 _ if final_byte == b'm' => Mouse::Release,
808 MouseButton::None if bits & 0b0010_0000 != 0 => Mouse::Move,
809 _ if bits & 0b0010_0000 != 0 => Mouse::Drag,
810 MouseButton::Left => Mouse::Click,
811 MouseButton::Middle => Mouse::MiddleClick,
812 MouseButton::Right => Mouse::RightClick,
813 MouseButton::None => Mouse::Move,
814 };
815 Decoded::Event(InputEvent::Mouse(MouseReport {
816 kind,
817 col: column.saturating_sub(1),
818 row: row.saturating_sub(1),
819 button,
820 mods: Mods {
821 shift: bits & 0b0000_0100 != 0,
822 alt: bits & 0b0000_1000 != 0,
823 ctrl: bits & 0b0001_0000 != 0,
824 ..Mods::default()
825 },
826 pressed: final_byte == b'M',
827 }))
828}
829
830fn chord_from_codepoint(codepoint: u32, modifiers: u32) -> Option<Chord> {
831 let key = match codepoint {
832 57344 | 27 => Some(Key::Esc),
833 57345 | 10 | 13 => Some(Key::Enter),
834 57346 | 9 => Some(Key::Tab),
835 57347 | 127 => Some(Key::Backspace),
836 57348 => Some(Key::Insert),
837 57349 => Some(Key::Delete),
838 57350 => Some(Key::Left),
839 57351 => Some(Key::Right),
840 57352 => Some(Key::Up),
841 57353 => Some(Key::Down),
842 57354 => Some(Key::PageUp),
843 57355 => Some(Key::PageDown),
844 57356 => Some(Key::Home),
845 57357 => Some(Key::End),
846 57364..=57375 => Some(Key::Function(u8::try_from(codepoint - 57363).ok()?)),
847 _ => character_to_key(char::from_u32(codepoint)?),
848 }?;
849 Some(Chord::with_modifiers(key, modifiers))
850}
851
852fn decode_plain(bytes: &[u8], alt: bool) -> Option<Chord> {
853 let mut chord = if bytes[0] < 0x20 || bytes[0] == 0x7f {
854 decode_control(bytes[0])?
855 } else {
856 let character = std::str::from_utf8(bytes).ok()?.chars().next()?;
857 Chord::plain(character_to_key(character)?)
858 };
859 chord.mods.alt |= alt;
860 Some(chord)
861}
862
863fn decode_control(byte: u8) -> Option<Chord> {
864 let chord = match byte {
865 b'\t' => Chord::plain(Key::Tab),
866 b'\r' | b'\n' => Chord::plain(Key::Enter),
867 0x7f | 0x08 => Chord::plain(Key::Backspace),
868 0x01..=0x1a => {
869 Chord::new(Key::Char(char::from(b'a' + byte - 1)), Mods { ctrl: true, ..Mods::default() })
870 },
871 0x1b => Chord::plain(Key::Esc),
872 _ => return None,
873 };
874 Some(chord)
875}
876
877const fn character_to_key(character: char) -> Option<Key> {
878 match character {
879 ' ' => Some(Key::Space),
880 '\r' | '\n' => Some(Key::Enter),
881 _ if !character.is_control() => Some(Key::Char(character)),
882 _ => None,
883 }
884}
885
886const fn utf8_width(byte: u8) -> usize {
887 match byte {
888 0x00..=0x7f => 1,
889 0xc2..=0xdf => 2,
890 0xe0..=0xef => 3,
891 0xf0..=0xf4 => 4,
892 _ => 1,
893 }
894}
895
896fn valid_sgr_body(body: &[u8]) -> bool {
897 let Some(body) = body.strip_prefix(b"<") else {
898 return false;
899 };
900 let Some(body) = body.strip_suffix(b"M").or_else(|| body.strip_suffix(b"m")) else {
901 return false;
902 };
903 let mut fields = body.split(|byte| *byte == b';');
904 (0..3).all(|_| fields.next().and_then(parse_decimal).is_some()) && fields.next().is_none()
905}
906
907fn is_sgr_mouse_partial(bytes: &[u8]) -> bool {
908 bytes.starts_with(b"\x1b[<")
909 && bytes[3..]
910 .iter()
911 .all(|byte| byte.is_ascii_digit() || *byte == b';')
912}
913
914fn parse_modifier(bytes: &[u8]) -> Option<u32> {
915 parse_decimal(bytes).map(|modifier| modifier.saturating_sub(1))
916}
917
918fn parse_decimal(bytes: &[u8]) -> Option<u32> {
919 (!bytes.is_empty() && bytes.iter().all(u8::is_ascii_digit))
920 .then(|| std::str::from_utf8(bytes).ok()?.parse().ok())
921 .flatten()
922}
923
924fn parse_decimal_u16(bytes: &[u8]) -> Option<u16> {
925 parse_decimal(bytes).and_then(|number| u16::try_from(number).ok())
926}
927
928fn parse_decimal_u8(bytes: &[u8]) -> Option<u8> {
929 parse_decimal(bytes).and_then(|number| u8::try_from(number).ok())
930}
931
932fn decode_text(bytes: &[u8]) -> Str {
933 Str::from_utf8_lossy(bytes)
934}
935fn parse_appearance_response(body: &[u8]) -> Option<u8> {
936 let mut fields = body.strip_prefix(b"?997;")?.split(|byte| *byte == b';');
937 let appearance = fields.next().and_then(parse_decimal_u8)?;
938 (matches!(appearance, 1 | 2) && fields.next().is_none()).then_some(appearance)
939}
940
941fn parse_in_band_resize(body: &[u8]) -> Option<(u16, u16, u16, u16)> {
942 let body = body.strip_suffix(b" ")?;
943 let mut fields = body.split(|byte| *byte == b';');
944 if fields.next()? != b"48" {
945 return None;
946 }
947 let mut number = || {
948 fields
949 .next()
950 .and_then(|field| field.split(|byte| *byte == b':').next())
951 .and_then(parse_decimal_u16)
952 };
953 let rows = number()?;
954 let cols = number()?;
955 let y_px = number()?;
956 let x_px = number()?;
957 (fields.next().is_none()).then_some((rows, cols, x_px, y_px))
958}
959
960fn parse_osc_color(payload: &[u8]) -> Option<(u8, u16, u16, u16)> {
961 let separator = payload.iter().position(|byte| *byte == b';')?;
962 let (index, color) = (&payload[..separator], &payload[separator + 1..]);
963 let index = parse_decimal_u8(index)?;
964 let color = color
965 .strip_prefix(b"rgb:")
966 .or_else(|| color.strip_prefix(b"rgba:"))?;
967 let mut components = color.split(|byte| *byte == b'/');
968 let r = components.next().and_then(parse_hex_component)?;
969 let g = components.next().and_then(parse_hex_component)?;
970 let b = components.next().and_then(parse_hex_component)?;
971 components.next().is_none().then_some((index, r, g, b))
972}
973
974fn parse_hex_component(bytes: &[u8]) -> Option<u16> {
975 if !matches!(bytes.len(), 2 | 4) || !bytes.iter().all(u8::is_ascii_hexdigit) {
976 return None;
977 }
978 let value = u16::from_str_radix(std::str::from_utf8(bytes).ok()?, 16).ok()?;
979 Some(if bytes.len() == 2 {
980 value * 0x101
981 } else {
982 value
983 })
984}
985
986fn emit_unterminated_response(bytes: &[u8], out: &mut Vec<InputEvent>) -> bool {
987 let response = if let Some(payload) = bytes.strip_prefix(b"\x1b]") {
988 TerminalResponse::Osc(decode_text(payload))
989 } else if let Some(payload) = bytes.strip_prefix(b"\x1b_G") {
990 TerminalResponse::KittyGraphics(decode_text(payload))
991 } else if let Some(payload) = bytes.strip_prefix(b"\x1b_") {
992 TerminalResponse::ApplicationProgramCommand(decode_text(payload))
993 } else if let Some(payload) = bytes.strip_prefix(b"\x1bP") {
994 TerminalResponse::DeviceControlString(decode_text(payload))
995 } else {
996 return false;
997 };
998 out.push(InputEvent::Response(response));
999 true
1000}
1001
1002fn emit_raw(bytes: &[u8], keymap: &Keymap, out: &mut Vec<InputEvent>) {
1003 let mut cursor = 0;
1004 while cursor < bytes.len() {
1005 let width = utf8_width(bytes[cursor]).min(bytes.len() - cursor).max(1);
1006 if let Some(chord) = decode_plain(&bytes[cursor..cursor + width], false) {
1007 emit_chord(keymap, chord, out);
1008 }
1009 cursor += width;
1010 }
1011}
1012
1013fn emit_chord(keymap: &Keymap, chord: Chord, out: &mut Vec<InputEvent>) {
1014 if let Some(key) = keymap.resolve(chord) {
1015 out.push(InputEvent::Key(key));
1016 }
1017}
1018
1019fn decode_reencoded_paste_controls(bytes: &[u8]) -> String {
1020 let mut decoded = Vec::with_capacity(bytes.len());
1021 let mut cursor = 0;
1022 while cursor < bytes.len() {
1023 if bytes[cursor..].starts_with(b"\x1b[") {
1024 let tail = &bytes[cursor + 2..];
1025 if let Some(end) = tail.iter().position(|byte| matches!(byte, b'u' | b'~')) {
1026 let body = &tail[..end];
1027 let final_byte = tail[end];
1028 let mut fields = body.split(|byte| *byte == b';');
1029 let codepoint = if final_byte == b'u' {
1030 match (fields.next(), fields.next(), fields.next()) {
1031 (Some(codepoint), Some(b"5"), None) => parse_decimal(codepoint),
1032 _ => None,
1033 }
1034 } else {
1035 match (fields.next(), fields.next(), fields.next(), fields.next()) {
1036 (Some(b"27"), Some(b"5"), Some(codepoint), None) => parse_decimal(codepoint),
1037 _ => None,
1038 }
1039 };
1040 if let Some(codepoint @ (65..=90 | 97..=122)) = codepoint {
1041 let control = if codepoint >= 97 {
1042 codepoint - 96
1043 } else {
1044 codepoint - 64
1045 };
1046 decoded.push(u8::try_from(control).expect("control byte fits"));
1047 cursor += 2 + end + 1;
1048 continue;
1049 }
1050 }
1051 }
1052 decoded.push(bytes[cursor]);
1053 cursor += 1;
1054 }
1055 String::from_utf8_lossy(&decoded).into_owned()
1056}
1057
1058pub fn decode_keys(bytes: &[u8], output: &mut Vec<Key>) {
1063 let mut decoder = InputDecoder::new();
1064 let now = Instant::now();
1065 let mut events = Vec::new();
1066 decoder.feed(bytes, now, &mut events);
1067 decoder.tick(now + INPUT_TIMEOUT + PARTIAL_HOLD_TIMEOUT, &mut events);
1068 output.extend(events.into_iter().filter_map(|event| match event {
1069 InputEvent::Key(key) => Some(key),
1070 _ => None,
1071 }));
1072}
1073
1074#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1080pub struct Chord {
1081 pub key: Key,
1083 pub mods: Mods,
1085}
1086
1087impl Chord {
1088 pub const fn new(key: Key, mods: Mods) -> Self {
1090 Self { key, mods }
1091 }
1092
1093 const fn plain(key: Key) -> Self {
1094 Self::new(key, Mods {
1095 shift: false,
1096 alt: false,
1097 ctrl: false,
1098 super_key: false,
1099 hyper: false,
1100 meta: false,
1101 })
1102 }
1103
1104 const fn with_modifiers(key: Key, modifiers: u32) -> Self {
1105 Self::new(key, Mods {
1106 shift: modifiers & 0b0000_0001 != 0,
1107 alt: modifiers & 0b0000_0010 != 0,
1108 ctrl: modifiers & 0b0000_0100 != 0,
1109 super_key: modifiers & 0b0000_1000 != 0,
1110 hyper: modifiers & 0b0001_0000 != 0,
1111 meta: modifiers & 0b0010_0000 != 0,
1112 })
1113 }
1114
1115 fn folded(self) -> Option<Self> {
1119 if !(self.mods.ctrl || self.mods.alt || self.mods.super_key) {
1120 return None;
1121 }
1122 let Key::Char(ch) = self.key else {
1123 return None;
1124 };
1125 let lowered = ch.to_ascii_lowercase();
1126 let mut mods = self.mods;
1127 mods.shift = false;
1128 (lowered != ch || mods != self.mods).then_some(Self { key: Key::Char(lowered), mods })
1129 }
1130}
1131
1132#[derive(Clone)]
1141pub struct Keymap {
1142 bindings: Vec<(Chord, Option<Key>)>,
1143}
1144
1145const DEFAULT_BINDINGS: &[(Key, u8, Key)] = &[
1151 (Key::Tab, 1, Key::BackTab),
1152 (Key::Left, 2, Key::WordLeft),
1153 (Key::Right, 2, Key::WordRight),
1154 (Key::Left, 4, Key::WordLeft),
1155 (Key::Right, 4, Key::WordRight),
1156 (Key::Char('f'), 2, Key::WordRight),
1157 (Key::Char('b'), 2, Key::WordLeft),
1158 (Key::Char('d'), 2, Key::WordDelete),
1159 (Key::Delete, 2, Key::WordDelete),
1160 (Key::Char('d'), 10, Key::WordDelete),
1161 (Key::Delete, 10, Key::WordDelete),
1162 (Key::Backspace, 4, Key::Ctrl('w')),
1163 (Key::Backspace, 2, Key::Ctrl('w')),
1164 (Key::Backspace, 10, Key::Ctrl('w')),
1165 (Key::Char('v'), 4, Key::Paste),
1166 (Key::Char('v'), 5, Key::PasteRaw),
1167 (Key::Char('V'), 5, Key::PasteRaw),
1170 (Key::Char('j'), 4, Key::ShiftEnter),
1173 (Key::Enter, 1, Key::ShiftEnter),
1174 (Key::Enter, 2, Key::ShiftEnter),
1175 (Key::Enter, 3, Key::ShiftEnter),
1176 (Key::Enter, 4, Key::ShiftEnter),
1177 (Key::Enter, 5, Key::ShiftEnter),
1178 (Key::Enter, 6, Key::ShiftEnter),
1179 (Key::Enter, 7, Key::ShiftEnter),
1180 (Key::Function(3), 1, Key::ShiftEnter),
1183];
1184
1185const fn mods_from_bits(bits: u8) -> Mods {
1186 Mods {
1187 shift: bits & 0b0000_0001 != 0,
1188 alt: bits & 0b0000_0010 != 0,
1189 ctrl: bits & 0b0000_0100 != 0,
1190 super_key: bits & 0b0000_1000 != 0,
1191 hyper: false,
1192 meta: false,
1193 }
1194}
1195
1196impl Default for Keymap {
1197 fn default() -> Self {
1198 Self {
1199 bindings: DEFAULT_BINDINGS
1200 .iter()
1201 .map(|&(key, bits, mapped)| (Chord::new(key, mods_from_bits(bits)), Some(mapped)))
1202 .collect(),
1203 }
1204 }
1205}
1206
1207impl Keymap {
1208 pub fn bind(&mut self, chord: Chord, key: Key) {
1210 self.set(chord, Some(key));
1211 }
1212
1213 pub fn disable(&mut self, chord: Chord) {
1216 self.set(chord, None);
1217 }
1218
1219 pub fn unbind(&mut self, chord: Chord) {
1221 self.bindings.retain(|(bound, _)| *bound != chord);
1222 }
1223
1224 fn set(&mut self, chord: Chord, key: Option<Key>) {
1225 match self.bindings.iter_mut().find(|(bound, _)| *bound == chord) {
1226 Some(slot) => slot.1 = key,
1227 None => self.bindings.push((chord, key)),
1228 }
1229 }
1230
1231 fn entry(&self, chord: Chord) -> Option<&(Chord, Option<Key>)> {
1232 self.bindings.iter().find(|(bound, _)| *bound == chord)
1233 }
1234
1235 pub fn resolve(&self, exact: Chord) -> Option<Key> {
1241 let folded = exact.folded();
1242 if let Some((_, entry)) = self
1243 .entry(exact)
1244 .or_else(|| folded.and_then(|chord| self.entry(chord)))
1245 {
1246 return *entry;
1247 }
1248 let chord = folded.unwrap_or(exact);
1249 if chord.mods.super_key || chord.mods.hyper || chord.mods.meta {
1250 return None;
1251 }
1252 if chord.mods.ctrl && chord.mods.alt {
1253 return match chord.key {
1254 Key::Char(ch) => Some(Key::CtrlAlt(ch.to_ascii_lowercase())),
1255 _ => None,
1256 };
1257 }
1258 if chord.mods.ctrl {
1259 return match chord.key {
1260 Key::Char(ch) => Some(Key::Ctrl(ch.to_ascii_lowercase())),
1261 _ => None,
1262 };
1263 }
1264 if chord.mods.alt {
1265 return match chord.key {
1266 Key::Char(ch) if ch.is_alphanumeric() => Some(Key::Alt(ch.to_ascii_lowercase())),
1267 _ => None,
1268 };
1269 }
1270 if chord.mods.shift && matches!(chord.key, Key::Function(_)) {
1271 return None;
1272 }
1273 Some(match chord.key {
1274 Key::Char(' ') => Key::Space,
1275 Key::Char(ch) if chord.mods.shift => Key::Char(ch.to_ascii_uppercase()),
1276 key => key,
1277 })
1278 }
1279}
1280
1281#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1283pub enum Mouse {
1284 Click,
1286 RightClick,
1288 MiddleClick,
1290 Move,
1292 Drag,
1294 Release,
1296 WheelUp,
1298 WheelDown,
1300 WheelLeft,
1302 WheelRight,
1304}
1305
1306#[derive(Clone, Debug, Eq, PartialEq)]
1308pub enum UiEvent {
1309 None,
1311 Submit,
1313 Cancel,
1315 Pressed(Str),
1317 Highlighted {
1319 id: Str,
1321 value: Str,
1323 },
1324 Changed {
1326 id: Str,
1328 value: Str,
1330 },
1331 Filtered {
1333 id: Str,
1335 query: Str,
1337 value: Option<Str>,
1340 },
1341}
1342
1343pub fn byte_at_column(text: &str, column: u16) -> usize {
1345 let mut cells = 0u16;
1346 for (offset, grapheme) in text.grapheme_indices() {
1347 if cells >= column {
1348 return offset;
1349 }
1350 cells += cell_width(grapheme);
1351 }
1352 text.len()
1353}
1354#[derive(Clone, Copy, Eq, PartialEq)]
1355enum WordClass {
1356 Word,
1357 Whitespace,
1358 Cjk,
1359 Delimiter,
1360}
1361
1362const fn is_cjk(character: char) -> bool {
1363 matches!(
1364 character as u32,
1365 0x2E80..=0x2FFF
1366 | 0x3040..=0x30FF
1367 | 0x3100..=0x312F
1368 | 0x3130..=0x318F
1369 | 0x31A0..=0x31BF
1370 | 0x31F0..=0x31FF
1371 | 0x3400..=0x4DBF
1372 | 0x4E00..=0x9FFF
1373 | 0xA960..=0xA97F
1374 | 0xAC00..=0xD7AF
1375 | 0xF900..=0xFAFF
1376 | 0x20000..=0x2FA1F
1377 )
1378}
1379
1380fn word_class(grapheme: &str) -> WordClass {
1381 let Some(character) = grapheme.chars().next() else {
1382 return WordClass::Delimiter;
1383 };
1384 if character.is_whitespace() {
1385 WordClass::Whitespace
1386 } else if is_cjk(character) {
1387 WordClass::Cjk
1388 } else if character.is_alphanumeric() || character == '_' {
1389 WordClass::Word
1390 } else {
1391 WordClass::Delimiter
1392 }
1393}
1394
1395fn is_word_joiner(grapheme: &str) -> bool {
1396 matches!(grapheme, "'" | "’" | "-" | "‐" | "‑")
1397}
1398
1399fn word_left_byte(text: &str, at: usize) -> usize {
1400 let mut graphemes = text[..at].grapheme_indices().rev().peekable();
1401 while graphemes
1402 .peek()
1403 .is_some_and(|(_, grapheme)| word_class(grapheme) == WordClass::Whitespace)
1404 {
1405 graphemes.next();
1406 }
1407 let Some((start, first)) = graphemes.next() else {
1408 return 0;
1409 };
1410 let class = word_class(first);
1411 if class == WordClass::Cjk {
1412 return start;
1413 }
1414 if class != WordClass::Word {
1415 let mut target = start;
1416 while let Some(&(offset, grapheme)) = graphemes.peek() {
1417 if word_class(grapheme) != class {
1418 break;
1419 }
1420 target = offset;
1421 graphemes.next();
1422 }
1423 return target;
1424 }
1425 let mut target = start;
1426 while let Some((offset, grapheme)) = graphemes.next() {
1427 if word_class(grapheme) == WordClass::Word {
1428 target = offset;
1429 } else if is_word_joiner(grapheme)
1430 && graphemes
1431 .peek()
1432 .is_some_and(|(_, left)| word_class(left) == WordClass::Word)
1433 {
1434 let (left_offset, _) = graphemes.next().expect("peeked left word");
1435 target = left_offset;
1436 } else {
1437 break;
1438 }
1439 }
1440 target
1441}
1442
1443fn word_right_byte(text: &str, at: usize) -> usize {
1444 let mut graphemes = text[at..].grapheme_indices().peekable();
1445 while graphemes
1446 .peek()
1447 .is_some_and(|(_, grapheme)| word_class(grapheme) == WordClass::Whitespace)
1448 {
1449 graphemes.next();
1450 }
1451 let Some((first_offset, first)) = graphemes.next() else {
1452 return text.len();
1453 };
1454 let class = word_class(first);
1455 let mut end = at + first_offset + first.len();
1456 if class == WordClass::Cjk {
1457 return end;
1458 }
1459 if class != WordClass::Word {
1460 while let Some(&(_, grapheme)) = graphemes.peek() {
1461 if word_class(grapheme) != class {
1462 break;
1463 }
1464 let (offset, grapheme) = graphemes.next().expect("peeked delimiter");
1465 end = at + offset + grapheme.len();
1466 }
1467 return end;
1468 }
1469 while let Some((offset, grapheme)) = graphemes.next() {
1470 if word_class(grapheme) == WordClass::Word
1471 || (is_word_joiner(grapheme)
1472 && graphemes
1473 .peek()
1474 .is_some_and(|(_, right)| word_class(right) == WordClass::Word))
1475 {
1476 end = at + offset + grapheme.len();
1477 } else {
1478 break;
1479 }
1480 }
1481 end
1482}
1483
1484pub fn word_left_column(text: &str, column: u16) -> u16 {
1486 cell_width(&text[..word_left_byte(text, byte_at_column(text, column))])
1487}
1488
1489pub fn word_right_column(text: &str, column: u16) -> u16 {
1491 cell_width(&text[..word_right_byte(text, byte_at_column(text, column))])
1492}
1493
1494pub fn word_rubout_start(text: &str, at: usize) -> usize {
1496 word_left_byte(text, at)
1497}
1498
1499pub fn sanitize_paste(text: &str) -> String {
1501 let normalized_newlines = text.replace("\r\n", "\n").replace('\r', "\n");
1502 normalized_newlines
1503 .chars()
1504 .filter(|character| !character.is_control() || matches!(character, '\n' | '\t'))
1505 .collect::<String>()
1506 .into_nfc()
1507}
1508
1509#[cfg(test)]
1510mod tests {
1511 use std::time::{Duration, Instant};
1512
1513 use super::{
1514 Chord, InputDecoder, InputEvent, Key, Keymap, Mods, Mouse, MouseButton, MouseReport,
1515 TerminalResponse, decode_keys, mods_from_bits,
1516 };
1517
1518 fn drip(bytes: &[u8]) -> Vec<InputEvent> {
1519 let start = Instant::now();
1520 let mut decoder = InputDecoder::new();
1521 let mut events = Vec::new();
1522 for (offset, byte) in bytes.iter().enumerate() {
1523 decoder.feed(
1524 std::slice::from_ref(byte),
1525 start + Duration::from_millis(u64::try_from(offset).unwrap()),
1526 &mut events,
1527 );
1528 }
1529 events
1530 }
1531
1532 #[test]
1533 fn native_keymap_covers_chords_and_motion() {
1534 let cases: &[(Key, u8, Key)] = &[
1535 (Key::Char('a'), 4, Key::Ctrl('a')),
1536 (Key::Char('W'), 5, Key::Ctrl('w')),
1537 (Key::Char('k'), 5, Key::Ctrl('k')),
1538 (Key::Left, 2, Key::WordLeft),
1539 (Key::Right, 4, Key::WordRight),
1540 (Key::Enter, 1, Key::ShiftEnter),
1541 (Key::Enter, 0, Key::Enter),
1542 (Key::Char(' '), 0, Key::Space),
1543 (Key::Char('Z'), 1, Key::Char('Z')),
1544 (Key::BackTab, 1, Key::BackTab),
1545 (Key::PageDown, 0, Key::PageDown),
1546 (Key::Enter, 2, Key::ShiftEnter),
1547 (Key::Enter, 4, Key::ShiftEnter),
1548 (Key::Char('j'), 4, Key::ShiftEnter),
1549 (Key::Function(3), 1, Key::ShiftEnter),
1550 (Key::Char('d'), 2, Key::WordDelete),
1551 (Key::Delete, 2, Key::WordDelete),
1552 (Key::Backspace, 4, Key::Ctrl('w')),
1553 (Key::Backspace, 10, Key::Ctrl('w')),
1554 (Key::Char('d'), 10, Key::WordDelete),
1555 (Key::Char('f'), 2, Key::WordRight),
1556 (Key::Char('b'), 2, Key::WordLeft),
1557 (Key::Char('d'), 4, Key::Ctrl('d')),
1558 (Key::Char('y'), 2, Key::Alt('y')),
1559 (Key::Char('Y'), 3, Key::Alt('y')),
1560 (Key::Char(']'), 6, Key::CtrlAlt(']')),
1561 ];
1562 let keymap = Keymap::default();
1563 for &(key, bits, expected) in cases {
1564 let chord = Chord::new(key, mods_from_bits(bits));
1565 assert_eq!(keymap.resolve(chord), Some(expected), "{chord:?}");
1566 }
1567 }
1568
1569 #[test]
1570 fn keymap_resolves_smart_and_raw_paste_chords() {
1571 let mut keymap = Keymap::default();
1572 let smart = Chord::new(Key::Char('v'), mods_from_bits(4));
1573 assert_eq!(keymap.resolve(smart), Some(Key::Paste));
1574 assert_eq!(
1575 keymap.resolve(Chord::new(Key::Char('v'), mods_from_bits(5))),
1576 Some(Key::PasteRaw)
1577 );
1578 assert_eq!(
1579 keymap.resolve(Chord::new(Key::Char('V'), mods_from_bits(5))),
1580 Some(Key::PasteRaw)
1581 );
1582
1583 keymap.unbind(smart);
1584 assert_eq!(keymap.resolve(smart), Some(Key::Ctrl('v')));
1585 }
1586
1587 #[test]
1588 fn decoder_normalizes_kitty_shifted_letters() {
1589 let cases: &[(&[u8], Key)] = &[
1590 (b"\x1b[97;2u", Key::Char('A')),
1591 (b"\x1b[65;2u", Key::Char('A')),
1592 (b"\x1b[49;2u", Key::Char('1')),
1593 (b"\x1b[97;5u", Key::Ctrl('a')),
1594 (b"A", Key::Char('A')),
1595 ];
1596 for &(bytes, expected) in cases {
1597 let mut keys = Vec::new();
1598 decode_keys(bytes, &mut keys);
1599 assert_eq!(keys, [expected], "{bytes:?}");
1600 }
1601 }
1602
1603 #[test]
1604 fn decoder_filters_releases_and_keymap_filters_os_chords() {
1605 let mut keys = Vec::new();
1606 decode_keys(b"\x1b[97;1:3u", &mut keys);
1607 assert!(keys.is_empty(), "kitty release must not become input");
1608
1609 let keymap = Keymap::default();
1610 let os_mods = [
1611 Mods { super_key: true, ..Mods::default() },
1612 Mods { hyper: true, ..Mods::default() },
1613 Mods { meta: true, ..Mods::default() },
1614 ];
1615 for mods in os_mods {
1616 assert_eq!(
1617 keymap.resolve(Chord::new(Key::Char('c'), mods)),
1618 None,
1619 "OS shortcuts must never type ({mods:?})"
1620 );
1621 }
1622 let hyper = Chord::new(Key::Char('c'), Mods { hyper: true, ..Mods::default() });
1623 let mut keymap = Keymap::default();
1624 keymap.bind(hyper, Key::Esc);
1625 assert_eq!(keymap.resolve(hyper), Some(Key::Esc));
1626 }
1627
1628 #[test]
1629 fn keymap_bindings_are_customizable() {
1630 let mut keymap = Keymap::default();
1631 let legacy = Chord::new(Key::Function(3), mods_from_bits(1));
1632 assert_eq!(keymap.resolve(legacy), Some(Key::ShiftEnter));
1633 keymap.unbind(legacy);
1634 assert_eq!(keymap.resolve(legacy), None);
1635
1636 let function = Chord::new(Key::Function(5), Mods::default());
1637 keymap.bind(function, Key::Ctrl('r'));
1638 assert_eq!(keymap.resolve(function), Some(Key::Ctrl('r')));
1639 keymap.bind(function, Key::Esc);
1640 assert_eq!(keymap.resolve(function), Some(Key::Esc));
1641
1642 let word = Chord::new(Key::Right, mods_from_bits(2));
1643 keymap.unbind(word);
1644 assert_eq!(keymap.resolve(word), None);
1645
1646 let quit = Chord::new(Key::Char('q'), Mods::default());
1647 keymap.disable(quit);
1648 assert_eq!(keymap.resolve(quit), None);
1649 keymap.unbind(quit);
1650 assert_eq!(keymap.resolve(quit), Some(Key::Char('q')));
1651
1652 let exact = Chord::new(Key::Char('Y'), mods_from_bits(3));
1653 keymap.bind(exact, Key::PageUp);
1654 assert_eq!(keymap.resolve(exact), Some(Key::PageUp));
1655 assert_eq!(
1656 keymap.resolve(Chord::new(Key::Char('y'), mods_from_bits(2))),
1657 Some(Key::Alt('y')),
1658 "lowercase spelling still uses the identity fallback"
1659 );
1660 }
1661
1662 #[test]
1663 fn decoder_applies_live_keymap_changes_once() {
1664 let start = Instant::now();
1665 let chord = Chord::new(Key::Char('f'), Mods { alt: true, ..Mods::default() });
1666 let mut decoder = InputDecoder::new();
1667 let mut events = Vec::new();
1668
1669 decoder.feed(b"\x1bf", start, &mut events);
1670 assert_eq!(events, [InputEvent::Key(Key::WordRight)]);
1671
1672 events.clear();
1673 decoder.keymap_mut().disable(chord);
1674 decoder.feed(b"\x1bf", start, &mut events);
1675 assert_eq!(events, [] as [InputEvent; 0]);
1676
1677 decoder.keymap_mut().bind(chord, Key::PageDown);
1678 decoder.feed(b"\x1bf", start, &mut events);
1679 assert_eq!(events, [InputEvent::Key(Key::PageDown)]);
1680
1681 events.clear();
1682 decoder.feed(b"x", start, &mut events);
1683 assert_eq!(events, [InputEvent::Key(Key::Char('x'))]);
1684 }
1685
1686 #[test]
1687 fn raw_key_decoder_covers_terminal_sequence_families_and_utf8() {
1688 let mut keys = Vec::new();
1689 decode_keys(
1690 b"\x1b[A\x1bOB\x1b[5~\x1b[6~\x1b[H\x1b[F\x1b[3~\x1b[13;2u\x01\x1bx\xc3\xa9\x1b",
1691 &mut keys,
1692 );
1693 assert_eq!(keys, [
1694 Key::Up,
1695 Key::Down,
1696 Key::PageUp,
1697 Key::PageDown,
1698 Key::Home,
1699 Key::End,
1700 Key::Delete,
1701 Key::ShiftEnter,
1702 Key::Ctrl('a'),
1703 Key::Alt('x'),
1704 Key::Char('é'),
1705 Key::Esc,
1706 ]);
1707 }
1708
1709 #[test]
1710 fn streaming_decoder_holds_split_escapes_until_timeout() {
1711 let start = Instant::now();
1712 let mut decoder = InputDecoder::new();
1713 let mut events = Vec::new();
1714 decoder.feed(b"\x1b", start, &mut events);
1715 decoder.feed(b"[A", start + Duration::from_millis(74), &mut events);
1716 assert_eq!(events, [InputEvent::Key(Key::Up)]);
1717
1718 events.clear();
1719 let mut decoder = InputDecoder::new();
1720 decoder.feed(b"\x1b", start, &mut events);
1721 decoder.feed(b"[A", start + Duration::from_millis(76), &mut events);
1722 assert_eq!(events, [
1723 InputEvent::Key(Key::Esc),
1724 InputEvent::Key(Key::Char('[')),
1725 InputEvent::Key(Key::Char('A')),
1726 ]);
1727
1728 events.clear();
1729 let mut decoder = InputDecoder::new();
1730 decoder.set_kitty_keyboard(true);
1731 decoder.feed(b"\x1b", start, &mut events);
1732 decoder.tick(start + Duration::from_millis(224), &mut events);
1733 assert_eq!(events, [] as [InputEvent; 0]);
1734 decoder.tick(start + Duration::from_millis(225), &mut events);
1735 assert_eq!(events, [InputEvent::Key(Key::Esc)]);
1736 }
1737 #[test]
1738 fn decoder_deadline_tracks_pending_partial_input() {
1739 let start = Instant::now();
1740 let mut decoder = InputDecoder::new();
1741 let mut events = Vec::new();
1742 assert_eq!(decoder.deadline(), None);
1743
1744 decoder.feed(b"\x1b[", start, &mut events);
1745 assert_eq!(events, [] as [InputEvent; 0]);
1746 assert_eq!(decoder.deadline(), Some(start + Duration::from_millis(75)));
1747
1748 decoder.tick(start + Duration::from_millis(75), &mut events);
1749 assert_eq!(events, [InputEvent::Key(Key::Esc), InputEvent::Key(Key::Char('['))]);
1750 assert_eq!(decoder.deadline(), None);
1751 }
1752
1753 #[test]
1754 fn streaming_decoder_disambiguates_alt_and_meta_escape_prefixes() {
1755 let start = Instant::now();
1756 let mut decoder = InputDecoder::new();
1757 let mut events = Vec::new();
1758 decoder.feed(b"\x1b\x1bd\x1b\x1b[D\x1b\x1b", start, &mut events);
1759 decoder.tick(start + Duration::from_millis(75), &mut events);
1760 assert_eq!(events, [
1761 InputEvent::Key(Key::Esc),
1762 InputEvent::Key(Key::WordDelete),
1763 InputEvent::Key(Key::WordLeft),
1764 InputEvent::Key(Key::Esc),
1765 InputEvent::Key(Key::Esc),
1766 ]);
1767 }
1768
1769 #[test]
1770 fn streaming_decoder_filters_late_replies_and_decodes_key_families() {
1771 let start = Instant::now();
1772 let mut decoder = InputDecoder::new();
1773 let mut events = Vec::new();
1774 decoder.feed(b"x\x1b[?1;2c\x1b[15~\x1b[24~\x1b[1;5D\x1b[I\x1b[O", start, &mut events);
1775 assert_eq!(events, [
1776 InputEvent::Key(Key::Char('x')),
1777 InputEvent::Response(TerminalResponse::DeviceAttributes("?1;2".into())),
1778 InputEvent::Key(Key::Function(5)),
1779 InputEvent::Key(Key::Function(12)),
1780 InputEvent::Key(Key::WordLeft),
1781 InputEvent::Focus(true),
1782 InputEvent::Focus(false),
1783 ]);
1784 }
1785
1786 #[test]
1787 fn kitty_csi_u_suppresses_release_delivers_repeat_and_deduplicates_printable() {
1788 let start = Instant::now();
1789 let mut decoder = InputDecoder::new();
1790 let mut events = Vec::new();
1791 decoder.feed(b"\x1b[97;1:3u\x1b[98;1:2u\x1b[97u", start, &mut events);
1792 decoder.feed(b"a", start + Duration::from_millis(25), &mut events);
1793 decoder.feed(b"a", start + Duration::from_millis(26), &mut events);
1794 decoder.feed(b"\x1b[32u ", start + Duration::from_millis(27), &mut events);
1795 assert_eq!(events, [
1796 InputEvent::Key(Key::Char('b')),
1797 InputEvent::Key(Key::Char('a')),
1798 InputEvent::Key(Key::Char('a')),
1799 InputEvent::Key(Key::Space),
1800 ]);
1801 }
1802
1803 #[test]
1804 fn bracketed_paste_reassembles_recovers_and_decodes_tmux_controls() {
1805 let start = Instant::now();
1806 let mut decoder = InputDecoder::new();
1807 let mut events = Vec::new();
1808 decoder.feed(b"\x1b[20", start, &mut events);
1809 decoder.feed(b"0~one\r", start + Duration::from_millis(10), &mut events);
1810 decoder.feed(b"\ntwo\x1b[201~", start + Duration::from_millis(20), &mut events);
1811 assert_eq!(events, [InputEvent::Paste("one\ntwo".into())]);
1812
1813 events.clear();
1814 decoder.feed(
1815 b"\x1b[200~a\x1b[106;5ub\x1b[27;5;105~c\x1b[201~",
1816 start + Duration::from_millis(30),
1817 &mut events,
1818 );
1819 assert_eq!(events, [InputEvent::Paste("a\nb\tc".into())]);
1820
1821 events.clear();
1822 decoder.feed(b"\x1b[200~unterminated", start + Duration::from_millis(40), &mut events);
1823 decoder.tick(start + Duration::from_millis(1040), &mut events);
1824 assert_eq!(events, [InputEvent::Paste("unterminated".into())]);
1825 }
1826
1827 #[test]
1828 fn sgr_mouse_reports_hold_splits_and_preserve_raw_details() {
1829 let start = Instant::now();
1830 let mut decoder = InputDecoder::new();
1831 let mut events = Vec::new();
1832 decoder.feed(b"\x1b[<60;1", start, &mut events);
1833 decoder.feed(b"0;4M", start + Duration::from_millis(150), &mut events);
1834 decoder.feed(b"\x1b[<65;3;2M\x1b[<32;7;8M", start + Duration::from_millis(151), &mut events);
1835 assert_eq!(events, [
1836 InputEvent::Mouse(MouseReport {
1837 kind: Mouse::Drag,
1838 col: 9,
1839 row: 3,
1840 button: MouseButton::Left,
1841 mods: Mods { shift: true, alt: true, ctrl: true, ..Mods::default() },
1842 pressed: true,
1843 }),
1844 InputEvent::Mouse(MouseReport {
1845 kind: Mouse::WheelDown,
1846 col: 2,
1847 row: 1,
1848 button: MouseButton::WheelDown,
1849 mods: Mods::default(),
1850 pressed: true,
1851 }),
1852 InputEvent::Mouse(MouseReport {
1853 kind: Mouse::Drag,
1854 col: 6,
1855 row: 7,
1856 button: MouseButton::Left,
1857 mods: Mods::default(),
1858 pressed: true,
1859 }),
1860 ]);
1861 }
1862
1863 #[test]
1864 fn sgr_mouse_maps_buttons_wheels_drag_and_release() {
1865 let cases = [
1866 (b"\x1b[<2;4;5M".as_slice(), Mouse::RightClick),
1867 (b"\x1b[<1;4;5M".as_slice(), Mouse::MiddleClick),
1868 (b"\x1b[<66;4;5M".as_slice(), Mouse::WheelLeft),
1869 (b"\x1b[<67;4;5M".as_slice(), Mouse::WheelRight),
1870 (b"\x1b[<32;4;5M".as_slice(), Mouse::Drag),
1871 (b"\x1b[<0;4;5m".as_slice(), Mouse::Release),
1872 ];
1873 for (bytes, kind) in cases {
1874 let start = Instant::now();
1875 let mut decoder = InputDecoder::new();
1876 let mut events = Vec::new();
1877 decoder.feed(bytes, start, &mut events);
1878 assert_eq!(events.len(), 1);
1879 let InputEvent::Mouse(report) = events[0] else {
1880 panic!("expected mouse report");
1881 };
1882 assert_eq!(report.kind, kind);
1883 assert_eq!((report.col, report.row), (3, 4));
1884 }
1885 }
1886
1887 #[test]
1888 fn capability_responses_parse_whole_and_byte_dripped() {
1889 let cases = [
1890 (
1891 b"\x1b]11;rgb:ffff/0000/8080\x07".as_slice(),
1892 InputEvent::Response(TerminalResponse::OscColor {
1893 index: 11,
1894 r: 0xffff,
1895 g: 0,
1896 b: 0x8080,
1897 }),
1898 ),
1899 (
1900 b"\x1b]11;rgba:ff/00/80\x1b\\".as_slice(),
1901 InputEvent::Response(TerminalResponse::OscColor {
1902 index: 11,
1903 r: 0xffff,
1904 g: 0,
1905 b: 0x8080,
1906 }),
1907 ),
1908 (b"\x1b[?997;1n".as_slice(), InputEvent::Response(TerminalResponse::AppearanceChanged(1))),
1909 (
1910 b"\x1b[48;24;80;1600;800 t".as_slice(),
1911 InputEvent::Response(TerminalResponse::InBandResize {
1912 rows: 24,
1913 cols: 80,
1914 x_px: 800,
1915 y_px: 1600,
1916 }),
1917 ),
1918 ];
1919 for (bytes, expected) in cases {
1920 let start = Instant::now();
1921 let mut decoder = InputDecoder::new();
1922 let mut events = Vec::new();
1923 decoder.feed(bytes, start, &mut events);
1924 assert_eq!(events.as_slice(), std::slice::from_ref(&expected));
1925 assert_eq!(drip(bytes), [expected]);
1926 }
1927 }
1928
1929 #[test]
1930 fn decoder_maps_mouse_gestures_with_position() {
1931 let cases = [
1932 (b"\x1b[<0;4;8M".as_slice(), Mouse::Click),
1933 (b"\x1b[<64;4;8M".as_slice(), Mouse::WheelUp),
1934 (b"\x1b[<35;4;8M".as_slice(), Mouse::Move),
1935 (b"\x1b[<2;4;8M".as_slice(), Mouse::RightClick),
1936 (b"\x1b[<1;4;8M".as_slice(), Mouse::MiddleClick),
1937 (b"\x1b[<32;4;8M".as_slice(), Mouse::Drag),
1938 (b"\x1b[<0;4;8m".as_slice(), Mouse::Release),
1939 (b"\x1b[<66;4;8M".as_slice(), Mouse::WheelLeft),
1940 (b"\x1b[<67;4;8M".as_slice(), Mouse::WheelRight),
1941 ];
1942 for (bytes, expected) in cases {
1943 let events = drip(bytes);
1944 let [InputEvent::Mouse(report)] = events.as_slice() else {
1945 panic!("expected one mouse event");
1946 };
1947 assert_eq!(report.kind, expected);
1948 assert_eq!((report.col, report.row), (3, 7));
1949 }
1950 }
1951
1952 #[test]
1953 fn word_helpers_follow_pi_coarse_semantics() {
1954 use super::{word_left_column, word_right_column, word_rubout_start};
1955 let text = "foo-bar baz";
1956 assert_eq!(word_left_column(text, 11), 8);
1957 assert_eq!(word_left_column(text, 8), 0);
1958 assert_eq!(word_right_column(text, 0), 7);
1959 assert_eq!(word_right_column(text, 7), 11);
1960 assert_eq!(word_rubout_start(text, 7), 0);
1961 assert_eq!(word_left_column("中文", 4), 2);
1962 assert_eq!(word_right_column("中文", 0), 2);
1963 }
1964}