Skip to main content

rich_interact/
event.rs

1//! Input events: keys, mouse, resizes, pastes and ticks.
2//!
3//! These are the crate's own types, so a component never sees `crossterm`,
4//! termion or termwiz, and a test can write `Key::parse("ctrl+c")` instead
5//! of building one. Each library's events translate here:
6//! [`from_crossterm`], and behind their features `from_termion` and
7//! `from_termwiz`.
8
9use std::fmt;
10
11#[cfg(all(unix, feature = "termion"))]
12mod via_termion;
13#[cfg(all(unix, feature = "termion"))]
14pub use via_termion::from_termion;
15#[cfg(feature = "termwiz")]
16mod via_termwiz;
17#[cfg(feature = "termwiz")]
18pub use via_termwiz::{from_termwiz, from_termwiz_kitty};
19
20/// Modifier keys held with a key or mouse event.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22pub struct Modifiers {
23    pub shift: bool,
24    pub ctrl: bool,
25    pub alt: bool,
26}
27
28impl Modifiers {
29    pub const NONE: Modifiers = Modifiers {
30        shift: false,
31        ctrl: false,
32        alt: false,
33    };
34    pub const CTRL: Modifiers = Modifiers {
35        shift: false,
36        ctrl: true,
37        alt: false,
38    };
39}
40
41/// A key, without its modifiers.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub enum KeyCode {
44    Char(char),
45    Enter,
46    Tab,
47    /// Shift+Tab, as terminals send it.
48    BackTab,
49    Backspace,
50    Delete,
51    Insert,
52    Escape,
53    Up,
54    Down,
55    Left,
56    Right,
57    Home,
58    End,
59    PageUp,
60    PageDown,
61    F(u8),
62    /// The menu key (the context-menu key beside the right Ctrl), parsed
63    /// as `menu`. Terminals with the kitty keyboard protocol report it;
64    /// others send nothing for it, so a binding on it usually has
65    /// Shift+F10 beside it.
66    Menu,
67    /// A modifier key on its own, parsed as `shift`, `ctrl`, `alt` or
68    /// `super`: only a terminal with the kitty keyboard protocol asked to
69    /// report every key reports these
70    /// ([`SessionOptions::all_keys`](crate::SessionOptions::all_keys)).
71    /// Either side's key is the same one.
72    Modifier(ModifierKey),
73}
74
75/// A modifier key pressed or let go on its own ([`KeyCode::Modifier`]).
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub enum ModifierKey {
78    Shift,
79    Ctrl,
80    Alt,
81    /// The Windows, Command or Super key.
82    Super,
83}
84
85/// Which key releases ([`Event::KeyUp`]) a terminal reports, so an app
86/// knows whether it can count on them: a game that moves while a key is
87/// held falls back on the terminal's repeats, or on releases it makes up
88/// itself, for the keys it is not told about.
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
90pub enum Releases {
91    /// None: a terminal without the kitty keyboard protocol (or a backend
92    /// that does not read releases).
93    #[default]
94    None,
95    /// Every key's but the text keys': characters typed without Ctrl or
96    /// Alt (Space and Shift with a letter too), Enter, Tab and Backspace.
97    /// The kitty keyboard protocol as a session pushes it by default.
98    ExceptText,
99    /// Every key's: the kitty keyboard protocol asked to report every key
100    /// ([`SessionOptions::all_keys`](crate::SessionOptions::all_keys)),
101    /// and the Windows console.
102    All,
103}
104
105impl Releases {
106    /// Whether the release of `key` is reported.
107    ///
108    /// ```
109    /// use rich_interact::{Key, Releases};
110    ///
111    /// let up = Key::parse("up").unwrap();
112    /// let w = Key::parse("w").unwrap();
113    /// assert!(Releases::ExceptText.reported(&up));
114    /// assert!(!Releases::ExceptText.reported(&w));
115    /// assert!(Releases::All.reported(&w));
116    /// assert!(!Releases::None.reported(&up));
117    /// ```
118    pub fn reported(self, key: &Key) -> bool {
119        match self {
120            Releases::None => false,
121            Releases::All => true,
122            Releases::ExceptText => {
123                let text = match key.code {
124                    KeyCode::Char(_) => !key.modifiers.ctrl && !key.modifiers.alt,
125                    KeyCode::Enter | KeyCode::Tab | KeyCode::Backspace => true,
126                    // Reported only with every key.
127                    KeyCode::Modifier(_) => true,
128                    _ => false,
129                };
130                !text
131            }
132        }
133    }
134}
135
136/// A key press.
137///
138/// Two keys are equal when their code and modifiers are. How the terminal
139/// reported one changes only which bindings it [`matches`](Key::matches):
140/// a terminal without the kitty keyboard protocol (a legacy terminal) sends
141/// one byte for Tab and for Ctrl+I, so its Tab could be either key, while a
142/// key marked [`exact`](Key::exact) is only itself.
143#[derive(Clone, Copy, Debug)]
144pub struct Key {
145    pub code: KeyCode,
146    pub modifiers: Modifiers,
147    /// Reported by a terminal that tells every key apart.
148    exact: bool,
149}
150
151impl PartialEq for Key {
152    fn eq(&self, other: &Key) -> bool {
153        self.code == other.code && self.modifiers == other.modifiers
154    }
155}
156
157impl Eq for Key {}
158
159impl std::hash::Hash for Key {
160    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
161        self.code.hash(state);
162        self.modifiers.hash(state);
163    }
164}
165
166impl Key {
167    pub const fn new(code: KeyCode) -> Key {
168        Key::with(code, Modifiers::NONE)
169    }
170
171    /// A key and the modifiers held with it.
172    pub const fn with(code: KeyCode, modifiers: Modifiers) -> Key {
173        Key {
174            code,
175            modifiers,
176            exact: false,
177        }
178    }
179
180    pub const fn ctrl(c: char) -> Key {
181        Key::with(KeyCode::Char(c), Modifiers::CTRL)
182    }
183
184    pub const fn char(c: char) -> Key {
185        Key::new(KeyCode::Char(c))
186    }
187
188    /// Parse a key name such as `enter`, `ctrl+c`, `shift+tab`, `pagedown`,
189    /// `f5` or a single character. Case does not matter for names. The
190    /// comma key is `,` or `comma` (`ctrl+comma`), the name a list of key
191    /// names can hold without quotes; the other punctuation keys have names
192    /// too, for where the character is awkward (a key list, a TOML string,
193    /// `ctrl+minus`): see [`punctuation`]. A key named so is the character,
194    /// and shows as it (`equals` is `=`).
195    ///
196    /// A name means the key: `ctrl+i` is Ctrl+I, not Tab, although a legacy
197    /// terminal sends the two alike; [`matches`](Key::matches) is what lets
198    /// a Tab from such a terminal fire a `ctrl+i` binding.
199    pub fn parse(text: &str) -> Option<Key> {
200        if text == "+" {
201            return Some(Key::char('+'));
202        }
203        let mut modifiers = Modifiers::NONE;
204        let mut parts: Vec<&str> = text.split('+').collect();
205        // `ctrl++` is Ctrl and the plus key.
206        if text.ends_with("++") {
207            parts.truncate(parts.len() - 2);
208            parts.push("+");
209        }
210        let (name, held) = parts.split_last()?;
211        for modifier in held {
212            match modifier.to_ascii_lowercase().as_str() {
213                "ctrl" | "control" => modifiers.ctrl = true,
214                "alt" | "meta" => modifiers.alt = true,
215                "shift" => modifiers.shift = true,
216                _ => return None,
217            }
218        }
219        let mut chars = name.chars();
220        let code = match (chars.next(), chars.next()) {
221            // Shift with a letter is the capital letter, with Ctrl too
222            // (`ctrl+shift+a`), as the key arrives. Shift with anything
223            // else (`shift+1`) depends on the keyboard layout, and is not a
224            // key name.
225            (Some(c), None) if modifiers.shift => {
226                if !c.is_alphabetic() {
227                    return None;
228                }
229                modifiers.shift = false;
230                KeyCode::Char(c.to_uppercase().next().unwrap_or(c))
231            }
232            // Ctrl with a letter of either case is the lower-case letter:
233            // `ctrl+A` is Ctrl+A, and Ctrl+Shift+A is `ctrl+shift+a`.
234            (Some(c), None) if modifiers.ctrl => KeyCode::Char(c.to_ascii_lowercase()),
235            (Some(c), None) => KeyCode::Char(c),
236            _ => match name.to_ascii_lowercase().as_str() {
237                "enter" | "return" => KeyCode::Enter,
238                "tab" if modifiers.shift => {
239                    modifiers.shift = false;
240                    KeyCode::BackTab
241                }
242                "tab" => KeyCode::Tab,
243                "backtab" => KeyCode::BackTab,
244                "backspace" => KeyCode::Backspace,
245                "delete" | "del" => KeyCode::Delete,
246                "insert" => KeyCode::Insert,
247                "menu" => KeyCode::Menu,
248                "shift" => KeyCode::Modifier(ModifierKey::Shift),
249                "ctrl" | "control" => KeyCode::Modifier(ModifierKey::Ctrl),
250                "alt" | "meta" => KeyCode::Modifier(ModifierKey::Alt),
251                "super" | "cmd" | "win" => KeyCode::Modifier(ModifierKey::Super),
252                "escape" | "esc" => KeyCode::Escape,
253                "space" => KeyCode::Char(' '),
254                // A list of key names is separated by commas too.
255                "comma" => KeyCode::Char(','),
256                // Shift with punctuation depends on the layout, as above.
257                other if punctuation(other).is_some() && modifiers.shift => return None,
258                other if punctuation(other).is_some() => KeyCode::Char(punctuation(other)?),
259                "up" => KeyCode::Up,
260                "down" => KeyCode::Down,
261                "left" => KeyCode::Left,
262                "right" => KeyCode::Right,
263                "home" => KeyCode::Home,
264                "end" => KeyCode::End,
265                "pageup" => KeyCode::PageUp,
266                "pagedown" => KeyCode::PageDown,
267                other => match other.strip_prefix('f')?.parse().ok()? {
268                    // The function keys terminals send.
269                    n @ 1..=24 => KeyCode::F(n),
270                    _ => return None,
271                },
272            },
273        };
274        Some(Key::with(code, modifiers))
275    }
276
277    /// This key as a terminal that tells every key apart reports it (the
278    /// kitty keyboard protocol): it [`matches`](Key::matches) a binding for
279    /// itself only. A [`Session`](crate::Session) with the protocol on
280    /// reads keys so, and a test marks them to stand for such a terminal.
281    #[must_use]
282    pub const fn exact(self) -> Key {
283        Key {
284            exact: true,
285            ..self
286        }
287    }
288
289    /// Whether the key came from a terminal that tells every key apart.
290    pub const fn is_exact(&self) -> bool {
291        self.exact
292    }
293
294    /// This key as a legacy terminal sends it: Ctrl+I arrives as Tab,
295    /// Ctrl+M as Enter, Ctrl+[ as Esc, Ctrl+\ ] ^ _ as Ctrl+4 to 7, Ctrl+@
296    /// as Ctrl+Space, and Ctrl+Shift with a letter as Ctrl and the letter.
297    /// Any other key arrives as itself. The result is not
298    /// [`exact`](Key::exact).
299    #[must_use]
300    pub fn legacy(self) -> Key {
301        let mut key = Key {
302            exact: false,
303            ..self
304        };
305        let KeyCode::Char(c) = key.code else {
306            return key;
307        };
308        if !key.modifiers.ctrl {
309            return key;
310        }
311        key.code = match c.to_ascii_lowercase() {
312            'i' => KeyCode::Tab,
313            'm' => KeyCode::Enter,
314            '[' => KeyCode::Escape,
315            '\\' => KeyCode::Char('4'),
316            ']' => KeyCode::Char('5'),
317            '^' => KeyCode::Char('6'),
318            '_' => KeyCode::Char('7'),
319            '@' => KeyCode::Char(' '),
320            c => KeyCode::Char(c),
321        };
322        // Tab, Enter and Esc are bytes of their own, without Ctrl.
323        if matches!(key.code, KeyCode::Tab | KeyCode::Enter | KeyCode::Escape) {
324            key.modifiers.ctrl = false;
325        }
326        key
327    }
328
329    /// Whether this key, as read, fires a binding for `binding`. A key
330    /// fires a binding for itself. One from a legacy terminal also fires a
331    /// binding for any key that terminal sends alike (see
332    /// [`legacy`](Key::legacy)): its Tab fires `tab` or `ctrl+i`; and its
333    /// Ctrl+H, which some terminals send for Backspace, fires `ctrl+h` or
334    /// `backspace`. An [`exact`](Key::exact) key fires only its own.
335    pub fn matches(&self, binding: &Key) -> bool {
336        if self == binding {
337            return true;
338        }
339        if self.exact {
340            return false;
341        }
342        if binding.legacy() == *self {
343            return true;
344        }
345        binding.code == KeyCode::Backspace
346            && !binding.modifiers.ctrl
347            && self.code == KeyCode::Char('h')
348            && self.modifiers
349                == Modifiers {
350                    ctrl: true,
351                    ..binding.modifiers
352                }
353    }
354
355    /// Whether this key fires a binding for any of `bindings`.
356    pub fn matches_any<'a>(&self, bindings: impl IntoIterator<Item = &'a Key>) -> bool {
357        bindings.into_iter().any(|binding| self.matches(binding))
358    }
359
360    /// Which of `sets` (each the keys of one binding) this key fires: the
361    /// first that has it exactly, else the first it
362    /// [`matches`](Key::matches). A Tab from a legacy terminal picks a
363    /// `tab` binding over an earlier `ctrl+i` one.
364    pub fn pick<'a>(&self, sets: impl IntoIterator<Item = &'a [Key]>) -> Option<usize> {
365        let sets: Vec<&[Key]> = sets.into_iter().collect();
366        sets.iter()
367            .position(|keys| keys.contains(self))
368            .or_else(|| sets.iter().position(|keys| self.matches_any(*keys)))
369    }
370
371    /// Ctrl+C: the event loop ends every component with
372    /// [`Outcome::Interrupted`](crate::Outcome::Interrupted) on it.
373    pub fn is_interrupt(&self) -> bool {
374        self.modifiers.ctrl && self.code == KeyCode::Char('c')
375    }
376}
377
378/// The character a punctuation key's name stands for: `equals` `=`,
379/// `minus` (or `dash`) `-`, `plus` `+`, `underscore` `_`, `slash` `/`,
380/// `backslash` `\`, `period` (or `dot`) `.`, `semicolon` `;`, `colon` `:`,
381/// `quote` (or `apostrophe`) `'`, `doublequote` `"`, `backtick` (or
382/// `grave`) `` ` ``, `tilde` `~`, `bracketleft` `[`, `bracketright` `]`,
383/// `braceleft` `{`, `braceright` `}`, `parenleft` `(`, `parenright` `)`,
384/// `lt` (or `less`) `<`, `gt` (or `greater`) `>`, `question` `?`, `exclam`
385/// (or `bang`) `!`, `at` `@`, `hash` `#`, `dollar` `$`, `percent` `%`,
386/// `caret` `^`, `ampersand` `&`, `asterisk` (or `star`) `*`, `pipe` `|`,
387/// and `comma` `,`. Lower case; [`Key::parse`] takes any case.
388///
389/// ```
390/// use rich_interact::event::punctuation;
391/// use rich_interact::Key;
392///
393/// assert_eq!(punctuation("equals"), Some('='));
394/// assert_eq!(Key::parse("ctrl+minus"), Some(Key::ctrl('-')));
395/// assert_eq!(Key::parse("question").unwrap().to_string(), "?");
396/// ```
397pub fn punctuation(name: &str) -> Option<char> {
398    Some(match name {
399        "equals" => '=',
400        "minus" | "dash" => '-',
401        "plus" => '+',
402        "underscore" => '_',
403        "slash" => '/',
404        "backslash" => '\\',
405        "period" | "dot" => '.',
406        "comma" => ',',
407        "semicolon" => ';',
408        "colon" => ':',
409        "quote" | "apostrophe" => '\'',
410        "doublequote" => '"',
411        "backtick" | "grave" => '`',
412        "tilde" => '~',
413        "bracketleft" => '[',
414        "bracketright" => ']',
415        "braceleft" => '{',
416        "braceright" => '}',
417        "parenleft" => '(',
418        "parenright" => ')',
419        "lt" | "less" => '<',
420        "gt" | "greater" => '>',
421        "question" => '?',
422        "exclam" | "bang" => '!',
423        "at" => '@',
424        "hash" => '#',
425        "dollar" => '$',
426        "percent" => '%',
427        "caret" => '^',
428        "ampersand" => '&',
429        "asterisk" | "star" => '*',
430        "pipe" => '|',
431        _ => return None,
432    })
433}
434
435impl fmt::Display for Key {
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        if self.modifiers.ctrl {
438            f.write_str("ctrl+")?;
439        }
440        if self.modifiers.alt {
441            f.write_str("alt+")?;
442        }
443        // Shift+Tab is written as it is parsed and documented: `shift+tab`,
444        // and so is Ctrl with a capital letter: `ctrl+shift+a`.
445        let capital =
446            matches!(self.code, KeyCode::Char(c) if self.modifiers.ctrl && c.is_ascii_uppercase());
447        if self.modifiers.shift || self.code == KeyCode::BackTab || capital {
448            f.write_str("shift+")?;
449        }
450        match self.code {
451            KeyCode::BackTab => f.write_str("tab"),
452            KeyCode::Char(' ') => f.write_str("space"),
453            KeyCode::Char(c) if capital => write!(f, "{}", c.to_ascii_lowercase()),
454            KeyCode::Char(c) => write!(f, "{c}"),
455            KeyCode::F(n) => write!(f, "f{n}"),
456            KeyCode::Modifier(key) => f.write_str(match key {
457                ModifierKey::Shift => "shift",
458                ModifierKey::Ctrl => "ctrl",
459                ModifierKey::Alt => "alt",
460                ModifierKey::Super => "super",
461            }),
462            code => f.write_str(&format!("{code:?}").to_ascii_lowercase()),
463        }
464    }
465}
466
467/// A mouse button.
468#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
469pub enum Button {
470    Left,
471    Right,
472    Middle,
473}
474
475/// What the mouse did.
476#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
477pub enum MouseKind {
478    Down(Button),
479    Up(Button),
480    Drag(Button),
481    Moved,
482    ScrollUp,
483    ScrollDown,
484}
485
486/// A mouse event, at a column and row (0-based). The event loop reports
487/// them to a component relative to the top left of its own view (#476), so
488/// row 0 is the view's first line wherever the view is on screen.
489#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
490pub struct Mouse {
491    pub kind: MouseKind,
492    pub column: u16,
493    pub row: u16,
494    pub modifiers: Modifiers,
495    /// Where in its cell the pointer was, from a terminal that reports the
496    /// mouse in pixels (SGR-Pixels, mode 1016; #819); `None` from one that
497    /// reports cells. It is relative to the cell, not the screen, so it
498    /// stays right however the column and row are moved into a view's or
499    /// a node's own coordinates. [`precise`](Self::precise) reads it.
500    pub pixel: Option<Subcell>,
501}
502
503/// Where in a cell a pixel-precise mouse event was ([`Mouse::pixel`]): the
504/// pixel's offset from the cell's top left corner, and the cell's size in
505/// pixels.
506#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
507pub struct Subcell {
508    /// Pixels from the cell's left edge, below [`width`](Self::width).
509    pub x: u16,
510    /// Pixels from the cell's top edge, below [`height`](Self::height).
511    pub y: u16,
512    /// The cell's width in pixels, never 0.
513    pub width: u16,
514    /// The cell's height in pixels, never 0.
515    pub height: u16,
516}
517
518impl Mouse {
519    pub const fn new(kind: MouseKind, column: u16, row: u16) -> Mouse {
520        Mouse {
521            kind,
522            column,
523            row,
524            modifiers: Modifiers::NONE,
525            pixel: None,
526        }
527    }
528
529    /// A mouse event a terminal reported in pixels (SGR-Pixels, mode
530    /// 1016): `x` and `y` from the screen's top left, 0-based, in cells of
531    /// `cell` pixels (width, height). `None` for a cell size of 0.
532    ///
533    /// ```
534    /// use rich_interact::{Mouse, MouseKind, Button, Subcell};
535    ///
536    /// // 10 by 20 pixel cells: pixel (25, 47) is in column 2, row 2.
537    /// let mouse = Mouse::from_pixels(MouseKind::Down(Button::Left), 25, 47, (10, 20)).unwrap();
538    /// assert_eq!((mouse.column, mouse.row), (2, 2));
539    /// assert_eq!(mouse.pixel, Some(Subcell { x: 5, y: 7, width: 10, height: 20 }));
540    /// let (column, row) = mouse.precise();
541    /// assert_eq!(format!("{column:.2} {row:.2}"), "2.50 2.35");
542    /// ```
543    pub fn from_pixels(kind: MouseKind, x: u32, y: u32, cell: (u16, u16)) -> Option<Mouse> {
544        let (width, height) = cell;
545        if width == 0 || height == 0 {
546            return None;
547        }
548        let (w, h) = (u32::from(width), u32::from(height));
549        let cells = |at: u32, size: u32| u16::try_from(at / size).unwrap_or(u16::MAX);
550        Some(Mouse {
551            pixel: Some(Subcell {
552                x: (x % w) as u16,
553                y: (y % h) as u16,
554                width,
555                height,
556            }),
557            ..Mouse::new(kind, cells(x, w), cells(y, h))
558        })
559    }
560
561    /// Where the pointer was, in fractional cells (column, row) of the
562    /// same coordinates as [`column`](Self::column) and [`row`](Self::row):
563    /// to the pixel from a pixel-precise terminal, else the cell's centre
564    /// (`column + 0.5`, `row + 0.5`).
565    ///
566    /// ```
567    /// use rich_interact::{Mouse, MouseKind};
568    ///
569    /// assert_eq!(Mouse::new(MouseKind::Moved, 3, 1).precise(), (3.5, 1.5));
570    /// ```
571    pub fn precise(&self) -> (f32, f32) {
572        match self.pixel {
573            Some(p) if p.width > 0 && p.height > 0 => (
574                f32::from(self.column) + f32::from(p.x) / f32::from(p.width),
575                f32::from(self.row) + f32::from(p.y) / f32::from(p.height),
576            ),
577            _ => (f32::from(self.column) + 0.5, f32::from(self.row) + 0.5),
578        }
579    }
580
581    /// The same event read as a pixel report: a terminal with mode 1016 on
582    /// sends pixels where the column and row are, and a library that
583    /// parses SGR mouse reports (crossterm, termion, termwiz) reads them
584    /// as cells. `None` for a cell size of 0.
585    pub fn reread_as_pixels(self, cell: (u16, u16)) -> Option<Mouse> {
586        Mouse::from_pixels(self.kind, u32::from(self.column), u32::from(self.row), cell).map(
587            |read| Mouse {
588                modifiers: self.modifiers,
589                ..read
590            },
591        )
592    }
593
594    /// A press of the left button: the start of a click.
595    pub fn is_click(&self) -> bool {
596        self.kind == MouseKind::Down(Button::Left)
597    }
598}
599
600/// One input event.
601#[derive(Clone, Debug, PartialEq, Eq)]
602pub enum Event {
603    /// A key pressed, or held down long enough to repeat.
604    Key(Key),
605    /// A key let go. Terminals report it with the kitty keyboard protocol
606    /// on (see [`SessionOptions::legacy_keys`](crate::SessionOptions)), and
607    /// the Windows console always. Nothing binds it: a component that wants
608    /// it matches it itself.
609    KeyUp(Key),
610    Mouse(Mouse),
611    /// The terminal is now this size.
612    Resize {
613        columns: u16,
614        rows: u16,
615    },
616    /// Text pasted with bracketed paste on.
617    Paste(String),
618    /// A component's tick interval passed (see
619    /// [`Component::tick`](crate::Component::tick)).
620    Tick,
621    /// A command the component handed the terminal to (with
622    /// [`Flow::Handoff`](crate::Flow::Handoff)) exited with this code
623    /// (`None`: killed by a signal).
624    Returned(Option<i32>),
625    /// The left button went down on a hyperlink (an OSC 8 region: a
626    /// [`Style`](rich::Style) with a link) in the component's view (#476).
627    /// Delivered instead of the [`Event::Mouse`]; what to do with the URL
628    /// (open it, print it, ignore it) is the component's or its caller's
629    /// choice, never the event loop's.
630    Link(String),
631}
632
633impl Event {
634    /// The key, when this is a key press.
635    pub fn key(&self) -> Option<Key> {
636        match self {
637            Event::Key(key) => Some(*key),
638            _ => None,
639        }
640    }
641}
642
643impl Event {
644    /// The mouse event, when this is one.
645    pub fn mouse(&self) -> Option<Mouse> {
646        match self {
647            Event::Mouse(mouse) => Some(*mouse),
648            _ => None,
649        }
650    }
651}
652
653impl From<Key> for Event {
654    fn from(key: Key) -> Event {
655        Event::Key(key)
656    }
657}
658
659/// The mouse button held down, for a library that does not say which
660/// button a release or a drag is of: termion names the button only when it
661/// is pressed, and termwiz reports which buttons are down rather than what
662/// changed. Keep one per terminal, and pass it to each translation.
663#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
664pub struct HeldButton(Option<Button>);
665
666impl HeldButton {
667    /// The button down, if any.
668    pub fn get(self) -> Option<Button> {
669        self.0
670    }
671}
672
673/// A character as it arrives from a terminal: a control character is the
674/// key that sends it (Enter, Tab, Esc, Backspace, or Ctrl with a letter,
675/// with Space for NUL and with 4 to 7 for the four after Esc). Any other
676/// character is itself.
677#[cfg(any(unix, feature = "termwiz"))]
678fn char_key(c: char, mut modifiers: Modifiers) -> Key {
679    let code = match c {
680        '\r' | '\n' => KeyCode::Enter,
681        '\t' => KeyCode::Tab,
682        '\x1b' => KeyCode::Escape,
683        '\x7f' => KeyCode::Backspace,
684        '\0' => {
685            modifiers.ctrl = true;
686            KeyCode::Char(' ')
687        }
688        '\x01'..='\x1a' => {
689            modifiers.ctrl = true;
690            KeyCode::Char((c as u8 - 1 + b'a') as char)
691        }
692        '\x1c'..='\x1f' => {
693            modifiers.ctrl = true;
694            KeyCode::Char((c as u8 - 0x1c + b'4') as char)
695        }
696        c => KeyCode::Char(c),
697    };
698    Key::with(code, modifiers)
699}
700
701/// The keys in `bytes` as a legacy terminal sends them (before the kitty
702/// keyboard protocol is pushed): characters, control characters, Alt with
703/// a character (Esc before it), and the cursor, editing and function keys
704/// as xterm's `CSI` and `SS3` sequences, with xterm's modifiers. A
705/// sequence it does not know, or one cut short at the end, is dropped.
706/// For bytes read before the library's reader opened (keys typed while the
707/// session waited for the terminal's answer).
708#[cfg(unix)]
709pub(crate) fn from_legacy_bytes(bytes: &[u8]) -> Vec<Event> {
710    let text = String::from_utf8_lossy(bytes);
711    let chars: Vec<char> = text.chars().collect();
712    let mut events = Vec::new();
713    let mut at = 0;
714    while at < chars.len() {
715        let c = chars[at];
716        if c != '\x1b' {
717            events.push(Event::Key(char_key(c, Modifiers::NONE)));
718            at += 1;
719            continue;
720        }
721        match chars.get(at + 1) {
722            None | Some('\x1b') => {
723                events.push(Event::Key(Key::new(KeyCode::Escape)));
724                at += 1;
725            }
726            Some('[') => {
727                let start = at + 2;
728                let mut end = start;
729                while chars
730                    .get(end)
731                    .is_some_and(|c| ('\x20'..='\x3f').contains(c))
732                {
733                    end += 1;
734                }
735                let Some(&last) = chars.get(end) else {
736                    break;
737                };
738                let parameters: String = chars[start..end].iter().collect();
739                if let Some(key) = csi_key(&parameters, last) {
740                    events.push(Event::Key(key));
741                }
742                at = end + 1;
743            }
744            Some('O') if at + 2 < chars.len() => {
745                if let Some(key) = csi_key("", chars[at + 2]) {
746                    events.push(Event::Key(key));
747                }
748                at += 3;
749            }
750            Some(&c) => {
751                let alt = Modifiers {
752                    alt: true,
753                    ..Modifiers::NONE
754                };
755                events.push(Event::Key(char_key(c, alt)));
756                at += 2;
757            }
758        }
759    }
760    events
761}
762
763/// The key a legacy `CSI <parameters> <last>` (or `SS3 <last>`) sequence
764/// stands for, with xterm's modifiers (`1;5A` is Ctrl+↑).
765#[cfg(unix)]
766fn csi_key(parameters: &str, last: char) -> Option<Key> {
767    let mut parts = parameters.split(';');
768    let first = parts.next().unwrap_or("");
769    let modifiers = match parts.next().map(str::parse::<u8>) {
770        Some(Ok(code)) => {
771            let bits = code.saturating_sub(1);
772            Modifiers {
773                shift: bits & 1 != 0,
774                alt: bits & 2 != 0,
775                ctrl: bits & 4 != 0,
776            }
777        }
778        Some(Err(_)) => return None,
779        None => Modifiers::NONE,
780    };
781    let code = match last {
782        'A' => KeyCode::Up,
783        'B' => KeyCode::Down,
784        'C' => KeyCode::Right,
785        'D' => KeyCode::Left,
786        'H' => KeyCode::Home,
787        'F' => KeyCode::End,
788        'P'..='S' => KeyCode::F(1 + (last as u8 - b'P')),
789        'Z' => return Some(Key::new(KeyCode::BackTab)),
790        '~' => match first.parse::<u8>().ok()? {
791            1 | 7 => KeyCode::Home,
792            2 => KeyCode::Insert,
793            3 => KeyCode::Delete,
794            4 | 8 => KeyCode::End,
795            5 => KeyCode::PageUp,
796            6 => KeyCode::PageDown,
797            n @ 11..=15 => KeyCode::F(n - 10),
798            n @ 17..=21 => KeyCode::F(n - 11),
799            n @ 23..=24 => KeyCode::F(n - 12),
800            _ => return None,
801        },
802        _ => return None,
803    };
804    Some(Key::with(code, modifiers))
805}
806
807#[cfg(not(target_arch = "wasm32"))]
808fn modifiers(state: crossterm::event::KeyModifiers) -> Modifiers {
809    use crossterm::event::KeyModifiers as M;
810    Modifiers {
811        shift: state.contains(M::SHIFT),
812        ctrl: state.contains(M::CONTROL),
813        alt: state.contains(M::ALT),
814    }
815}
816
817#[cfg(not(target_arch = "wasm32"))]
818fn button(button: crossterm::event::MouseButton) -> Button {
819    use crossterm::event::MouseButton as B;
820    match button {
821        B::Left => Button::Left,
822        B::Right => Button::Right,
823        B::Middle => Button::Middle,
824    }
825}
826
827/// Translate a crossterm event, for a loop of your own that reads the
828/// terminal with crossterm (or with ratatui, which re-exports it), in its
829/// legacy mode: keys may be others the terminal sends alike (see
830/// [`Key::matches`]). With the kitty protocol pushed, use
831/// [`from_crossterm_kitty`]. Repeats arrive as presses, releases as
832/// [`Event::KeyUp`]; focus changes are dropped.
833#[cfg(not(target_arch = "wasm32"))]
834pub fn from_crossterm(event: crossterm::event::Event) -> Option<Event> {
835    translate(event, false)
836}
837
838/// [`from_crossterm`] for a terminal with the kitty keyboard protocol
839/// pushed (`PushKeyboardEnhancementFlags` with at least
840/// `DISAMBIGUATE_ESCAPE_CODES`): every key is [`exact`](Key::exact).
841#[cfg(not(target_arch = "wasm32"))]
842pub fn from_crossterm_kitty(event: crossterm::event::Event) -> Option<Event> {
843    translate(event, true)
844}
845
846#[cfg(not(target_arch = "wasm32"))]
847fn translate(event: crossterm::event::Event, exact: bool) -> Option<Event> {
848    use crossterm::event::{
849        Event as E, KeyCode as K, KeyEventKind, KeyModifiers as M, MouseEventKind as MK,
850    };
851    Some(match event {
852        E::Key(key) if matches!(key.code, crossterm::event::KeyCode::Modifier(_)) => {
853            use crossterm::event::{KeyCode as K, ModifierKeyCode as MK};
854            let K::Modifier(which) = key.code else {
855                return None;
856            };
857            let code = KeyCode::Modifier(match which {
858                MK::LeftShift | MK::RightShift => ModifierKey::Shift,
859                MK::LeftControl | MK::RightControl => ModifierKey::Ctrl,
860                MK::LeftAlt | MK::RightAlt | MK::LeftMeta | MK::RightMeta => ModifierKey::Alt,
861                MK::LeftSuper | MK::RightSuper => ModifierKey::Super,
862                _ => return None,
863            });
864            // The key itself, not the modifiers it leaves held.
865            let mut read = Key::new(code);
866            read.exact = exact;
867            if key.kind == KeyEventKind::Release {
868                Event::KeyUp(read)
869            } else {
870                Event::Key(read)
871            }
872        }
873        E::Key(key) => {
874            // Super and Hyper, which only the kitty protocol reports, are
875            // not modifiers here: Super+A is not A.
876            if key.modifiers.intersects(M::SUPER | M::HYPER) {
877                return None;
878            }
879            let mut held = modifiers(key.modifiers);
880            held.alt |= key.modifiers.contains(M::META);
881            let code = match key.code {
882                K::Char(c) => {
883                    // An upper-case letter already says Shift; the kitty
884                    // protocol sends Ctrl+Shift+A as Shift and `a`.
885                    if held.shift && c.is_ascii_lowercase() {
886                        held.shift = false;
887                        KeyCode::Char(c.to_ascii_uppercase())
888                    } else {
889                        if c.is_uppercase() {
890                            held.shift = false;
891                        }
892                        KeyCode::Char(c)
893                    }
894                }
895                K::Enter => KeyCode::Enter,
896                K::Tab => KeyCode::Tab,
897                K::BackTab => {
898                    held.shift = false;
899                    KeyCode::BackTab
900                }
901                K::Backspace => KeyCode::Backspace,
902                K::Delete => KeyCode::Delete,
903                K::Insert => KeyCode::Insert,
904                K::Esc => KeyCode::Escape,
905                K::Up => KeyCode::Up,
906                K::Down => KeyCode::Down,
907                K::Left => KeyCode::Left,
908                K::Right => KeyCode::Right,
909                K::Home => KeyCode::Home,
910                K::End => KeyCode::End,
911                K::PageUp => KeyCode::PageUp,
912                K::PageDown => KeyCode::PageDown,
913                K::F(n) => KeyCode::F(n),
914                K::Menu => KeyCode::Menu,
915                _ => return None,
916            };
917            let mut read = Key::with(code, held);
918            read.exact = exact;
919            if key.kind == KeyEventKind::Release {
920                Event::KeyUp(read)
921            } else {
922                Event::Key(read)
923            }
924        }
925        E::Mouse(mouse) => Event::Mouse(Mouse {
926            kind: match mouse.kind {
927                MK::Down(b) => MouseKind::Down(button(b)),
928                MK::Up(b) => MouseKind::Up(button(b)),
929                MK::Drag(b) => MouseKind::Drag(button(b)),
930                MK::Moved => MouseKind::Moved,
931                MK::ScrollUp => MouseKind::ScrollUp,
932                MK::ScrollDown => MouseKind::ScrollDown,
933                _ => return None,
934            },
935            column: mouse.column,
936            row: mouse.row,
937            modifiers: modifiers(mouse.modifiers),
938            pixel: None,
939        }),
940        E::Resize(columns, rows) => Event::Resize { columns, rows },
941        E::Paste(text) => Event::Paste(text),
942        _ => return None,
943    })
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949
950    #[cfg(unix)]
951    #[test]
952    fn reads_legacy_bytes() {
953        let keys = |bytes: &[u8]| -> Vec<String> {
954            from_legacy_bytes(bytes)
955                .into_iter()
956                .map(|event| event.key().expect("a key").to_string())
957                .collect()
958        };
959        assert_eq!(
960            keys(b"qA \r\t\x7f\x03"),
961            ["q", "A", "space", "enter", "tab", "backspace", "ctrl+c"]
962        );
963        assert_eq!(
964            keys(b"\x1b[A\x1b[1;5C\x1bOP\x1b[5~\x1b[15~\x1b[Z"),
965            ["up", "ctrl+right", "f1", "pageup", "f5", "shift+tab"]
966        );
967        assert_eq!(keys("\x1bxé\x1b".as_bytes()), ["alt+x", "é", "escape"]);
968        // Unknown, and cut short: dropped.
969        assert_eq!(keys(b"a\x1b[99q b\x1b[1;"), ["a", "space", "b"]);
970    }
971
972    #[test]
973    fn parses_key_names() {
974        assert_eq!(Key::parse("enter"), Some(Key::new(KeyCode::Enter)));
975        assert_eq!(Key::parse("Ctrl+C"), Some(Key::ctrl('c')));
976        assert!(Key::parse("ctrl+c").unwrap().is_interrupt());
977        assert_eq!(Key::parse("shift+tab"), Some(Key::new(KeyCode::BackTab)));
978        assert_eq!(Key::parse("f12"), Some(Key::new(KeyCode::F(12))));
979        assert_eq!(Key::parse("space"), Some(Key::char(' ')));
980        assert_eq!(Key::parse("comma"), Some(Key::char(',')));
981        assert_eq!(Key::parse("ctrl+comma"), Some(Key::ctrl(',')));
982        assert_eq!(Key::parse("x"), Some(Key::char('x')));
983        assert_eq!(Key::parse("ctrl++").unwrap().code, KeyCode::Char('+'));
984        assert_eq!(Key::parse("+"), Some(Key::char('+')));
985        assert_eq!(Key::parse("hyper+x"), None);
986        assert_eq!(Key::parse("fx"), None);
987    }
988
989    #[test]
990    fn punctuation_keys_have_names() {
991        for (name, c) in [
992            ("equals", '='),
993            ("minus", '-'),
994            ("plus", '+'),
995            ("underscore", '_'),
996            ("slash", '/'),
997            ("backslash", '\\'),
998            ("period", '.'),
999            ("dot", '.'),
1000            ("semicolon", ';'),
1001            ("colon", ':'),
1002            ("quote", '\''),
1003            ("backtick", '`'),
1004            ("tilde", '~'),
1005            ("bracketleft", '['),
1006            ("bracketright", ']'),
1007            ("lt", '<'),
1008            ("gt", '>'),
1009            ("question", '?'),
1010        ] {
1011            assert_eq!(Key::parse(name), Some(Key::char(c)), "{name}");
1012            // Shown as the character it names.
1013            assert_eq!(Key::parse(name).unwrap().to_string(), c.to_string());
1014        }
1015        assert_eq!(Key::parse("Ctrl+Equals"), Some(Key::ctrl('=')));
1016        let alt = Modifiers {
1017            alt: true,
1018            ..Modifiers::NONE
1019        };
1020        assert_eq!(
1021            Key::parse("alt+slash"),
1022            Some(Key::with(KeyCode::Char('/'), alt))
1023        );
1024        // Shift with punctuation is a layout's, not a key name.
1025        assert_eq!(Key::parse("shift+minus"), None);
1026    }
1027
1028    #[test]
1029    fn the_menu_key_has_a_name() {
1030        assert_eq!(Key::parse("menu"), Some(Key::new(KeyCode::Menu)));
1031        assert_eq!(Key::parse("Menu").unwrap().to_string(), "menu");
1032        assert_eq!(Key::parse("shift+menu").unwrap().to_string(), "shift+menu");
1033    }
1034
1035    fn key(name: &str) -> Key {
1036        Key::parse(name).expect("a key name")
1037    }
1038
1039    #[test]
1040    fn names_are_the_keys_terminals_send() {
1041        // Shift and a letter is the capital letter, with Ctrl too.
1042        assert_eq!(Key::parse("shift+a"), Some(Key::char('A')));
1043        assert_eq!(Key::parse("alt+shift+a"), Key::parse("alt+A"));
1044        assert_eq!(
1045            Key::parse("ctrl+shift+a"),
1046            Some(Key::with(KeyCode::Char('A'), Modifiers::CTRL))
1047        );
1048        assert_eq!(Key::parse("ctrl+A"), Some(Key::ctrl('a')));
1049        // Shift with a symbol depends on the layout.
1050        assert_eq!(Key::parse("shift+1"), None);
1051        // A name means the key, even where a legacy terminal sends another.
1052        assert_eq!(Key::parse("ctrl+i"), Some(Key::ctrl('i')));
1053        assert_eq!(Key::parse("ctrl+m"), Some(Key::ctrl('m')));
1054        assert_eq!(Key::parse("ctrl+["), Some(Key::ctrl('[')));
1055        assert_eq!(Key::parse("ctrl+]"), Some(Key::ctrl(']')));
1056        assert_eq!(Key::parse("ctrl+@"), Some(Key::ctrl('@')));
1057        assert_eq!(Key::parse("ctrl+h"), Some(Key::ctrl('h')));
1058        // ... and a key from a legacy terminal fires the bindings of every
1059        // key it could be.
1060        assert!(key("tab").matches(&key("ctrl+i")));
1061        assert!(key("enter").matches(&key("ctrl+m")));
1062        assert!(key("esc").matches(&key("ctrl+[")));
1063        assert!(Key::ctrl('5').matches(&key("ctrl+]")));
1064        assert!(key("ctrl+space").matches(&key("ctrl+@")));
1065        assert!(key("ctrl+a").matches(&key("ctrl+shift+a")));
1066        // Function keys terminals have.
1067        assert_eq!(Key::parse("f24"), Some(Key::new(KeyCode::F(24))));
1068        assert_eq!(Key::parse("f0"), None);
1069        assert_eq!(Key::parse("f99"), None);
1070    }
1071
1072    #[test]
1073    fn legacy_keys_match_every_key_they_could_be() {
1074        // What a legacy terminal sends for each name.
1075        for (name, sent) in [
1076            ("ctrl+i", "tab"),
1077            ("ctrl+m", "enter"),
1078            ("ctrl+[", "esc"),
1079            ("ctrl+\\", "ctrl+4"),
1080            ("ctrl+]", "ctrl+5"),
1081            ("ctrl+^", "ctrl+6"),
1082            ("ctrl+_", "ctrl+7"),
1083            ("ctrl+@", "ctrl+space"),
1084            ("alt+ctrl+i", "alt+tab"),
1085            ("ctrl+shift+a", "ctrl+a"),
1086            ("ctrl+h", "ctrl+h"),
1087            ("tab", "tab"),
1088            ("f5", "f5"),
1089        ] {
1090            assert_eq!(key(name).legacy(), key(sent), "{name}");
1091            // Its bytes fire a binding for the name and for what was sent.
1092            assert!(key(sent).matches(&key(name)), "{sent} fires {name}");
1093            assert!(key(sent).matches(&key(sent)), "{sent} fires {sent}");
1094        }
1095        // Some terminals send Ctrl+H for Backspace; Backspace is only itself.
1096        assert!(key("ctrl+h").matches(&key("backspace")));
1097        assert!(key("alt+ctrl+h").matches(&key("alt+backspace")));
1098        assert!(!key("backspace").matches(&key("ctrl+h")));
1099        // Keys that send bytes of their own never match each other.
1100        assert!(!key("ctrl+i").matches(&key("tab")));
1101        assert!(!key("tab").matches(&key("ctrl+j")));
1102        assert!(!key("ctrl+a").matches(&key("ctrl+b")));
1103        assert!(key("tab").matches_any(&keys(&["x", "ctrl+i"])));
1104    }
1105
1106    fn keys(names: &[&str]) -> Vec<Key> {
1107        names.iter().map(|name| key(name)).collect()
1108    }
1109
1110    #[test]
1111    fn exact_keys_match_only_themselves() {
1112        assert!(key("tab").exact().matches(&key("tab")));
1113        assert!(!key("tab").exact().matches(&key("ctrl+i")));
1114        assert!(key("ctrl+i").exact().matches(&key("ctrl+i")));
1115        assert!(!key("ctrl+i").exact().matches(&key("tab")));
1116        assert!(!key("enter").exact().matches(&key("ctrl+m")));
1117        assert!(!key("esc").exact().matches(&key("ctrl+[")));
1118        assert!(!key("ctrl+h").exact().matches(&key("backspace")));
1119        assert!(!key("ctrl+a").exact().matches(&key("ctrl+shift+a")));
1120        // How a key was read is not part of what it is.
1121        assert_eq!(key("tab").exact(), key("tab"));
1122        assert!(key("tab").exact().is_exact());
1123        assert!(!key("tab").exact().legacy().is_exact());
1124        let mut set = std::collections::HashSet::new();
1125        set.insert(key("q").exact());
1126        assert!(set.contains(&key("q")));
1127    }
1128
1129    #[test]
1130    fn an_exact_name_wins_over_a_key_it_could_be() {
1131        let bindings = [keys(&["ctrl+i"]), keys(&["x", "tab"])];
1132        let sets = || bindings.iter().map(Vec::as_slice);
1133        assert_eq!(key("tab").pick(sets()), Some(1));
1134        assert_eq!(key("tab").exact().pick(sets()), Some(1));
1135        assert_eq!(key("ctrl+i").pick(sets()), Some(0));
1136        let only = [keys(&["ctrl+i"])];
1137        assert_eq!(key("tab").pick(only.iter().map(Vec::as_slice)), Some(0));
1138        assert_eq!(
1139            key("tab").exact().pick(only.iter().map(Vec::as_slice)),
1140            None
1141        );
1142    }
1143
1144    #[test]
1145    fn keys_display_as_they_parse() {
1146        for name in [
1147            "ctrl+c",
1148            "enter",
1149            "pagedown",
1150            "space",
1151            "f3",
1152            "alt+x",
1153            "ctrl+i",
1154            "ctrl+[",
1155            "ctrl+shift+a",
1156            "shift+tab",
1157            "A",
1158        ] {
1159            assert_eq!(key(name).to_string(), name);
1160        }
1161    }
1162
1163    #[test]
1164    fn translates_crossterm_events() {
1165        use crossterm::event::{Event as E, KeyCode as K, KeyEvent, KeyModifiers as M};
1166        let press = E::Key(KeyEvent::new(K::Char('c'), M::CONTROL));
1167        assert_eq!(from_crossterm(press), Some(Event::Key(Key::ctrl('c'))));
1168        let upper = E::Key(KeyEvent::new(K::Char('A'), M::SHIFT));
1169        assert_eq!(from_crossterm(upper), Some(Event::Key(Key::char('A'))));
1170        assert_eq!(
1171            from_crossterm(E::Resize(80, 24)),
1172            Some(Event::Resize {
1173                columns: 80,
1174                rows: 24
1175            })
1176        );
1177    }
1178
1179    #[test]
1180    fn releases_and_repeats_from_crossterm() {
1181        use crossterm::event::{
1182            Event as E, KeyCode as K, KeyEvent, KeyEventKind, KeyModifiers as M,
1183        };
1184        let with = |code, modifiers, kind| {
1185            let mut event = KeyEvent::new(code, modifiers);
1186            event.kind = kind;
1187            E::Key(event)
1188        };
1189        let release = with(K::Char('q'), M::NONE, KeyEventKind::Release);
1190        assert_eq!(from_crossterm(release), Some(Event::KeyUp(Key::char('q'))));
1191        let repeat = with(K::Down, M::NONE, KeyEventKind::Repeat);
1192        assert_eq!(
1193            from_crossterm(repeat),
1194            Some(Event::Key(Key::new(KeyCode::Down)))
1195        );
1196        // Released, the key is read as pressed: exact with the protocol.
1197        let up = with(K::Tab, M::NONE, KeyEventKind::Release);
1198        let Some(Event::KeyUp(tab)) = from_crossterm_kitty(up) else {
1199            panic!("a release");
1200        };
1201        assert!(tab.is_exact());
1202    }
1203
1204    #[test]
1205    fn kitty_keys_from_crossterm_are_exact() {
1206        use crossterm::event::{Event as E, KeyCode as K, KeyEvent, KeyModifiers as M};
1207        let read =
1208            |code, modifiers| match from_crossterm_kitty(E::Key(KeyEvent::new(code, modifiers))) {
1209                Some(Event::Key(key)) => Some(key),
1210                _ => None,
1211            };
1212        let ctrl_i = read(K::Char('i'), M::CONTROL).unwrap();
1213        assert!(ctrl_i.is_exact() && ctrl_i.matches(&key("ctrl+i")));
1214        let tab = read(K::Tab, M::NONE).unwrap();
1215        assert!(tab.matches(&key("tab")) && !tab.matches(&key("ctrl+i")));
1216        // Ctrl+Shift+A arrives as Shift and `a`: the name is ctrl+shift+a.
1217        assert_eq!(
1218            read(K::Char('a'), M::CONTROL | M::SHIFT),
1219            Key::parse("ctrl+shift+a")
1220        );
1221        // Super is not a modifier here, so Super+A is not A.
1222        assert_eq!(read(K::Char('a'), M::SUPER), None);
1223        // The legacy translation leaves the same keys ambiguous.
1224        let Some(Event::Key(legacy_tab)) = from_crossterm(E::Key(KeyEvent::new(K::Tab, M::NONE)))
1225        else {
1226            panic!("a key");
1227        };
1228        assert!(!legacy_tab.is_exact() && legacy_tab.matches(&key("ctrl+i")));
1229    }
1230}