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