prettytty/event.rs
1//! Support for keyboard, mouse, and query response events.
2//!
3//! # Background
4//!
5//! Prettytty's [`Scan`] trait reads so-called [`Token`]s from terminal input,
6//! largely to distinguish between runs of UTF-8 encoded text and ANSI escape
7//! sequences. To avoid the overhead of heap allocation for every single token,
8//! their payloads draw on the scanner's internal buffer. That necessitates
9//! processing a token's payload before reading the next.
10//!
11//! By contrast, this module's [`Event`]s are higher-level abstractions, which
12//! often stem from entire ANSI escape sequences. They also are self-contained,
13//! i.e., not restricted by explicit lifetimes, yet lightweight enough to
14//! implement [`Copy`]. They include key, mouse, window, and response-to-query
15//! events. Since not all valid ANSI escape sequences map to this module's
16//! events, [`ScanEvent::read_event`] method returns [`TokenOrEvent`], with
17//! tokens serving as fallback. Consistent with how hardware terminals treat
18//! unknown escape sequences, an application may simply ignore such tokens.
19//!
20//! Tokens should suffice for applications that only need to process key presses
21//! for letters, numbers, and common symbols—or the occasional ANSI escape
22//! sequence. Applications that need to process modifier keys, mouse input,
23//! and ANSI escape sequences should prefer events.
24//!
25//! ## Keyboard Input
26//!
27//! Unfortunately, there are several conventions for encoding keyboard and mouse
28//! input from terminals. Legacy encodings for keyboard events share some
29//! patterns, such as
30//! <code>CSI <em>P<sub>key</sub></em> ; <em>P<sub>mod</sub></em> ~</code>
31//! or <code>SS3 <em>key</em></code>. But they also differ for PC, VT-220,
32//! VT-52, Sun, and HP keyboards, as covered in
33//! [xterm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Special-Keyboard-Keys)'s
34//! documentation and highlighted by [this
35//! table](https://invisible-island.net/xterm/xterm-function-keys.html). Worse,
36//! legacy encodings are incomplete and ambiguous. For example, there is no way
37//! to distinguish between <kbd>ctrl-i</kbd> and <kbd>shift-ctrl-i</kbd>.
38//! <kbd>alt-c</kbd> may really be <kbd>esc</kbd> and <kbd>c</kbd> typed too
39//! quickly after each other. <kbd>alt-c</kbd> also overlaps with the first byte
40//! in the UTF-8 encoding of many extended Latin characters such as é.
41//!
42//! There are a number of attempts to fix these short-comings:
43//!
44//! * [Fixterms](http://www.leonerd.org.uk/hacks/fixterms/)
45//! * [kitty's terminal protocol
46//! extensions](https://sw.kovidgoyal.net/kitty/protocol-extensions/#keyboard-handling),
47//! which build on fixterms
48//! * [xterm's
49//! modifyOtherKeys](https://invisible-island.net/xterm/modified-keys.html)
50//! enables a partially overlapping encoding
51//! * [`win32-input-mode`](https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md),
52//! which encodes the Windows-specific `KEY_EVENT_RECORD` structure
53//!
54//! Currently, prettytty supports common legacy encodings as well as the
55//! encoding based on <code>CSI⋯u</code> shared between fixterms, kitty, and
56//! xterm. No configuration is necessary, all of them are recognized by default.
57//! Prettytty does *not* yet support kitty's progressive enhancements. While
58//! many of the enhancements are useful, their encoding is rather awkward,
59//! distinguishing between semicolons and colons. While that may be consistent
60//! with the letter of some ISO standards, it also is wilfully different from
61//! all other ANSI escape sequences and well-established terminal practice.
62//!
63//!
64//! # The `event` Feature
65//!
66//! This module is entirely optional and requires that the `event` feature is
67//! enabled.
68
69// Some background on encoding [mouse
70// events](https://leonerds-code.blogspot.com/2012/04/wide-mouse-support-in-libvterm.html)
71
72use crate::cmd::{ModeStatus, RequestColor};
73use crate::Token;
74
75/// A token or an event.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum TokenOrEvent<'a> {
78 /// An event.
79 Event(Event),
80 /// A token, which is guaranteed not to be a text token.
81 Token(Token<'a>),
82}
83
84impl TokenOrEvent<'_> {
85 /// Determine whether this is a token.
86 pub const fn is_token(&self) -> bool {
87 matches!(*self, TokenOrEvent::Token(_))
88 }
89
90 /// Determine whether this is an event.
91 pub const fn is_event(&self) -> bool {
92 matches!(*self, TokenOrEvent::Event(_))
93 }
94
95 /// Drop any token, returning events only.
96 pub fn as_event(&self) -> Option<Event> {
97 match *self {
98 TokenOrEvent::Event(event) => Some(event),
99 _ => None,
100 }
101 }
102
103 /// Map an event, dropping the token.
104 pub fn map_event<R, F>(&self, op: F) -> Option<R>
105 where
106 F: FnOnce(Event) -> R,
107 {
108 match *self {
109 TokenOrEvent::Event(event) => Some(op(event)),
110 _ => None,
111 }
112 }
113
114 /// Map either token or event to a common (result) type.
115 ///
116 /// This method consumes the token-or-event instance.
117 pub fn map_either<R, Fe, Ft>(self, map_event: Fe, map_token: Ft) -> Option<R>
118 where
119 Fe: FnOnce(Event) -> Option<R>,
120 Ft: FnOnce(Token<'_>) -> Option<R>,
121 {
122 match self {
123 TokenOrEvent::Event(event) => map_event(event),
124 TokenOrEvent::Token(token) => map_token(token),
125 }
126 }
127}
128
129/// An input event.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum Event {
132 /// A key press, repeat, or release.
133 Key(KeyEvent),
134 /// A mouse move, button press, mouse drag, or button release.
135 Mouse(MouseEvent),
136 /// A window event.
137 Window(WindowEvent),
138 /// A cursor event.
139 Cursor(CursorEvent),
140 /// A color event.
141 Color(ColorEvent),
142 /// A terminal mode event.
143 Mode(ModeStatus),
144}
145
146// -------------------------------------------------------------------------------------
147
148/// A mouse event.
149///
150/// This struct captures mouse movements along with keyboard modifiers and
151/// button presses. Since every mouse event includes a screen coordinate, this
152/// struct includes both coordinates inline.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub struct MouseEvent {
155 pub kind: MouseEventKind,
156 pub modifiers: Modifiers,
157 pub column: u16,
158 pub row: u16,
159}
160
161/// The kind of mouse event.
162///
163/// What [`Press`](MouseEventKind::Press), [`Drag`](MouseEventKind::Drag), and
164/// [`Release`](MouseEventKind::Release) are to mice,
165/// [`Press`](KeyboardEventKind::Press), [`Repeat`](MouseEventKind::Repeat), and
166/// [`Release`](MouseEventKind::Release) are to keyboards.
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum MouseEventKind {
169 Press(MouseButton),
170 Drag(MouseButton),
171 Release(MouseButton),
172 Move,
173}
174
175/// The mouse button.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub enum MouseButton {
178 Left,
179 Middle,
180 Right,
181}
182
183// -------------------------------------------------------------------------------------
184
185/// A key event.
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct KeyEvent {
188 pub kind: KeyEventKind,
189 pub modifiers: Modifiers,
190 pub key: Key,
191}
192
193/// The kind of key event.
194///
195/// What [`Press`](MouseEventKind::Press), [`Drag`](MouseEventKind::Drag), and
196/// [`Release`](MouseEventKind::Release) are to mice,
197/// [`Press`](KeyboardEventKind::Press), [`Repeat`](MouseEventKind::Repeat), and
198/// [`Release`](MouseEventKind::Release) are to keyboards.
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
200pub enum KeyEventKind {
201 Press,
202 Repeat,
203 Release,
204}
205
206/// A logical modifier of keys and mouse button presses.
207///
208/// This enumeration abstracts over the concrete [modifier key](ModifierKey),
209/// which may be on the left or right side of the keyboard, and instead includes
210/// variants for the logical modifier only. Contemporary keyboard generally
211/// include the following modifier keys:
212///
213/// * <kbd>shift</kbd>
214/// * <kbd>alt</kbd> or<kbd>option</kbd>
215/// * <kbd>control</kbd>
216/// * <kbd>Windows</kbd>, <kbd>Linux</kbd>, or <kbd>command</kbd>
217/// * <kbd>caps lock</kbd>
218/// * possibly <kbd>num lock</kbd>
219///
220/// Influential past keyboards, such as the [space-cadet
221/// keyboard](https://en.wikipedia.org/wiki/Space-cadet_keyboard), further
222/// included the <kbd>super</kbd>, <kbd>hyper</kbd>, and <kbd>meta</kbd>
223/// modifiers.
224///
225/// Terminal emulators usually agree only on the names of the first three
226/// modifiers, (1) <kbd>shift</kbd>, (2) <kbd>alt</kbd>/<kbd>option</kbd>, and
227/// (3) <kbd>control</kbd>. Amongst further modifiers, xterm labels the fourth
228/// one <kbd>meta</kbd>, whereas kitty calls that modifier <kbd>super</kbd> and
229/// the sixth modifier <kbd>meta</kbd>. Given these divergent names, prettytty
230/// uses a neutral term, <kbd>command</kbd>, for the fourth modifier.
231///
232/// The [`Modifier::Keypad`] variant is a virtual modifier, i.e., it has no
233/// physical key. It is used to distinguish keys, such as <kbd>/</kbd> or
234/// <kbd>=</kbd>, that appear even on a 60% or 65% keyboard from equally
235/// labelled keys abutting the numeric pad, which only appears on 96% or 100%
236/// keyboards.
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238#[non_exhaustive]
239pub enum Modifier {
240 /// The <kbd>shift</kbd> key.
241 Shift = 0x01,
242 /// The <kbd>alt</kbd> or <kbd>option</kbd> key.
243 Option = 0x02,
244 /// The <kbd>control</kbd> key.
245 Control = 0x04,
246 /// The <kbd>Windows</kbd>, <kbd>Linux</kbd>, or <kbd>command</kbd> key.
247 /// Xterm labels this modifier as <kbd>meta</kbd> and Kitty labels it as
248 /// <kbd>super</kbd>
249 Command = 0x08,
250 /// A first extra modifier, labelled <kbd>hyper</kbd> by Kitty.
251 Extra1 = 0x10,
252 /// A second extra modifier, labelled <kbd>meta</kbd> by Kitty.
253 Extra2 = 0x20,
254 /// The <kbd>caps lock</kbd> key.
255 CapsLock = 0x40,
256 /// The <kbd>number lock</kbd> key.
257 NumLock = 0x80,
258 /// A virtual modifier indicating a keypad key.
259 Keypad = 0x100,
260}
261
262impl Modifier {
263 const fn successor(&self) -> Option<Self> {
264 use Modifier::*;
265
266 Some(match *self {
267 Shift => Option,
268 Option => Control,
269 Control => Command,
270 Command => Extra1,
271 Extra1 => Extra2,
272 Extra2 => CapsLock,
273 CapsLock => NumLock,
274 NumLock => Keypad,
275 Keypad => return None,
276 })
277 }
278}
279
280impl From<Modifier> for Modifiers {
281 fn from(value: Modifier) -> Self {
282 Self(value as u16)
283 }
284}
285
286impl<M: Into<Modifiers>> core::ops::Add<M> for Modifier {
287 type Output = Modifiers;
288
289 fn add(self, rhs: M) -> Self::Output {
290 Modifiers::combine(self as u16, rhs.into().0)
291 }
292}
293
294impl<M: Into<Modifiers>> core::ops::Add<M> for Modifiers {
295 type Output = Modifiers;
296
297 fn add(self, rhs: M) -> Self::Output {
298 Self::combine(self.0, rhs.into().0)
299 }
300}
301
302impl<M: Into<Modifiers>> core::ops::AddAssign<M> for Modifiers {
303 fn add_assign(&mut self, rhs: M) {
304 *self = Self::combine(self.0, rhs.into().0)
305 }
306}
307
308impl<M: Into<Modifiers>> core::ops::Sub<M> for Modifier {
309 type Output = Modifiers;
310
311 fn sub(self, rhs: M) -> Self::Output {
312 Modifiers(self as u16 & !rhs.into().0)
313 }
314}
315
316impl<M: Into<Modifiers>> core::ops::Sub<M> for Modifiers {
317 type Output = Modifiers;
318
319 fn sub(self, rhs: M) -> Self::Output {
320 Self(self.0 & !rhs.into().0)
321 }
322}
323
324impl<M: Into<Modifiers>> core::ops::SubAssign<M> for Modifiers {
325 fn sub_assign(&mut self, rhs: M) {
326 *self = Self(self.0 & !rhs.into().0)
327 }
328}
329
330/// A key's logical modifiers.
331///
332/// This struct combines zero or more logical modifiers. In fact, it defaults to
333/// zero modifiers.
334#[derive(Clone, Copy, Default, PartialEq, Eq)]
335pub struct Modifiers(u16);
336
337impl Modifiers {
338 fn combine(left: u16, right: u16) -> Self {
339 Self(left | right)
340 }
341
342 /// Decode an ANSI escape parameter.
343 pub fn from_escape(code: u16) -> Option<Self> {
344 let code = code.saturating_sub(1);
345 if code <= (u8::MAX as u16) {
346 Some(Self(code))
347 } else {
348 None
349 }
350 }
351
352 /// Determine whether there are no active modifiers.
353 pub const fn is_empty(&self) -> bool {
354 self.0 == 0
355 }
356
357 /// Get the number of active modifiers.
358 pub const fn len(&self) -> usize {
359 self.0.count_ones() as usize
360 }
361
362 /// Determine whether the given modifier is enabled.
363 pub fn has(&self, modifier: Modifier) -> bool {
364 self.0 & modifier as u16 != 0
365 }
366
367 /// Get an iterator over the active modifiers.
368 pub fn modifiers(&self) -> ModifierIter {
369 ModifierIter {
370 modifiers: *self,
371 cursor: None,
372 remaining: self.len(),
373 }
374 }
375}
376
377impl core::fmt::Debug for Modifiers {
378 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
379 f.debug_set().entries(self.modifiers()).finish()
380 }
381}
382
383/// An iterator over modifiers.
384#[derive(Debug)]
385pub struct ModifierIter {
386 modifiers: Modifiers,
387 cursor: Option<Modifier>,
388 remaining: usize,
389}
390
391impl Iterator for ModifierIter {
392 type Item = Modifier;
393
394 fn next(&mut self) -> Option<Self::Item> {
395 loop {
396 let modifier = match self.cursor {
397 None => Modifier::Shift,
398 Some(Modifier::NumLock) => return None,
399 Some(modifier) => modifier
400 .successor()
401 .expect("with no-successor case already handled, successor must exist"),
402 };
403 self.cursor = Some(modifier);
404
405 if self.modifiers.has(modifier) {
406 self.remaining -= 1;
407 return Some(modifier);
408 }
409 }
410 }
411
412 fn size_hint(&self) -> (usize, Option<usize>) {
413 (self.remaining, Some(self.remaining))
414 }
415}
416
417impl ExactSizeIterator for ModifierIter {
418 fn len(&self) -> usize {
419 self.remaining
420 }
421}
422
423impl core::iter::FusedIterator for ModifierIter {}
424
425// -------------------------------------------------------------------------------------
426
427/// A key.
428#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
429#[non_exhaustive]
430pub enum Key {
431 Escape,
432 Enter,
433 Tab,
434 Backspace,
435 Insert,
436 Delete,
437 Left,
438 Right,
439 Up,
440 Down,
441 PageUp,
442 PageDown,
443 Home,
444 End,
445 CapsLock,
446 ScrollLock,
447 NumLock,
448 PrintScreen,
449 Pause,
450 Menu,
451 KeypadBegin,
452 F(u8),
453 Media(MediaKey),
454 Mod(ModifierKey),
455 Char(char),
456}
457
458impl Key {
459 /// Get the key corresponding to SS3/CSI-1 and the given byte.
460 pub fn with_ss3(byte: u8) -> Option<Self> {
461 use Key::*;
462
463 let key = match byte {
464 b' ' => Char(' '),
465 b'A' => Up,
466 b'B' => Down,
467 b'C' => Right,
468 b'D' => Left,
469 b'E' => KeypadBegin,
470 b'F' => End,
471 b'H' => Home,
472 b'I' => Tab,
473 b'M' => Enter,
474 b'P' => F(1),
475 b'Q' => F(2),
476 b'R' => F(3),
477 b'S' => F(4),
478 b'X' => Char('='),
479 b'j' => Char('*'),
480 b'k' => Char('+'),
481 b'l' => Char(','),
482 b'm' => Char('-'),
483 b'n' => Char('.'),
484 b'o' => Char('/'),
485 b'p' => Char('0'),
486 b'q' => Char('1'),
487 b'r' => Char('2'),
488 b's' => Char('3'),
489 b't' => Char('4'),
490 b'u' => Char('5'),
491 b'v' => Char('6'),
492 b'w' => Char('7'),
493 b'x' => Char('8'),
494 b'y' => Char('9'),
495 _ => return None,
496 };
497
498 Some(key)
499 }
500
501 /// Map a function key code to a key.
502 ///
503 /// For a DECFNK escape sequence, that is, CSI *Pcode* ; *Pmod* ~, this
504 /// method maps the code parameter to a key.
505 pub fn with_function_key(code: u8) -> Option<Self> {
506 use Key::*;
507
508 // https://www.xfree86.org/current/ctlseqs.html
509 // https://vt100.net/docs/vt510-rm/DECFNK.html
510 let key = match code {
511 1 => Home,
512 2 => Insert,
513 3 => Delete,
514 4 => End,
515 5 => PageUp,
516 6 => PageDown,
517 7 => Left, // kitty incorrectly assigns Home
518 8 => Down, // kitty incorrectly assigns End
519 9 => Up,
520 10 => Right,
521 11..=15 => F(code - 10), // F1..=F5
522 17..=21 => F(code - 11), // F6..=F10
523 23..=26 => F(code - 12), // F11..=F14
524 28..=29 => F(code - 13), // F15..=F16
525 31..=34 => F(code - 14), // F17..=F20
526 _ => return None,
527 };
528
529 Some(key)
530 }
531
532 /// Map the CSI/u key code to a key.
533 pub fn with_csi_u(code: u16) -> Option<(Self, Modifiers)> {
534 use Key::*;
535
536 let key = match code {
537 9 => Tab,
538 13 => Enter,
539 27 => Escape,
540 127 => Backspace,
541
542 57_358 => CapsLock,
543 57_359 => ScrollLock,
544 57_360 => NumLock,
545 57_361 => PrintScreen,
546 57_362 => Pause,
547 57_363 => Menu,
548 57_376..=57_398 => F((code - 57_376) as u8 + 13),
549 // Keypad keys
550 57_399..=57_408 => Char((b'0' + (code - 57_399) as u8) as char),
551 57_409 => Char('.'),
552 57_410 => Char('/'),
553 57_411 => Char('*'),
554 57_412 => Char('-'),
555 57_413 => Char('+'),
556 57_414 => Enter,
557 57_415 => Char('='),
558 57_416 => Char(','),
559 57_417 => Left,
560 57_418 => Right,
561 57_419 => Up,
562 57_420 => Down,
563 57_421 => PageUp,
564 57_422 => PageDown,
565 57_423 => Home,
566 57_424 => End,
567 57_425 => Insert,
568 57_426 => Delete,
569 57_427 => KeypadBegin,
570 // Media and modifier keys
571 57_428..=57_440 => Media(MediaKey::with_csi_u(code)?),
572 57_441..=57_452 => Mod(ModifierKey::with_csi_u(code)?),
573 _ => return None,
574 };
575
576 let modifiers = if (57_399..=57_427).contains(&code) {
577 self::Modifier::Keypad.into()
578 } else {
579 Modifiers::default()
580 };
581
582 Some((key, modifiers))
583 }
584}
585
586/// A media key.
587#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
588pub enum MediaKey {
589 Play,
590 Pause,
591 PlayPause,
592 Reverse,
593 Stop,
594 FastForward,
595 Rewind,
596 NextTrack,
597 PreviousTrack,
598 Record,
599 LowerVolume,
600 RaiseVolume,
601 MuteVolume,
602}
603
604impl MediaKey {
605 /// Map the CSI/u key code to a media key.
606 pub fn with_csi_u(code: u16) -> Option<Self> {
607 use MediaKey::*;
608
609 let key = match code {
610 57_428 => Play,
611 57_429 => Pause,
612 57_430 => PlayPause,
613 57_431 => Reverse,
614 57_432 => Stop,
615 57_433 => FastForward,
616 57_434 => Rewind,
617 57_435 => NextTrack,
618 57_436 => PreviousTrack,
619 57_437 => Record,
620 57_438 => LowerVolume,
621 57_439 => RaiseVolume,
622 57_440 => MuteVolume,
623 _ => return None,
624 };
625
626 Some(key)
627 }
628}
629
630/// A physical modifier key.
631///
632/// This enumeration comprises physical keys instead of logical [`Modifier`]s.
633#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
634pub enum ModifierKey {
635 LeftShift,
636 LeftControl,
637 LeftOption,
638 LeftCommand,
639 LeftExtra1,
640 LeftExtra2,
641 RightShift,
642 RightControl,
643 RightOption,
644 RightCommand,
645 RightExtra1,
646 RightExtra2,
647}
648
649impl ModifierKey {
650 /// Map the CSI/u key code to a modifier key.
651 pub fn with_csi_u(code: u16) -> Option<Self> {
652 use ModifierKey::*;
653
654 let key = match code {
655 57_441 => LeftShift,
656 57_442 => LeftControl,
657 57_443 => LeftOption,
658 57_444 => LeftCommand,
659 57_445 => LeftExtra1,
660 57_446 => LeftExtra2,
661 57_447 => RightShift,
662 57_448 => RightControl,
663 57_449 => RightOption,
664 57_450 => RightCommand,
665 57_451 => RightExtra1,
666 57_452 => RightExtra2,
667 _ => return None,
668 };
669
670 Some(key)
671 }
672
673 /// Convert this modifier key into a logical modifier flag.
674 pub fn as_modifier(&self) -> Modifier {
675 use ModifierKey::*;
676
677 match *self {
678 LeftShift => Modifier::Shift,
679 LeftControl => Modifier::Control,
680 LeftOption => Modifier::Option,
681 LeftCommand => Modifier::Command,
682 LeftExtra1 => Modifier::Extra1,
683 LeftExtra2 => Modifier::Extra2,
684 RightShift => Modifier::Shift,
685 RightControl => Modifier::Control,
686 RightOption => Modifier::Option,
687 RightCommand => Modifier::Command,
688 RightExtra1 => Modifier::Extra1,
689 RightExtra2 => Modifier::Extra2,
690 }
691 }
692}
693
694/// An event to indicate a change in terminal size.
695#[derive(Clone, Copy, Debug, PartialEq, Eq)]
696pub struct WindowEvent {
697 pub columns: u16,
698 pub rows: u16,
699}
700
701/// An event to indicate a cursor position.
702#[derive(Clone, Copy, Debug, PartialEq, Eq)]
703pub struct CursorEvent {
704 pub column: u16,
705 pub row: u16,
706}
707
708/// An event to indicate a concrete color.
709#[derive(Clone, Copy, Debug, PartialEq, Eq)]
710pub struct ColorEvent {
711 pub kind: RequestColor,
712 /// The value has three components, with each component comprising a
713 /// magnitude and a maximum value (which is either 0xff, 0xffff, 0xffffff,
714 /// or 0xffff_ffff).
715 pub value: [(u16, u16); 3],
716}