Skip to main content

uncurses/event/
key.rs

1//! Key payloads, modifiers, display forms, and binding parsing.
2//!
3//! ## Purpose
4//!
5//! This module defines the key identity used by [`Event::KeyPress`],
6//! [`Event::KeyRepeat`], and [`Event::KeyRelease`]. It normalizes the many wire
7//! encodings a terminal may use into a stable [`Key`] value suitable for
8//! equality, hashing, display, and string-based shortcut matching.
9//!
10//! ```text
11//! terminal encoding ──▶ KeyCode + KeyModifiers ──▶ normalize ──▶ Key
12//!       │                         │
13//!       │                         └─ lock-state bits are informational
14//!       └─ optional text / shifted / base glyph metadata
15//! ```
16//!
17//! ## Key types
18//!
19//! * [`KeyCode`] identifies the logical key: a character, navigation key,
20//!   function key, keypad key, media key, or modifier key.
21//! * [`KeyModifiers`] separates binding modifiers (`Ctrl`, `Alt`, `Shift`,
22//!   `Super`, `Hyper`, `Meta`) from lock states.
23//! * [`Key`] combines a code and modifiers with optional text metadata supplied
24//!   by richer keyboard protocols.
25//! * [`ParseKeyError`] reports why a user-facing binding string did not parse.
26//!
27//! ## Matching and display
28//!
29//! [`Key`] equality and hashing use only the code plus binding modifiers. Text,
30//! alternate-key metadata, and lock-state bits are ignored so bindings are
31//! stable across terminal protocols and keyboard latch states. [`Key::matches`]
32//! first checks exact produced text, then falls back to parsing the pattern as a
33//! key string such as `"ctrl+c"`, `"shift+f1"`, or `"alt+plus"`.
34//!
35//! ## Gotchas
36//!
37//! Uppercase character codes normalize to lowercase plus [`KeyModifiers::SHIFT`]
38//! unless [`KeyModifiers::CAPS_LOCK`] explains the case. Bare legacy encodings
39//! cannot always recover the physical base key for shifted symbols or
40//! Ctrl+Shift letters; richer encodings may provide [`Key::text`],
41//! [`Key::shifted_key`], or [`Key::base_key`] for that extra context.
42use bitflags::bitflags;
43use std::fmt;
44
45bitflags! {
46    /// Keyboard modifier flags.
47    ///
48    /// Modifiers split into two categories:
49    ///
50    /// * **Binding modifiers** — `SHIFT`, `ALT`, `CTRL`, `META`,
51    ///   `HYPER`, `SUPER`. These participate in [`Key`] equality and
52    ///   in [`Key::matches`].
53    /// * **Lock states** — `CAPS_LOCK`, `NUM_LOCK`, `SCROLL_LOCK`,
54    ///   collectively [`LOCK_MASK`](Self::LOCK_MASK). These report
55    ///   the keyboard latch and are *never* binding modifiers: they
56    ///   are ignored by `Key` equality, hashing, [`Key::matches`],
57    ///   and the [`Display`](fmt::Display) form. Hosts that need to
58    ///   inspect lock state can mask `modifiers` with `LOCK_MASK`.
59    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
60    pub struct KeyModifiers: u16 {
61        /// Shift key modifier.
62        const SHIFT       = 0b0000_0000_0000_0001;
63        /// Alt key modifier.
64        const ALT         = 0b0000_0000_0000_0010;
65        /// Control key modifier.
66        const CTRL        = 0b0000_0000_0000_0100;
67        /// Meta key modifier.
68        const META        = 0b0000_0000_0000_1000;
69        /// Hyper key modifier.
70        const HYPER       = 0b0000_0000_0001_0000;
71        /// Super key modifier.
72        const SUPER       = 0b0000_0000_0010_0000;
73        /// Caps Lock state.
74        const CAPS_LOCK   = 0b0000_0000_0100_0000;
75        /// Num Lock state.
76        const NUM_LOCK    = 0b0000_0000_1000_0000;
77        /// Scroll Lock state.
78        const SCROLL_LOCK = 0b0000_0001_0000_0000;
79
80        /// Mask of lock-state bits (`CAPS_LOCK | NUM_LOCK | SCROLL_LOCK`).
81        ///
82        /// Lock states report the current keyboard latch and are *not*
83        /// binding modifiers: they are ignored by [`Key`] equality,
84        /// hashing, [`Key::matches`], and [`Display`](fmt::Display).
85        /// Callers that need to inspect or compare lock state can mask
86        /// the modifier set with this constant.
87        const LOCK_MASK   = Self::CAPS_LOCK.bits()
88                          | Self::NUM_LOCK.bits()
89                          | Self::SCROLL_LOCK.bits();
90    }
91}
92
93/// A key event.
94///
95/// Equality and hashing intentionally consider only [`code`](Self::code)
96/// and the *binding* portion of [`modifiers`](Self::modifiers): two
97/// keys representing the same press compare equal regardless of
98/// whether informational fields ([`text`](Self::text),
99/// [`shifted_key`](Self::shifted_key), [`base_key`](Self::base_key))
100/// were populated by the producing decoder, and regardless of lock
101/// state ([`CAPS_LOCK`](KeyModifiers::CAPS_LOCK),
102/// [`NUM_LOCK`](KeyModifiers::NUM_LOCK),
103/// [`SCROLL_LOCK`](KeyModifiers::SCROLL_LOCK)). This makes `Key` safe
104/// to use as a `HashMap` key for binding lookups across decoder paths
105/// and across keyboard latch states.
106///
107/// For string-based binding matching that additionally folds letter
108/// case and consults [`text`](Self::text) for layout-specific glyphs,
109/// see [`matches`](Self::matches) and [`matches_any`](Self::matches_any).
110#[derive(Debug, Clone)]
111pub struct Key {
112    /// The key code (which key was pressed).
113    pub code: KeyCode,
114    /// Active modifiers.
115    pub modifiers: KeyModifiers,
116    /// Associated text (from Kitty protocol or composed input).
117    ///
118    /// Informational; ignored by `==` and `Hash`. Consulted by
119    /// [`Key::matches`] when it carries a layout-specific glyph that
120    /// differs from `code` (for example `"!"` for `shift+1` on a US
121    /// layout), enabling layout-independent string bindings.
122    pub text: Option<String>,
123    /// Shifted-layer codepoint (Kitty Report-Alternate-Keys).
124    ///
125    /// Informational; ignored by `==` and `Hash`.
126    pub shifted_key: Option<char>,
127    /// Base-layout codepoint (Kitty Report-Alternate-Keys).
128    ///
129    /// Informational; ignored by `==` and `Hash`.
130    pub base_key: Option<char>,
131}
132
133impl PartialEq for Key {
134    fn eq(&self, other: &Self) -> bool {
135        self.code == other.code
136            && self.modifiers.difference(KeyModifiers::LOCK_MASK)
137                == other.modifiers.difference(KeyModifiers::LOCK_MASK)
138    }
139}
140
141impl Eq for Key {}
142
143impl std::hash::Hash for Key {
144    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
145        self.code.hash(state);
146        self.modifiers
147            .difference(KeyModifiers::LOCK_MASK)
148            .hash(state);
149    }
150}
151
152impl Key {
153    /// Construct a [`Key`] with the given code and modifiers.
154    ///
155    /// This is a transparent constructor: no canonicalization is
156    /// performed. Optional fields (`text`, `shifted_key`, `base_key`)
157    /// start empty. Decoder paths populate the optional fields as
158    /// needed and then chain [`Key::normalized`] (or call
159    /// [`Key::normalize`] in place) to apply the canonical identity
160    /// rules (case folding, printable text auto-population).
161    pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
162        Self {
163            code,
164            modifiers,
165            text: None,
166            shifted_key: None,
167            base_key: None,
168        }
169    }
170
171    /// Consume `self` and return the canonical form. The recommended
172    /// way to build a canonical key in expression position
173    /// (`Key::new(code, mods).normalized()`); see [`Key::normalize`]
174    /// for the in-place equivalent and the full list of rules.
175    pub fn normalized(mut self) -> Self {
176        self.normalize();
177        self
178    }
179
180    /// Return the character if this is a simple character key.
181    /// [`KeyCode::Space`] reports `' '`; other named keys report `None`.
182    pub fn char(&self) -> Option<char> {
183        match self.code {
184            KeyCode::Char(c) => Some(c),
185            KeyCode::Space => Some(' '),
186            _ => None,
187        }
188    }
189
190    /// Test whether this key matches a string pattern.
191    ///
192    /// Matching is two-tier:
193    ///
194    /// 1. If this key carries a [`text`](Self::text) value and it
195    ///    equals `pattern` byte-for-byte, the match succeeds. Lets
196    ///    bindings be written as the produced glyph (`"?"`, `"!"`,
197    ///    `"@"`) and hit any keystroke that resolves to that text,
198    ///    independent of keyboard layout. Note that `text` reflects
199    ///    the user-perceived character, so `CapsLock` *can* shift
200    ///    the matched form (pressing `g` with CapsLock on yields
201    ///    `text == "G"` and matches the pattern `"G"`).
202    /// 2. Otherwise, `pattern` is parsed as a [`Key`] with the same
203    ///    grammar as [`FromStr`](std::str::FromStr) (for example
204    ///    `"ctrl+c"`, `"shift+f1"`, `"alt+plus"`) and compared with
205    ///    [`PartialEq`]. Lock state (`CapsLock`, `NumLock`,
206    ///    `ScrollLock`) is ignored by `==`, so step 2 is layout-
207    ///    sensitive but lock-insensitive. Matching is otherwise
208    ///    case-sensitive — `"g"` and `"G"` are distinct (`"G"` is a
209    ///    synonym for `"shift+g"`).
210    ///
211    /// Returns `false` for any pattern that fails to parse and has
212    /// no matching `text`; invalid patterns never panic.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use uncurses::event::{Key, KeyCode, KeyModifiers};
218    ///
219    /// // Case-sensitive: g and G are distinct (vim-style).
220    /// let plain_g = Key::new(KeyCode::Char('g'), KeyModifiers::empty()).normalized();
221    /// let big_g   = Key::new(KeyCode::Char('G'), KeyModifiers::empty()).normalized();
222    /// assert!(plain_g.matches("g"));
223    /// assert!(!plain_g.matches("G"));
224    /// assert!(big_g.matches("G"));
225    /// assert!(big_g.matches("shift+g")); // "G" and "shift+g" are synonyms.
226    /// assert!(!big_g.matches("g"));
227    /// ```
228    pub fn matches(&self, pattern: &str) -> bool {
229        if let Some(text) = self.text.as_deref()
230            && text == pattern
231        {
232            return true;
233        }
234        pattern.parse::<Key>().is_ok_and(|p| *self == p)
235    }
236
237    /// Test whether this key matches any pattern in the iterator.
238    ///
239    /// Equivalent to calling [`matches`](Self::matches) for each
240    /// pattern and stopping at the first hit. Accepts any iterable
241    /// yielding string-like items, including arrays, slices, and
242    /// vectors.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use uncurses::event::{Key, KeyCode, KeyModifiers};
248    ///
249    /// let key = Key::new(KeyCode::Char('c'), KeyModifiers::CTRL).normalized();
250    /// assert!(key.matches_any(["esc", "ctrl+c", "q"]));
251    /// assert!(!key.matches_any(["esc", "q"]));
252    /// ```
253    pub fn matches_any<I, S>(&self, patterns: I) -> bool
254    where
255        I: IntoIterator<Item = S>,
256        S: AsRef<str>,
257    {
258        patterns.into_iter().any(|p| self.matches(p.as_ref()))
259    }
260
261    /// Apply the canonical identity rules in place. Idempotent.
262    ///
263    /// Decoder paths construct keys via [`Key::new`], populate optional
264    /// fields, and then call this method (or chain
265    /// [`Key::normalized`]) to produce the canonical form. The rules
266    /// applied are:
267    ///
268    /// - Case folding for printable `Char` codes: uppercase chars are
269    ///   lowercased and the original is stored in `shifted_key`;
270    ///   `SHIFT` is added only when `CAPS_LOCK` is not set (with
271    ///   CapsLock the case change is attributed to the lock state, not
272    ///   a Shift press).
273    /// - Printable text auto-population when `text` is empty and the
274    ///   modifier set is a subset of `SHIFT | CAPS_LOCK | NUM_LOCK`.
275    pub fn normalize(&mut self) {
276        // Case folding for printable Char codes.
277        if let KeyCode::Char(c) = self.code {
278            if c.is_uppercase() {
279                let mut iter = c.to_lowercase();
280                if let Some(lower) = iter.next()
281                    && iter.next().is_none()
282                    && lower != c
283                {
284                    self.code = KeyCode::Char(lower);
285                    if self.shifted_key.is_none() {
286                        self.shifted_key = Some(c);
287                    }
288                    if !self.modifiers.contains(KeyModifiers::CAPS_LOCK) {
289                        self.modifiers |= KeyModifiers::SHIFT;
290                    }
291                }
292            } else if c.is_lowercase()
293                && self
294                    .modifiers
295                    .intersects(KeyModifiers::SHIFT | KeyModifiers::CAPS_LOCK)
296                && self.shifted_key.is_none()
297            {
298                let mut iter = c.to_uppercase();
299                if let Some(upper) = iter.next()
300                    && iter.next().is_none()
301                    && upper != c
302                {
303                    self.shifted_key = Some(upper);
304                }
305            }
306        }
307
308        // Text auto-population for printable input.
309        if self.text.is_none() {
310            const PRINTABLE_ALLOWED: KeyModifiers = KeyModifiers::SHIFT
311                .union(KeyModifiers::CAPS_LOCK)
312                .union(KeyModifiers::NUM_LOCK);
313            if (self.modifiers - PRINTABLE_ALLOWED).is_empty() {
314                // The shifted glyph is only what the user perceives
315                // when the shifted layer is actually engaged. Decoders
316                // may populate `shifted_key` regardless of whether
317                // Shift is actually held, so we must not blindly use
318                // it here.
319                //
320                // CapsLock only acts as a shifted layer for cased
321                // letters. `Key::new` normalizes uppercase to lowercase
322                // + SHIFT before we get here, so `is_lowercase()` is
323                // the right test: it covers every Unicode cased letter
324                // (ASCII, Cyrillic, Greek, Latin-Extended, …) and
325                // excludes digits, symbols, and uncased scripts where
326                // CapsLock has no effect on common layouts.
327                //
328                // Whether Shift cancels CapsLock on letters is a host
329                // convention (some platforms cancel, most OR); the
330                // library takes the OR-side that matches the majority.
331                // Hosts wanting cancellation should populate `text`
332                // directly from the resolved character.
333                let glyph: Option<char> = match self.code {
334                    KeyCode::Char(c) if !c.is_control() => {
335                        let shifted = if c.is_lowercase() {
336                            self.modifiers
337                                .intersects(KeyModifiers::SHIFT | KeyModifiers::CAPS_LOCK)
338                        } else {
339                            self.modifiers.contains(KeyModifiers::SHIFT)
340                        };
341                        Some(if shifted {
342                            self.shifted_key.unwrap_or(c)
343                        } else {
344                            c
345                        })
346                    }
347                    KeyCode::Space => Some(' '),
348                    _ => None,
349                };
350                if let Some(g) = glyph {
351                    self.text = Some(g.to_string());
352                }
353            }
354        }
355    }
356}
357
358/// Logical identity of a key, before modifiers are considered.
359///
360/// `KeyCode` is intentionally broader than printable text: it covers named
361/// navigation/editing keys, keypad keys, media keys, and the richer modifier-key
362/// identities reported by modern keyboard protocols. Combine it with
363/// [`KeyModifiers`] in a [`Key`] to represent a full key event.
364///
365/// Use [`KeyCode::function`] when constructing function keys from untrusted
366/// numeric input; the raw [`KeyCode::F`] variant is public for pattern matching
367/// and decoder construction.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
369pub enum KeyCode {
370    /// A Unicode character key.
371    ///
372    /// Printable spaces are normally represented as [`KeyCode::Space`] for
373    /// cross-decoder stability. Other printable and non-control Unicode scalar
374    /// values use this variant after [`Key::normalize`] applies case folding.
375    Char(char),
376    /// Function key. Valid range is `1..=35` (xterm goes to F20,
377    /// kitty extends to F35). Construct via [`KeyCode::function`] for
378    /// a checked builder; the bare variant is `pub` so decoders can
379    /// emit literals, but downstream code matching on `F(n)` should
380    /// treat values outside the valid range as bugs.
381    F(u8),
382    /// Legacy VT220 Find key, distinct from Home. Only emitted when the
383    /// decoder is configured to recognize the legacy Find key.
384    Find,
385    /// Legacy VT220 Select key, distinct from End. Only emitted when the
386    /// decoder is configured to recognize the legacy Select key.
387    Select,
388    // Navigation
389    /// Up arrow key.
390    Up,
391    /// Down arrow key.
392    Down,
393    /// Left arrow key.
394    Left,
395    /// Right arrow key.
396    Right,
397    /// Home key.
398    Home,
399    /// End key.
400    End,
401    /// Page Up key.
402    PageUp,
403    /// Page Down key.
404    PageDown,
405    // Editing
406    /// Backspace key.
407    Backspace,
408    /// Delete key.
409    Delete,
410    /// Insert key.
411    Insert,
412    /// Tab key.
413    Tab,
414    /// Enter key.
415    Enter,
416    // Whitespace
417    /// Space key.
418    ///
419    /// The space bar decodes to this variant, **not** [`KeyCode::Char(' ')`]
420    /// — match on `KeyCode::Space` (a `Char(' ')` arm never fires). It still
421    /// reports `text` of `" "`, so text entry via [`Key::text`] is unaffected.
422    /// Rust has no variant aliases; to treat both alike by character, use
423    /// [`Key::char`], which returns `Some(' ')` for `Space` and `Char(' ')`.
424    ///
425    /// [`KeyCode::Char(' ')`]: KeyCode::Char
426    Space,
427    // Special
428    /// Escape key.
429    Escape,
430    /// Caps Lock key.
431    CapsLock,
432    /// Scroll Lock key.
433    ScrollLock,
434    /// Num Lock key.
435    NumLock,
436    /// Print Screen key.
437    PrintScreen,
438    /// Pause key.
439    Pause,
440    /// Menu key.
441    Menu,
442    // Keypad
443    /// Keypad Enter key.
444    KpEnter,
445    /// Keypad Add key.
446    KpAdd,
447    /// Keypad Subtract key.
448    KpSubtract,
449    /// Keypad Multiply key.
450    KpMultiply,
451    /// Keypad Divide key.
452    KpDivide,
453    /// Keypad Decimal key.
454    KpDecimal,
455    /// Keypad Equal key.
456    KpEqual,
457    /// Keypad Separator key.
458    KpSeparator,
459    /// Keypad Left key.
460    KpLeft,
461    /// Keypad Right key.
462    KpRight,
463    /// Keypad Up key.
464    KpUp,
465    /// Keypad Down key.
466    KpDown,
467    /// Keypad Page Up key.
468    KpPageUp,
469    /// Keypad Page Down key.
470    KpPageDown,
471    /// Keypad Home key.
472    KpHome,
473    /// Keypad End key.
474    KpEnd,
475    /// Keypad Insert key.
476    KpInsert,
477    /// Keypad Delete key.
478    KpDelete,
479    /// Keypad Begin key.
480    KpBegin,
481    /// Keypad 0 key.
482    Kp0,
483    /// Keypad 1 key.
484    Kp1,
485    /// Keypad 2 key.
486    Kp2,
487    /// Keypad 3 key.
488    Kp3,
489    /// Keypad 4 key.
490    Kp4,
491    /// Keypad 5 key.
492    Kp5,
493    /// Keypad 6 key.
494    Kp6,
495    /// Keypad 7 key.
496    Kp7,
497    /// Keypad 8 key.
498    Kp8,
499    /// Keypad 9 key.
500    Kp9,
501    // Media
502    /// Media Play key.
503    MediaPlay,
504    /// Media Pause key.
505    MediaPause,
506    /// Media Play/Pause key.
507    MediaPlayPause,
508    /// Media Reverse key.
509    MediaReverse,
510    /// Media Stop key.
511    MediaStop,
512    /// Media Rewind key.
513    MediaRewind,
514    /// Media Fast Forward key.
515    MediaFastForward,
516    /// Media Next key.
517    MediaNext,
518    /// Media Previous key.
519    MediaPrev,
520    /// Media Record key.
521    MediaRecord,
522    /// Volume Up key.
523    VolumeUp,
524    /// Volume Down key.
525    VolumeDown,
526    /// Volume Mute key.
527    VolumeMute,
528    // Modifier keys (reported with Kitty protocol)
529    /// Left Shift key.
530    LeftShift,
531    /// Right Shift key.
532    RightShift,
533    /// Left Control key.
534    LeftCtrl,
535    /// Right Control key.
536    RightCtrl,
537    /// Left Alt key.
538    LeftAlt,
539    /// Right Alt key.
540    RightAlt,
541    /// Left Super key.
542    LeftSuper,
543    /// Right Super key.
544    RightSuper,
545    /// Left Hyper key.
546    LeftHyper,
547    /// Right Hyper key.
548    RightHyper,
549    /// Left Meta key.
550    LeftMeta,
551    /// Right Meta key.
552    RightMeta,
553    /// ISO Level 3 Shift (typically AltGr).
554    IsoLevel3Shift,
555    /// ISO Level 5 Shift.
556    IsoLevel5Shift,
557}
558
559impl KeyCode {
560    /// Highest valid function-key index accepted by [`KeyCode::function`].
561    ///
562    /// Function keys are represented as `F(1)` through `F(35)`. Values outside
563    /// that range are not produced by the checked constructor.
564    pub const FUNCTION_KEY_MAX: u8 = 35;
565
566    /// Construct a checked function-key code.
567    ///
568    /// Returns `Some(KeyCode::F(n))` when `n` is in
569    /// `1..=KeyCode::FUNCTION_KEY_MAX`; returns `None` for `0` or values above
570    /// the supported range. This function is useful when decoding or accepting
571    /// user input that may contain an invalid function-key number. It never
572    /// panics.
573    pub fn function(n: u8) -> Option<KeyCode> {
574        if (1..=Self::FUNCTION_KEY_MAX).contains(&n) {
575            Some(KeyCode::F(n))
576        } else {
577            None
578        }
579    }
580}
581
582/// Formats the key as its canonical binding spelling: a modifier prefix
583/// (`ctrl+`, `alt+`, `shift+`, `super+`, `hyper+`, `meta+`) followed by the
584/// [`KeyCode`] name, for example `"ctrl+c"`, `"shift+f1"`, or `"space"`.
585///
586/// This is the inverse of [`FromStr`](std::str::FromStr): every string it
587/// produces parses back to an equal `Key`. It renders the structural key
588/// identity only and intentionally ignores the informational
589/// [`text`](Self::text), [`shifted_key`](Self::shifted_key), and
590/// [`base_key`](Self::base_key) fields, along with lock state
591/// (`CapsLock`/`NumLock`/`ScrollLock`). It therefore always spells the space
592/// bar as `"space"` (never `" "`), and composed or IME input is shown by its
593/// `code`, not its produced glyph.
594///
595/// For the user-perceived character that was typed, use
596/// [`char`](Self::char) or read [`text`](Self::text) directly; for
597/// layout-aware string matching that consults `text`, use
598/// [`matches`](Self::matches).
599impl fmt::Display for Key {
600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601        let mods = self.modifiers;
602        // Canonical order: ctrl, alt, shift, super, hyper, meta. Lock
603        // states (Caps/Num/Scroll) are intentionally omitted — they are
604        // session state, not binding modifiers.
605        if mods.contains(KeyModifiers::CTRL) {
606            f.write_str("ctrl+")?;
607        }
608        if mods.contains(KeyModifiers::ALT) {
609            f.write_str("alt+")?;
610        }
611        if mods.contains(KeyModifiers::SHIFT) {
612            f.write_str("shift+")?;
613        }
614        if mods.contains(KeyModifiers::SUPER) {
615            f.write_str("super+")?;
616        }
617        if mods.contains(KeyModifiers::HYPER) {
618            f.write_str("hyper+")?;
619        }
620        if mods.contains(KeyModifiers::META) {
621            f.write_str("meta+")?;
622        }
623
624        match self.code {
625            KeyCode::Char('+') => f.write_str("plus"),
626            KeyCode::Char('-') => f.write_str("minus"),
627            KeyCode::Char('=') => f.write_str("equals"),
628            KeyCode::Char(c) => write!(f, "{c}"),
629            KeyCode::F(n) => write!(f, "f{n}"),
630            KeyCode::Up => f.write_str("up"),
631            KeyCode::Down => f.write_str("down"),
632            KeyCode::Left => f.write_str("left"),
633            KeyCode::Right => f.write_str("right"),
634            KeyCode::Home => f.write_str("home"),
635            KeyCode::End => f.write_str("end"),
636            KeyCode::Find => f.write_str("find"),
637            KeyCode::Select => f.write_str("select"),
638            KeyCode::PageUp => f.write_str("pageup"),
639            KeyCode::PageDown => f.write_str("pagedown"),
640            KeyCode::Backspace => f.write_str("backspace"),
641            KeyCode::Delete => f.write_str("delete"),
642            KeyCode::Insert => f.write_str("insert"),
643            KeyCode::Tab => f.write_str("tab"),
644            KeyCode::Enter => f.write_str("enter"),
645            KeyCode::Space => f.write_str("space"),
646            KeyCode::Escape => f.write_str("escape"),
647            KeyCode::CapsLock => f.write_str("capslock"),
648            KeyCode::ScrollLock => f.write_str("scrolllock"),
649            KeyCode::NumLock => f.write_str("numlock"),
650            KeyCode::PrintScreen => f.write_str("printscreen"),
651            KeyCode::Pause => f.write_str("pause"),
652            KeyCode::Menu => f.write_str("menu"),
653            // Keypad
654            KeyCode::KpEnter => f.write_str("kpenter"),
655            KeyCode::KpAdd => f.write_str("kpadd"),
656            KeyCode::KpSubtract => f.write_str("kpsubtract"),
657            KeyCode::KpMultiply => f.write_str("kpmultiply"),
658            KeyCode::KpDivide => f.write_str("kpdivide"),
659            KeyCode::KpDecimal => f.write_str("kpdecimal"),
660            KeyCode::KpEqual => f.write_str("kpequal"),
661            KeyCode::KpSeparator => f.write_str("kpseparator"),
662            KeyCode::KpLeft => f.write_str("kpleft"),
663            KeyCode::KpRight => f.write_str("kpright"),
664            KeyCode::KpUp => f.write_str("kpup"),
665            KeyCode::KpDown => f.write_str("kpdown"),
666            KeyCode::KpPageUp => f.write_str("kppageup"),
667            KeyCode::KpPageDown => f.write_str("kppagedown"),
668            KeyCode::KpHome => f.write_str("kphome"),
669            KeyCode::KpEnd => f.write_str("kpend"),
670            KeyCode::KpInsert => f.write_str("kpinsert"),
671            KeyCode::KpDelete => f.write_str("kpdelete"),
672            KeyCode::KpBegin => f.write_str("kpbegin"),
673            KeyCode::Kp0 => f.write_str("kp0"),
674            KeyCode::Kp1 => f.write_str("kp1"),
675            KeyCode::Kp2 => f.write_str("kp2"),
676            KeyCode::Kp3 => f.write_str("kp3"),
677            KeyCode::Kp4 => f.write_str("kp4"),
678            KeyCode::Kp5 => f.write_str("kp5"),
679            KeyCode::Kp6 => f.write_str("kp6"),
680            KeyCode::Kp7 => f.write_str("kp7"),
681            KeyCode::Kp8 => f.write_str("kp8"),
682            KeyCode::Kp9 => f.write_str("kp9"),
683            // Media
684            KeyCode::MediaPlay => f.write_str("mediaplay"),
685            KeyCode::MediaPause => f.write_str("mediapause"),
686            KeyCode::MediaPlayPause => f.write_str("mediaplaypause"),
687            KeyCode::MediaReverse => f.write_str("mediareverse"),
688            KeyCode::MediaStop => f.write_str("mediastop"),
689            KeyCode::MediaRewind => f.write_str("mediarewind"),
690            KeyCode::MediaFastForward => f.write_str("mediafastforward"),
691            KeyCode::MediaNext => f.write_str("medianext"),
692            KeyCode::MediaPrev => f.write_str("mediaprev"),
693            KeyCode::MediaRecord => f.write_str("mediarecord"),
694            KeyCode::VolumeUp => f.write_str("volumeup"),
695            KeyCode::VolumeDown => f.write_str("volumedown"),
696            KeyCode::VolumeMute => f.write_str("volumemute"),
697            // Modifier keys
698            KeyCode::LeftShift => f.write_str("leftshift"),
699            KeyCode::RightShift => f.write_str("rightshift"),
700            KeyCode::LeftCtrl => f.write_str("leftctrl"),
701            KeyCode::RightCtrl => f.write_str("rightctrl"),
702            KeyCode::LeftAlt => f.write_str("leftalt"),
703            KeyCode::RightAlt => f.write_str("rightalt"),
704            KeyCode::LeftSuper => f.write_str("leftsuper"),
705            KeyCode::RightSuper => f.write_str("rightsuper"),
706            KeyCode::LeftHyper => f.write_str("lefthyper"),
707            KeyCode::RightHyper => f.write_str("righthyper"),
708            KeyCode::LeftMeta => f.write_str("leftmeta"),
709            KeyCode::RightMeta => f.write_str("rightmeta"),
710            KeyCode::IsoLevel3Shift => f.write_str("isolevel3shift"),
711            KeyCode::IsoLevel5Shift => f.write_str("isolevel5shift"),
712        }
713    }
714}
715
716impl fmt::Display for KeyCode {
717    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
718        // Delegate to Key display with no modifiers
719        write!(
720            f,
721            "{}",
722            Key {
723                code: *self,
724                modifiers: KeyModifiers::empty(),
725                text: None,
726                shifted_key: None,
727                base_key: None,
728            }
729        )
730    }
731}
732
733/// Error produced when parsing a [`Key`] or [`KeyCode`] from a binding string.
734///
735/// Parsing is used by [`Key::matches`], [`std::str::FromStr`] for [`Key`], and
736/// [`std::str::FromStr`] for [`KeyCode`]. The variants retain the offending
737/// token where useful so configuration UIs can report actionable messages.
738#[derive(Debug, Clone, PartialEq, Eq)]
739pub enum ParseKeyError {
740    /// Input was empty or whitespace-only.
741    Empty,
742    /// A `+`-separated component was empty.
743    ///
744    /// Examples include `"ctrl++a"`, `"ctrl+"`, and strings with a leading
745    /// separator such as `"+a"`.
746    EmptyComponent,
747    /// A modifier token was not recognized.
748    ///
749    /// The contained string is the exact modifier component that failed to
750    /// parse, before any case normalization beyond comparison.
751    UnknownModifier(String),
752    /// The terminal key token was not recognized.
753    ///
754    /// The contained string is the key component after modifiers have been
755    /// split off. Single-character tokens are accepted as [`KeyCode::Char`], so
756    /// this usually indicates an unknown named key.
757    UnknownKey(String),
758    /// A function-key token (`f<n>`) had an out-of-range index.
759    ///
760    /// Valid function keys are `f1` through `f35`, matching
761    /// [`KeyCode::FUNCTION_KEY_MAX`].
762    InvalidFunctionKey(String),
763}
764
765impl fmt::Display for ParseKeyError {
766    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
767        match self {
768            Self::Empty => f.write_str("empty key string"),
769            Self::EmptyComponent => f.write_str("empty `+`-separated component"),
770            Self::UnknownModifier(s) => write!(f, "unknown modifier: {s:?}"),
771            Self::UnknownKey(s) => write!(f, "unknown key name: {s:?}"),
772            Self::InvalidFunctionKey(s) => write!(f, "invalid function key: {s:?}"),
773        }
774    }
775}
776
777impl std::error::Error for ParseKeyError {}
778
779fn parse_modifier(token: &str) -> Result<KeyModifiers, ParseKeyError> {
780    let mods = if token.eq_ignore_ascii_case("ctrl") || token.eq_ignore_ascii_case("control") {
781        KeyModifiers::CTRL
782    } else if token.eq_ignore_ascii_case("alt")
783        || token.eq_ignore_ascii_case("opt")
784        || token.eq_ignore_ascii_case("option")
785    {
786        KeyModifiers::ALT
787    } else if token.eq_ignore_ascii_case("shift") {
788        KeyModifiers::SHIFT
789    } else if token.eq_ignore_ascii_case("super")
790        || token.eq_ignore_ascii_case("win")
791        || token.eq_ignore_ascii_case("cmd")
792        || token.eq_ignore_ascii_case("command")
793    {
794        KeyModifiers::SUPER
795    } else if token.eq_ignore_ascii_case("hyper") {
796        KeyModifiers::HYPER
797    } else if token.eq_ignore_ascii_case("meta") {
798        KeyModifiers::META
799    } else {
800        return Err(ParseKeyError::UnknownModifier(token.to_string()));
801    };
802    Ok(mods)
803}
804
805/// Maximum byte length of any recognized key-code keyword. The
806/// longest spellings (`isolevel3shift`, `mediafastforward`) are 14
807/// and 16 bytes respectively, so anything longer than this cap cannot
808/// match a known keyword and parsing can bail out without further
809/// work. The buffer in [`parse_key_code`] is sized to this value so
810/// the ASCII-lowercase conversion stays on the stack.
811const KEY_KEYWORD_MAX_LEN: usize = 16;
812
813fn parse_key_code(token: &str) -> Result<KeyCode, ParseKeyError> {
814    // Single character: treat as Char (case preserved so `Key::new`
815    // can canonicalize uppercase into shift+lowercase). Includes `+`
816    // — the modifier-separator handling in `Key::from_str` passes
817    // the literal `+` through to this function as a single-char
818    // token.
819    let mut chars = token.chars();
820    if let Some(first) = chars.next()
821        && chars.next().is_none()
822    {
823        return Ok(KeyCode::Char(first));
824    }
825
826    // All recognized keywords are ASCII and fit in KEY_KEYWORD_MAX_LEN
827    // bytes. Tokens longer than that cannot match — bail to the
828    // unknown-key error without touching the allocator. Tokens
829    // containing non-ASCII bytes also can't match a keyword; fall
830    // through to the same error.
831    if token.len() > KEY_KEYWORD_MAX_LEN || !token.is_ascii() {
832        return Err(ParseKeyError::UnknownKey(token.to_string()));
833    }
834    let mut buf = [0u8; KEY_KEYWORD_MAX_LEN];
835    for (i, b) in token.as_bytes().iter().enumerate() {
836        buf[i] = b.to_ascii_lowercase();
837    }
838    let lower = std::str::from_utf8(&buf[..token.len()])
839        .expect("ASCII lowercase of ASCII input is valid UTF-8");
840
841    // Function key: f<n> where 1 <= n <= KeyCode::FUNCTION_KEY_MAX.
842    if let Some(rest) = lower.strip_prefix('f')
843        && !rest.is_empty()
844        && rest.bytes().all(|c| c.is_ascii_digit())
845    {
846        let n: u8 = rest
847            .parse()
848            .map_err(|_| ParseKeyError::InvalidFunctionKey(token.to_string()))?;
849        return KeyCode::function(n)
850            .ok_or_else(|| ParseKeyError::InvalidFunctionKey(token.to_string()));
851    }
852
853    Ok(match lower {
854        // Navigation
855        "up" => KeyCode::Up,
856        "down" => KeyCode::Down,
857        "left" => KeyCode::Left,
858        "right" => KeyCode::Right,
859        "home" => KeyCode::Home,
860        "end" => KeyCode::End,
861        "find" => KeyCode::Find,
862        "select" => KeyCode::Select,
863        "pgup" | "pageup" => KeyCode::PageUp,
864        "pgdn" | "pgdown" | "pagedown" => KeyCode::PageDown,
865        // Editing
866        "backspace" | "bs" => KeyCode::Backspace,
867        "delete" | "del" => KeyCode::Delete,
868        "insert" | "ins" => KeyCode::Insert,
869        "tab" => KeyCode::Tab,
870        "enter" | "return" | "ret" => KeyCode::Enter,
871        // Whitespace / special
872        "space" | "spc" => KeyCode::Space,
873        "esc" | "escape" => KeyCode::Escape,
874        "capslock" => KeyCode::CapsLock,
875        "scrolllock" => KeyCode::ScrollLock,
876        "numlock" => KeyCode::NumLock,
877        "printscreen" | "prtsc" => KeyCode::PrintScreen,
878        "pause" => KeyCode::Pause,
879        "menu" => KeyCode::Menu,
880        // Punctuation aliases. `plus`, `minus`, and `equals` are the
881        // Display forms for `Char('+')`, `Char('-')`, and `Char('=')`
882        // respectively; the literal characters are also accepted via
883        // the single-character path. `dash` / `hyphen` and `equal` are
884        // additional spellings for the same two keys.
885        "plus" => KeyCode::Char('+'),
886        "minus" | "dash" | "hyphen" => KeyCode::Char('-'),
887        "equals" | "equal" => KeyCode::Char('='),
888        // Keypad
889        "kpenter" => KeyCode::KpEnter,
890        "kpadd" => KeyCode::KpAdd,
891        "kpsubtract" => KeyCode::KpSubtract,
892        "kpmultiply" => KeyCode::KpMultiply,
893        "kpdivide" => KeyCode::KpDivide,
894        "kpdecimal" => KeyCode::KpDecimal,
895        "kpequal" => KeyCode::KpEqual,
896        "kpseparator" => KeyCode::KpSeparator,
897        "kpleft" => KeyCode::KpLeft,
898        "kpright" => KeyCode::KpRight,
899        "kpup" => KeyCode::KpUp,
900        "kpdown" => KeyCode::KpDown,
901        "kppgup" | "kppageup" => KeyCode::KpPageUp,
902        "kppgdn" | "kppgdown" | "kppagedown" => KeyCode::KpPageDown,
903        "kphome" => KeyCode::KpHome,
904        "kpend" => KeyCode::KpEnd,
905        "kpinsert" => KeyCode::KpInsert,
906        "kpdelete" => KeyCode::KpDelete,
907        "kpbegin" => KeyCode::KpBegin,
908        "kp0" => KeyCode::Kp0,
909        "kp1" => KeyCode::Kp1,
910        "kp2" => KeyCode::Kp2,
911        "kp3" => KeyCode::Kp3,
912        "kp4" => KeyCode::Kp4,
913        "kp5" => KeyCode::Kp5,
914        "kp6" => KeyCode::Kp6,
915        "kp7" => KeyCode::Kp7,
916        "kp8" => KeyCode::Kp8,
917        "kp9" => KeyCode::Kp9,
918        // Media
919        "mediaplay" => KeyCode::MediaPlay,
920        "mediapause" => KeyCode::MediaPause,
921        "mediaplaypause" => KeyCode::MediaPlayPause,
922        "mediareverse" => KeyCode::MediaReverse,
923        "mediastop" => KeyCode::MediaStop,
924        "mediarewind" => KeyCode::MediaRewind,
925        "mediafastforward" => KeyCode::MediaFastForward,
926        "medianext" => KeyCode::MediaNext,
927        "mediaprev" => KeyCode::MediaPrev,
928        "mediarecord" => KeyCode::MediaRecord,
929        "volumeup" => KeyCode::VolumeUp,
930        "volumedown" => KeyCode::VolumeDown,
931        "volumemute" => KeyCode::VolumeMute,
932        // Modifier keys
933        "leftshift" => KeyCode::LeftShift,
934        "rightshift" => KeyCode::RightShift,
935        "leftctrl" => KeyCode::LeftCtrl,
936        "rightctrl" => KeyCode::RightCtrl,
937        "leftalt" => KeyCode::LeftAlt,
938        "rightalt" => KeyCode::RightAlt,
939        "leftsuper" => KeyCode::LeftSuper,
940        "rightsuper" => KeyCode::RightSuper,
941        "lefthyper" => KeyCode::LeftHyper,
942        "righthyper" => KeyCode::RightHyper,
943        "leftmeta" => KeyCode::LeftMeta,
944        "rightmeta" => KeyCode::RightMeta,
945        "isolevel3shift" => KeyCode::IsoLevel3Shift,
946        "isolevel5shift" => KeyCode::IsoLevel5Shift,
947        _ => return Err(ParseKeyError::UnknownKey(token.to_string())),
948    })
949}
950
951impl std::str::FromStr for Key {
952    type Err = ParseKeyError;
953
954    fn from_str(s: &str) -> Result<Self, Self::Err> {
955        let s = s.trim();
956        if s.is_empty() {
957            return Err(ParseKeyError::Empty);
958        }
959
960        // Single-character input: treat as a literal key with no
961        // modifiers. This lets `+`, `-`, and any other single character
962        // round-trip cleanly without being misread as a `+`-separator.
963        if s.chars().nth(1).is_none() {
964            let code = parse_key_code(s)?;
965            return Ok(Key::new(code, KeyModifiers::empty()).normalized());
966        }
967
968        // Split on the last `+` to separate modifiers from the key.
969        // The literal `+` key is spelled `plus` in Display output, but
970        // forms like `ctrl++` are also accepted: a trailing `++` means
971        // "the `+` key, with the preceding `+` as separator". A single
972        // trailing `+` with no preceding `+` is a dangling separator.
973        // A leading `+` (e.g. `"+a"`) is also a dangling separator
974        // since there is no modifier before it.
975        let (mod_part, key_part) = if let Some(head) = s.strip_suffix('+') {
976            match head.strip_suffix('+') {
977                Some(mods) => (mods, "+"),
978                None => return Err(ParseKeyError::EmptyComponent),
979            }
980        } else {
981            match s.rsplit_once('+') {
982                Some(("", _)) => return Err(ParseKeyError::EmptyComponent),
983                Some(parts) => parts,
984                None => ("", s),
985            }
986        };
987
988        if key_part.is_empty() {
989            return Err(ParseKeyError::EmptyComponent);
990        }
991
992        let mut modifiers = KeyModifiers::empty();
993        if !mod_part.is_empty() {
994            for tok in mod_part.split('+') {
995                if tok.is_empty() {
996                    return Err(ParseKeyError::EmptyComponent);
997                }
998                modifiers |= parse_modifier(tok)?;
999            }
1000        }
1001
1002        // `backtab` is a legacy spelling for `shift+tab`. Accept it as
1003        // a key-part alias (also when combined with other modifiers,
1004        // e.g. `alt+backtab`) even though there is no
1005        // `KeyCode::BackTab` variant; the canonical form is the
1006        // uniform `Tab + SHIFT`.
1007        if key_part.eq_ignore_ascii_case("backtab") {
1008            return Ok(Key::new(KeyCode::Tab, modifiers | KeyModifiers::SHIFT).normalized());
1009        }
1010
1011        let code = parse_key_code(key_part)?;
1012        Ok(Key::new(code, modifiers).normalized())
1013    }
1014}
1015
1016impl std::str::FromStr for KeyCode {
1017    type Err = ParseKeyError;
1018
1019    fn from_str(s: &str) -> Result<Self, Self::Err> {
1020        let s = s.trim();
1021        if s.is_empty() {
1022            return Err(ParseKeyError::Empty);
1023        }
1024        parse_key_code(s)
1025    }
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030    use super::*;
1031
1032    #[test]
1033    fn test_key_display() {
1034        let k = Key::new(KeyCode::Char('a'), KeyModifiers::CTRL).normalized();
1035        assert_eq!(k.to_string(), "ctrl+a");
1036    }
1037
1038    #[test]
1039    fn test_key_display_function() {
1040        let k = Key::new(KeyCode::F(12), KeyModifiers::empty()).normalized();
1041        assert_eq!(k.to_string(), "f12");
1042    }
1043
1044    #[test]
1045    fn test_key_char() {
1046        let k = Key::new(KeyCode::Char('x'), KeyModifiers::empty()).normalized();
1047        assert_eq!(k.char(), Some('x'));
1048    }
1049
1050    #[test]
1051    fn test_key_char_special() {
1052        let k = Key::new(KeyCode::Enter, KeyModifiers::empty()).normalized();
1053        assert_eq!(k.char(), None);
1054    }
1055
1056    #[test]
1057    fn new_uppercase_ascii_lowers_code_and_adds_shift() {
1058        let k = Key::new(KeyCode::Char('A'), KeyModifiers::empty()).normalized();
1059        assert_eq!(k.code, KeyCode::Char('a'));
1060        assert_eq!(k.shifted_key, Some('A'));
1061        assert!(k.modifiers.contains(KeyModifiers::SHIFT));
1062        assert_eq!(k.text.as_deref(), Some("A"));
1063    }
1064
1065    #[test]
1066    fn new_uppercase_with_caps_lock_does_not_add_shift() {
1067        let k = Key::new(KeyCode::Char('A'), KeyModifiers::CAPS_LOCK).normalized();
1068        assert_eq!(k.code, KeyCode::Char('a'));
1069        assert_eq!(k.shifted_key, Some('A'));
1070        assert!(!k.modifiers.contains(KeyModifiers::SHIFT));
1071        assert!(k.modifiers.contains(KeyModifiers::CAPS_LOCK));
1072        assert_eq!(k.text.as_deref(), Some("A"));
1073    }
1074
1075    #[test]
1076    fn new_lowercase_with_shift_populates_shifted_key() {
1077        let k = Key::new(KeyCode::Char('a'), KeyModifiers::SHIFT).normalized();
1078        assert_eq!(k.code, KeyCode::Char('a'));
1079        assert_eq!(k.shifted_key, Some('A'));
1080        assert_eq!(k.text.as_deref(), Some("A"));
1081    }
1082
1083    #[test]
1084    fn shifted_key_does_not_pollute_text_without_shift() {
1085        // A decoder may have pre-populated `shifted_key` from protocol
1086        // metadata (e.g. an alternate-key report) even on a bare key
1087        // press. `text` must reflect what the user actually typed,
1088        // which is the unshifted glyph, not the shifted one.
1089        let mut k = Key {
1090            code: KeyCode::Char('a'),
1091            modifiers: KeyModifiers::empty(),
1092            text: None,
1093            shifted_key: Some('A'),
1094            base_key: None,
1095        };
1096        k.normalize();
1097        assert_eq!(k.text.as_deref(), Some("a"));
1098        // With Shift, the shifted glyph wins.
1099        let mut k = Key {
1100            code: KeyCode::Char('a'),
1101            modifiers: KeyModifiers::SHIFT,
1102            text: None,
1103            shifted_key: Some('A'),
1104            base_key: None,
1105        };
1106        k.normalize();
1107        assert_eq!(k.text.as_deref(), Some("A"));
1108    }
1109
1110    #[test]
1111    fn caps_lock_shifts_cased_letters_only() {
1112        // CapsLock does not produce the shifted glyph for digits or
1113        // symbols on any common layout. Even if a decoder populated
1114        // `shifted_key` for a non-letter key, `text` derived under
1115        // CapsLock alone must use the base glyph.
1116        let mut k = Key {
1117            code: KeyCode::Char('2'),
1118            modifiers: KeyModifiers::CAPS_LOCK,
1119            text: None,
1120            shifted_key: Some('@'),
1121            base_key: None,
1122        };
1123        k.normalize();
1124        assert_eq!(k.text.as_deref(), Some("2"));
1125
1126        // ASCII letters treat CapsLock as a shifted layer.
1127        let mut k = Key {
1128            code: KeyCode::Char('a'),
1129            modifiers: KeyModifiers::CAPS_LOCK,
1130            text: None,
1131            shifted_key: Some('A'),
1132            base_key: None,
1133        };
1134        k.normalize();
1135        assert_eq!(k.text.as_deref(), Some("A"));
1136
1137        // Non-ASCII cased letters (e.g. Cyrillic, Greek, Latin
1138        // Extended) participate too — the rule is "any cased letter",
1139        // not "ASCII only".
1140        let mut k = Key {
1141            code: KeyCode::Char('ä'),
1142            modifiers: KeyModifiers::CAPS_LOCK,
1143            text: None,
1144            shifted_key: Some('Ä'),
1145            base_key: None,
1146        };
1147        k.normalize();
1148        assert_eq!(k.text.as_deref(), Some("Ä"));
1149
1150        let mut k = Key {
1151            code: KeyCode::Char('д'),
1152            modifiers: KeyModifiers::CAPS_LOCK,
1153            text: None,
1154            shifted_key: Some('Д'),
1155            base_key: None,
1156        };
1157        k.normalize();
1158        assert_eq!(k.text.as_deref(), Some("Д"));
1159
1160        // With Shift held, non-letters still take the shifted glyph.
1161        let mut k = Key {
1162            code: KeyCode::Char('2'),
1163            modifiers: KeyModifiers::SHIFT,
1164            text: None,
1165            shifted_key: Some('@'),
1166            base_key: None,
1167        };
1168        k.normalize();
1169        assert_eq!(k.text.as_deref(), Some("@"));
1170    }
1171
1172    #[test]
1173    fn new_lowercase_without_shift_populates_text() {
1174        let k = Key::new(KeyCode::Char('a'), KeyModifiers::empty()).normalized();
1175        assert_eq!(k.code, KeyCode::Char('a'));
1176        assert_eq!(k.shifted_key, None);
1177        assert!(k.modifiers.is_empty());
1178        assert_eq!(k.text.as_deref(), Some("a"));
1179    }
1180
1181    #[test]
1182    fn new_ctrl_uppercase_does_not_set_text() {
1183        let k = Key::new(KeyCode::Char('A'), KeyModifiers::CTRL).normalized();
1184        assert_eq!(k.code, KeyCode::Char('a'));
1185        assert_eq!(k.shifted_key, Some('A'));
1186        assert!(k.modifiers.contains(KeyModifiers::CTRL));
1187        assert!(k.modifiers.contains(KeyModifiers::SHIFT));
1188        assert!(k.text.is_none());
1189    }
1190
1191    #[test]
1192    fn new_ctrl_shift_lowercase_does_not_set_text() {
1193        let k = Key::new(KeyCode::Char('a'), KeyModifiers::CTRL | KeyModifiers::SHIFT).normalized();
1194        assert_eq!(k.code, KeyCode::Char('a'));
1195        assert_eq!(k.shifted_key, Some('A'));
1196        assert!(k.text.is_none());
1197    }
1198
1199    #[test]
1200    fn new_cyrillic_uppercase() {
1201        let k = Key::new(KeyCode::Char('Ц'), KeyModifiers::empty()).normalized();
1202        assert_eq!(k.code, KeyCode::Char('ц'));
1203        assert_eq!(k.shifted_key, Some('Ц'));
1204        assert!(k.modifiers.contains(KeyModifiers::SHIFT));
1205        assert_eq!(k.text.as_deref(), Some("Ц"));
1206    }
1207
1208    #[test]
1209    fn new_greek_lowercase_with_shift() {
1210        let k = Key::new(KeyCode::Char('α'), KeyModifiers::SHIFT).normalized();
1211        assert_eq!(k.code, KeyCode::Char('α'));
1212        assert_eq!(k.shifted_key, Some('Α'));
1213        assert_eq!(k.text.as_deref(), Some("Α"));
1214    }
1215
1216    #[test]
1217    fn new_multi_codepoint_lower_left_alone() {
1218        // 'İ' lowercases to "i\u{307}" — two codepoints; leave as-is.
1219        let k = Key::new(KeyCode::Char('İ'), KeyModifiers::empty()).normalized();
1220        assert_eq!(k.code, KeyCode::Char('İ'));
1221        assert_eq!(k.shifted_key, None);
1222        assert!(k.modifiers.is_empty());
1223        // Still printable input — text auto-populates with the original codepoint.
1224        assert_eq!(k.text.as_deref(), Some("İ"));
1225    }
1226
1227    #[test]
1228    fn new_titlecase_digraph_left_alone() {
1229        // 'Dž' is titlecase; is_uppercase() and is_lowercase() are both false.
1230        let k = Key::new(KeyCode::Char('Dž'), KeyModifiers::empty()).normalized();
1231        assert_eq!(k.code, KeyCode::Char('Dž'));
1232        assert_eq!(k.shifted_key, None);
1233        assert_eq!(k.text.as_deref(), Some("Dž"));
1234    }
1235
1236    #[test]
1237    fn new_digit_with_shift_keeps_digit_text() {
1238        // '1' has no case variant; text auto-populates from the codepoint.
1239        let k = Key::new(KeyCode::Char('1'), KeyModifiers::SHIFT).normalized();
1240        assert_eq!(k.code, KeyCode::Char('1'));
1241        assert_eq!(k.shifted_key, None);
1242        assert!(k.modifiers.contains(KeyModifiers::SHIFT));
1243        assert_eq!(k.text.as_deref(), Some("1"));
1244    }
1245
1246    #[test]
1247    fn new_non_char_no_text() {
1248        let k = Key::new(KeyCode::Enter, KeyModifiers::SHIFT).normalized();
1249        assert_eq!(k.code, KeyCode::Enter);
1250        assert_eq!(k.shifted_key, None);
1251        assert!(k.text.is_none());
1252    }
1253
1254    #[test]
1255    fn new_space_populates_text() {
1256        let k = Key::new(KeyCode::Space, KeyModifiers::empty()).normalized();
1257        assert_eq!(k.code, KeyCode::Space);
1258        assert_eq!(k.text.as_deref(), Some(" "));
1259    }
1260
1261    #[test]
1262    fn new_space_with_ctrl_no_text() {
1263        let k = Key::new(KeyCode::Space, KeyModifiers::CTRL).normalized();
1264        assert_eq!(k.code, KeyCode::Space);
1265        assert!(k.text.is_none());
1266    }
1267
1268    #[test]
1269    fn direct_field_mutation_overrides_auto_text() {
1270        let mut k = Key::new(KeyCode::Char('2'), KeyModifiers::SHIFT).normalized();
1271        // Simulate a decoder that knows the terminal-reported shifted glyph.
1272        k.shifted_key = Some('@');
1273        k.text = Some("@".to_string());
1274        assert_eq!(k.text.as_deref(), Some("@"));
1275        assert_eq!(k.shifted_key, Some('@'));
1276    }
1277
1278    #[test]
1279    fn eq_ignores_informational_fields() {
1280        let bare = Key::new(KeyCode::Char('a'), KeyModifiers::SHIFT).normalized();
1281        let mut decorated = Key::new(KeyCode::Char('a'), KeyModifiers::SHIFT).normalized();
1282        decorated.text = Some("custom".to_string());
1283        decorated.shifted_key = Some('Z');
1284        decorated.base_key = Some('q');
1285        assert_eq!(bare, decorated);
1286    }
1287
1288    #[test]
1289    fn hash_ignores_informational_fields() {
1290        use std::collections::HashMap;
1291        let mut map: HashMap<Key, &'static str> = HashMap::new();
1292        map.insert(
1293            Key::new(KeyCode::Char('a'), KeyModifiers::CTRL).normalized(),
1294            "ctrl-a",
1295        );
1296
1297        let mut lookup = Key::new(KeyCode::Char('a'), KeyModifiers::CTRL).normalized();
1298        lookup.text = Some("ignored".to_string());
1299        lookup.shifted_key = Some('X');
1300        assert_eq!(map.get(&lookup), Some(&"ctrl-a"));
1301    }
1302
1303    #[test]
1304    fn eq_distinguishes_code_and_modifiers() {
1305        let a = Key::new(KeyCode::Char('a'), KeyModifiers::empty()).normalized();
1306        let ctrl_a = Key::new(KeyCode::Char('a'), KeyModifiers::CTRL).normalized();
1307        let b = Key::new(KeyCode::Char('b'), KeyModifiers::empty()).normalized();
1308        assert_ne!(a, ctrl_a);
1309        assert_ne!(a, b);
1310    }
1311
1312    #[test]
1313    fn display_canonical_mod_order() {
1314        // Construct with all six binding modifiers; expect canonical
1315        // ctrl, alt, shift, super, hyper, meta order regardless of
1316        // input insertion.
1317        let mods = KeyModifiers::META
1318            | KeyModifiers::HYPER
1319            | KeyModifiers::SUPER
1320            | KeyModifiers::SHIFT
1321            | KeyModifiers::ALT
1322            | KeyModifiers::CTRL;
1323        let k = Key {
1324            code: KeyCode::Char('a'),
1325            modifiers: mods,
1326            text: None,
1327            shifted_key: None,
1328            base_key: None,
1329        };
1330        assert_eq!(k.to_string(), "ctrl+alt+shift+super+hyper+meta+a");
1331    }
1332
1333    #[test]
1334    fn display_omits_lock_state() {
1335        let k = Key {
1336            code: KeyCode::Char('a'),
1337            modifiers: KeyModifiers::CAPS_LOCK | KeyModifiers::NUM_LOCK | KeyModifiers::SCROLL_LOCK,
1338            text: None,
1339            shifted_key: None,
1340            base_key: None,
1341        };
1342        assert_eq!(k.to_string(), "a");
1343    }
1344
1345    #[test]
1346    fn display_lock_state_combined_with_binding_mod() {
1347        // Real input: Ctrl+a while CapsLock is on. Caps drops from
1348        // Display so the binding string stays stable.
1349        let k = Key {
1350            code: KeyCode::Char('a'),
1351            modifiers: KeyModifiers::CTRL | KeyModifiers::CAPS_LOCK,
1352            text: None,
1353            shifted_key: None,
1354            base_key: None,
1355        };
1356        assert_eq!(k.to_string(), "ctrl+a");
1357    }
1358
1359    #[test]
1360    fn display_char_plus_uses_word() {
1361        let k = Key::new(KeyCode::Char('+'), KeyModifiers::CTRL).normalized();
1362        assert_eq!(k.to_string(), "ctrl+plus");
1363    }
1364
1365    #[test]
1366    fn display_modifier_key_variants() {
1367        let k = Key::new(KeyCode::LeftShift, KeyModifiers::empty()).normalized();
1368        assert_eq!(k.to_string(), "leftshift");
1369        let k = Key::new(KeyCode::RightAlt, KeyModifiers::empty()).normalized();
1370        assert_eq!(k.to_string(), "rightalt");
1371        let k = Key::new(KeyCode::IsoLevel3Shift, KeyModifiers::empty()).normalized();
1372        assert_eq!(k.to_string(), "isolevel3shift");
1373    }
1374
1375    #[test]
1376    fn display_uses_full_words() {
1377        // Display always emits the unabbreviated name; the short forms
1378        // (`pgup`, `pgdn`, `esc`, etc.) live only on the parse side.
1379        assert_eq!(
1380            Key::new(KeyCode::PageUp, KeyModifiers::empty())
1381                .normalized()
1382                .to_string(),
1383            "pageup"
1384        );
1385        assert_eq!(
1386            Key::new(KeyCode::PageDown, KeyModifiers::empty())
1387                .normalized()
1388                .to_string(),
1389            "pagedown"
1390        );
1391        assert_eq!(
1392            Key::new(KeyCode::Escape, KeyModifiers::empty())
1393                .normalized()
1394                .to_string(),
1395            "escape"
1396        );
1397        // Round-trip the short forms through Display and back to the
1398        // same Key.
1399        assert_eq!(parse("pgup"), parse("pageup"));
1400        assert_eq!(parse("pgdn"), parse("pagedown"));
1401        assert_eq!(parse("pgdown"), parse("pagedown"));
1402        assert_eq!(parse("esc"), parse("escape"));
1403    }
1404
1405    #[test]
1406    fn display_keypad_variants() {
1407        assert_eq!(
1408            Key::new(KeyCode::Kp0, KeyModifiers::empty())
1409                .normalized()
1410                .to_string(),
1411            "kp0"
1412        );
1413        assert_eq!(
1414            Key::new(KeyCode::KpEnter, KeyModifiers::empty())
1415                .normalized()
1416                .to_string(),
1417            "kpenter"
1418        );
1419        assert_eq!(
1420            Key::new(KeyCode::KpPageUp, KeyModifiers::empty())
1421                .normalized()
1422                .to_string(),
1423            "kppageup"
1424        );
1425    }
1426
1427    #[test]
1428    fn display_media_variants() {
1429        assert_eq!(
1430            Key::new(KeyCode::MediaPlayPause, KeyModifiers::empty())
1431                .normalized()
1432                .to_string(),
1433            "mediaplaypause"
1434        );
1435        assert_eq!(
1436            Key::new(KeyCode::VolumeMute, KeyModifiers::empty())
1437                .normalized()
1438                .to_string(),
1439            "volumemute"
1440        );
1441    }
1442
1443    // --- FromStr ------------------------------------------------------
1444
1445    fn parse(s: &str) -> Key {
1446        s.parse::<Key>()
1447            .unwrap_or_else(|e| panic!("parse {s:?}: {e}"))
1448    }
1449
1450    #[test]
1451    fn fromstr_single_char_lowercase() {
1452        let k = parse("a");
1453        assert_eq!(k.code, KeyCode::Char('a'));
1454        assert!(k.modifiers.is_empty());
1455    }
1456
1457    #[test]
1458    fn fromstr_uppercase_char_canonicalizes_to_shift_lowercase() {
1459        // Reuse Key::new's canonicalization so callers writing
1460        // "A" or "shift+a" end up at the same identity.
1461        assert_eq!(
1462            "A".parse::<Key>().unwrap(),
1463            "shift+a".parse::<Key>().unwrap()
1464        );
1465    }
1466
1467    #[test]
1468    fn fromstr_modifier_order_independent() {
1469        let canonical = parse("ctrl+shift+a");
1470        assert_eq!(parse("shift+ctrl+a"), canonical);
1471        assert_eq!(parse("Shift+Ctrl+a"), canonical);
1472    }
1473
1474    #[test]
1475    fn fromstr_function_keys() {
1476        assert_eq!(parse("f1").code, KeyCode::F(1));
1477        assert_eq!(parse("f12").code, KeyCode::F(12));
1478        assert_eq!(parse("f24").code, KeyCode::F(24));
1479        assert_eq!(parse("f35").code, KeyCode::F(35));
1480    }
1481
1482    #[test]
1483    fn fromstr_function_key_out_of_range() {
1484        assert!(matches!(
1485            "f0".parse::<Key>(),
1486            Err(ParseKeyError::InvalidFunctionKey(_))
1487        ));
1488        assert!(matches!(
1489            "f36".parse::<Key>(),
1490            Err(ParseKeyError::InvalidFunctionKey(_))
1491        ));
1492    }
1493
1494    #[test]
1495    fn keycode_function_validates_range() {
1496        assert_eq!(KeyCode::function(1), Some(KeyCode::F(1)));
1497        assert_eq!(
1498            KeyCode::function(KeyCode::FUNCTION_KEY_MAX),
1499            Some(KeyCode::F(KeyCode::FUNCTION_KEY_MAX))
1500        );
1501        assert_eq!(KeyCode::function(0), None);
1502        assert_eq!(KeyCode::function(KeyCode::FUNCTION_KEY_MAX + 1), None);
1503    }
1504
1505    #[test]
1506    fn fromstr_named_keys() {
1507        assert_eq!(parse("esc").code, KeyCode::Escape);
1508        assert_eq!(parse("escape").code, KeyCode::Escape);
1509        assert_eq!(parse("pgup").code, KeyCode::PageUp);
1510        assert_eq!(parse("pageup").code, KeyCode::PageUp);
1511        assert_eq!(parse("enter").code, KeyCode::Enter);
1512        assert_eq!(parse("return").code, KeyCode::Enter);
1513        assert_eq!(parse("ret").code, KeyCode::Enter);
1514        assert_eq!(parse("backspace").code, KeyCode::Backspace);
1515        assert_eq!(parse("bs").code, KeyCode::Backspace);
1516        // `backtab` is an input alias for `shift+tab`. There is no
1517        // dedicated `KeyCode::BackTab` variant. The alias also
1518        // composes with other modifiers.
1519        let backtab = parse("backtab");
1520        assert_eq!(backtab.code, KeyCode::Tab);
1521        assert_eq!(backtab.modifiers, KeyModifiers::SHIFT);
1522        assert_eq!(parse("shift+tab"), backtab);
1523        // Display formats this identity as `shift+tab`, never `backtab`.
1524        assert_eq!(backtab.to_string(), "shift+tab");
1525        let alt_backtab = parse("alt+backtab");
1526        assert_eq!(alt_backtab.code, KeyCode::Tab);
1527        assert_eq!(
1528            alt_backtab.modifiers,
1529            KeyModifiers::SHIFT | KeyModifiers::ALT
1530        );
1531        assert_eq!(parse("alt+shift+tab"), alt_backtab);
1532        assert_eq!(alt_backtab.to_string(), "alt+shift+tab");
1533        assert_eq!(parse("delete").code, KeyCode::Delete);
1534        assert_eq!(parse("del").code, KeyCode::Delete);
1535        assert_eq!(parse("space").code, KeyCode::Space);
1536    }
1537
1538    #[test]
1539    fn fromstr_modifier_aliases() {
1540        assert_eq!(parse("control+a"), parse("ctrl+a"));
1541        assert_eq!(parse("option+a"), parse("alt+a"));
1542        assert_eq!(parse("cmd+a"), parse("super+a"));
1543        assert_eq!(parse("command+a"), parse("super+a"));
1544        assert_eq!(parse("win+a"), parse("super+a"));
1545    }
1546
1547    #[test]
1548    fn fromstr_plus_alias_round_trips() {
1549        let k = parse("ctrl+plus");
1550        assert_eq!(k.code, KeyCode::Char('+'));
1551        assert!(k.modifiers.contains(KeyModifiers::CTRL));
1552        assert_eq!(k.to_string(), "ctrl+plus");
1553    }
1554
1555    #[test]
1556    fn fromstr_keypad_and_media() {
1557        assert_eq!(parse("kp0").code, KeyCode::Kp0);
1558        assert_eq!(parse("kpenter").code, KeyCode::KpEnter);
1559        assert_eq!(parse("mediaplaypause").code, KeyCode::MediaPlayPause);
1560        assert_eq!(parse("volumemute").code, KeyCode::VolumeMute);
1561    }
1562
1563    #[test]
1564    fn fromstr_modifier_keys_themselves() {
1565        assert_eq!(parse("leftshift").code, KeyCode::LeftShift);
1566        assert_eq!(parse("rightalt").code, KeyCode::RightAlt);
1567        assert_eq!(parse("isolevel3shift").code, KeyCode::IsoLevel3Shift);
1568    }
1569
1570    #[test]
1571    fn fromstr_unicode_char() {
1572        assert_eq!(parse("ц").code, KeyCode::Char('ц'));
1573        assert_eq!(parse("α").code, KeyCode::Char('α'));
1574    }
1575
1576    #[test]
1577    fn fromstr_trims_whitespace() {
1578        assert_eq!(parse("  ctrl+a  "), parse("ctrl+a"));
1579    }
1580
1581    #[test]
1582    fn fromstr_errors() {
1583        assert_eq!("".parse::<Key>(), Err(ParseKeyError::Empty));
1584        assert_eq!("   ".parse::<Key>(), Err(ParseKeyError::Empty));
1585        assert_eq!("ctrl+".parse::<Key>(), Err(ParseKeyError::EmptyComponent));
1586        // `ctrl++a` still errors: the inner `+` produces an empty
1587        // modifier token. (`ctrl++` alone is the ctrl+literal-`+` form.)
1588        assert_eq!("ctrl++a".parse::<Key>(), Err(ParseKeyError::EmptyComponent));
1589        // Leading `+` is a dangling separator with no modifier before it.
1590        assert_eq!("+a".parse::<Key>(), Err(ParseKeyError::EmptyComponent));
1591        assert_eq!("+ctrl+a".parse::<Key>(), Err(ParseKeyError::EmptyComponent));
1592        assert!(matches!(
1593            "foo+a".parse::<Key>(),
1594            Err(ParseKeyError::UnknownModifier(_))
1595        ));
1596        assert!(matches!(
1597            "ctrl+xyz".parse::<Key>(),
1598            Err(ParseKeyError::UnknownKey(_))
1599        ));
1600    }
1601
1602    #[test]
1603    fn fromstr_keycode_only() {
1604        let kc: KeyCode = "esc".parse().unwrap();
1605        assert_eq!(kc, KeyCode::Escape);
1606        let kc: KeyCode = "f5".parse().unwrap();
1607        assert_eq!(kc, KeyCode::F(5));
1608        assert!(matches!(
1609            "ctrl+a".parse::<KeyCode>(),
1610            Err(ParseKeyError::UnknownKey(_))
1611        ));
1612    }
1613
1614    #[test]
1615    fn fromstr_literal_plus() {
1616        // Bare `+` is the literal key character (single-char shortcut).
1617        assert_eq!(parse("+").code, KeyCode::Char('+'));
1618        // `plus` alias parses identically.
1619        assert_eq!(parse("plus").code, KeyCode::Char('+'));
1620        // `ctrl++` is ctrl + the literal `+` key.
1621        assert_eq!(
1622            parse("ctrl++"),
1623            Key::new(KeyCode::Char('+'), KeyModifiers::CTRL).normalized()
1624        );
1625        // `ctrl+plus` resolves to the same Key.
1626        assert_eq!(parse("ctrl++"), parse("ctrl+plus"));
1627    }
1628
1629    #[test]
1630    fn fromstr_literal_symbols() {
1631        // Symbol literals are accepted via the single-char path.
1632        assert_eq!(parse("-").code, KeyCode::Char('-'));
1633        assert_eq!(parse("*").code, KeyCode::Char('*'));
1634        assert_eq!(parse("/").code, KeyCode::Char('/'));
1635        assert_eq!(parse("=").code, KeyCode::Char('='));
1636        assert_eq!(parse("[").code, KeyCode::Char('['));
1637        assert_eq!(parse("ctrl+-").code, KeyCode::Char('-'));
1638        assert_eq!(parse("ctrl+/").code, KeyCode::Char('/'));
1639        assert_eq!(parse("alt+[").code, KeyCode::Char('['));
1640    }
1641
1642    #[test]
1643    fn fromstr_minus_aliases() {
1644        assert_eq!(parse("minus").code, KeyCode::Char('-'));
1645        assert_eq!(parse("dash").code, KeyCode::Char('-'));
1646        assert_eq!(parse("hyphen").code, KeyCode::Char('-'));
1647        assert_eq!(parse("ctrl+minus"), parse("ctrl+-"));
1648        assert_eq!(parse("ctrl+dash"), parse("ctrl+-"));
1649    }
1650
1651    #[test]
1652    fn fromstr_equals_aliases() {
1653        assert_eq!(parse("equals").code, KeyCode::Char('='));
1654        assert_eq!(parse("equal").code, KeyCode::Char('='));
1655        assert_eq!(parse("ctrl+equals"), parse("ctrl+="));
1656    }
1657
1658    #[test]
1659    fn display_uses_named_symbol_forms() {
1660        assert_eq!(
1661            Key::new(KeyCode::Char('-'), KeyModifiers::empty())
1662                .normalized()
1663                .to_string(),
1664            "minus"
1665        );
1666        assert_eq!(
1667            Key::new(KeyCode::Char('='), KeyModifiers::empty())
1668                .normalized()
1669                .to_string(),
1670            "equals"
1671        );
1672        assert_eq!(
1673            Key::new(KeyCode::Char('+'), KeyModifiers::empty())
1674                .normalized()
1675                .to_string(),
1676            "plus"
1677        );
1678        assert_eq!(
1679            Key::new(KeyCode::Char('-'), KeyModifiers::CTRL)
1680                .normalized()
1681                .to_string(),
1682            "ctrl+minus"
1683        );
1684    }
1685
1686    #[test]
1687    fn fromstr_rejects_hyphen_aliases() {
1688        // Hyphenated key-name aliases are intentionally not accepted;
1689        // `+` is the only valid binding separator.
1690        assert!(matches!(
1691            "page-up".parse::<Key>(),
1692            Err(ParseKeyError::UnknownKey(_))
1693        ));
1694        assert!(matches!(
1695            "back-tab".parse::<Key>(),
1696            Err(ParseKeyError::UnknownKey(_))
1697        ));
1698        assert!(matches!(
1699            "caps-lock".parse::<Key>(),
1700            Err(ParseKeyError::UnknownKey(_))
1701        ));
1702    }
1703
1704    #[test]
1705    fn display_kp_pageup_long_form() {
1706        assert_eq!(
1707            Key::new(KeyCode::KpPageDown, KeyModifiers::empty())
1708                .normalized()
1709                .to_string(),
1710            "kppagedown"
1711        );
1712        // The short `kppgup`/`kppgdn` forms remain accepted on input.
1713        assert_eq!(parse("kppgup").code, KeyCode::KpPageUp);
1714        assert_eq!(parse("kppgdn").code, KeyCode::KpPageDown);
1715    }
1716
1717    #[test]
1718    fn display_fromstr_roundtrip_named_variants() {
1719        // Every variant emitted by Display must round-trip back to an
1720        // equal Key.
1721        let cases: &[(KeyCode, KeyModifiers)] = &[
1722            (KeyCode::Char('a'), KeyModifiers::empty()),
1723            (KeyCode::Char('a'), KeyModifiers::CTRL),
1724            (KeyCode::Char('a'), KeyModifiers::CTRL | KeyModifiers::ALT),
1725            (
1726                KeyCode::Char('a'),
1727                KeyModifiers::CTRL
1728                    | KeyModifiers::ALT
1729                    | KeyModifiers::SHIFT
1730                    | KeyModifiers::SUPER
1731                    | KeyModifiers::HYPER
1732                    | KeyModifiers::META,
1733            ),
1734            (KeyCode::Char('+'), KeyModifiers::CTRL),
1735            (KeyCode::Char('-'), KeyModifiers::CTRL),
1736            (KeyCode::Char('='), KeyModifiers::CTRL),
1737            (KeyCode::Char('-'), KeyModifiers::empty()),
1738            (KeyCode::Char('='), KeyModifiers::empty()),
1739            (KeyCode::Char('ц'), KeyModifiers::empty()),
1740            (KeyCode::F(1), KeyModifiers::empty()),
1741            (KeyCode::F(24), KeyModifiers::CTRL),
1742            (KeyCode::F(35), KeyModifiers::empty()),
1743            (KeyCode::Up, KeyModifiers::empty()),
1744            (KeyCode::Down, KeyModifiers::SHIFT),
1745            (KeyCode::Left, KeyModifiers::ALT),
1746            (KeyCode::Right, KeyModifiers::CTRL),
1747            (KeyCode::Home, KeyModifiers::empty()),
1748            (KeyCode::End, KeyModifiers::empty()),
1749            (KeyCode::PageUp, KeyModifiers::empty()),
1750            (KeyCode::PageDown, KeyModifiers::empty()),
1751            (KeyCode::Backspace, KeyModifiers::empty()),
1752            (KeyCode::Delete, KeyModifiers::empty()),
1753            (KeyCode::Insert, KeyModifiers::empty()),
1754            (KeyCode::Tab, KeyModifiers::empty()),
1755            (KeyCode::Tab, KeyModifiers::SHIFT),
1756            (KeyCode::Enter, KeyModifiers::empty()),
1757            (KeyCode::Space, KeyModifiers::empty()),
1758            (KeyCode::Escape, KeyModifiers::empty()),
1759            (KeyCode::CapsLock, KeyModifiers::empty()),
1760            (KeyCode::ScrollLock, KeyModifiers::empty()),
1761            (KeyCode::NumLock, KeyModifiers::empty()),
1762            (KeyCode::PrintScreen, KeyModifiers::empty()),
1763            (KeyCode::Pause, KeyModifiers::empty()),
1764            (KeyCode::Menu, KeyModifiers::empty()),
1765            (KeyCode::Kp0, KeyModifiers::empty()),
1766            (KeyCode::Kp9, KeyModifiers::empty()),
1767            (KeyCode::KpEnter, KeyModifiers::empty()),
1768            (KeyCode::KpPageUp, KeyModifiers::empty()),
1769            (KeyCode::KpPageDown, KeyModifiers::empty()),
1770            (KeyCode::KpBegin, KeyModifiers::empty()),
1771            (KeyCode::MediaPlay, KeyModifiers::empty()),
1772            (KeyCode::MediaPlayPause, KeyModifiers::empty()),
1773            (KeyCode::VolumeUp, KeyModifiers::empty()),
1774            (KeyCode::VolumeMute, KeyModifiers::empty()),
1775            (KeyCode::LeftShift, KeyModifiers::empty()),
1776            (KeyCode::RightMeta, KeyModifiers::empty()),
1777            (KeyCode::IsoLevel3Shift, KeyModifiers::empty()),
1778            (KeyCode::IsoLevel5Shift, KeyModifiers::empty()),
1779        ];
1780        for (code, mods) in cases {
1781            let k = Key::new(*code, *mods).normalized();
1782            let s = k.to_string();
1783            let parsed = s
1784                .parse::<Key>()
1785                .unwrap_or_else(|e| panic!("failed to parse {s:?} (from {code:?}, {mods:?}): {e}"));
1786            assert_eq!(parsed, k, "round-trip mismatch for {s:?}");
1787        }
1788    }
1789
1790    #[test]
1791    fn eq_ignores_lock_state() {
1792        let plain = Key::new(KeyCode::Char('c'), KeyModifiers::CTRL).normalized();
1793        let with_caps = Key::new(
1794            KeyCode::Char('c'),
1795            KeyModifiers::CTRL | KeyModifiers::CAPS_LOCK,
1796        )
1797        .normalized();
1798        let with_num = Key::new(
1799            KeyCode::Char('c'),
1800            KeyModifiers::CTRL | KeyModifiers::NUM_LOCK,
1801        )
1802        .normalized();
1803        let with_scroll = Key::new(
1804            KeyCode::Char('c'),
1805            KeyModifiers::CTRL | KeyModifiers::SCROLL_LOCK,
1806        )
1807        .normalized();
1808        let with_all_locks = Key::new(
1809            KeyCode::Char('c'),
1810            KeyModifiers::CTRL | KeyModifiers::LOCK_MASK,
1811        )
1812        .normalized();
1813        assert_eq!(plain, with_caps);
1814        assert_eq!(plain, with_num);
1815        assert_eq!(plain, with_scroll);
1816        assert_eq!(plain, with_all_locks);
1817    }
1818
1819    #[test]
1820    fn hash_ignores_lock_state() {
1821        use std::collections::hash_map::DefaultHasher;
1822        use std::hash::{Hash, Hasher};
1823        fn h(k: &Key) -> u64 {
1824            let mut s = DefaultHasher::new();
1825            k.hash(&mut s);
1826            s.finish()
1827        }
1828        let plain = Key::new(KeyCode::Char('c'), KeyModifiers::CTRL).normalized();
1829        let with_locks = Key::new(
1830            KeyCode::Char('c'),
1831            KeyModifiers::CTRL | KeyModifiers::LOCK_MASK,
1832        )
1833        .normalized();
1834        assert_eq!(h(&plain), h(&with_locks));
1835    }
1836
1837    #[test]
1838    fn eq_still_distinguishes_binding_modifiers() {
1839        let ctrl = Key::new(KeyCode::Char('c'), KeyModifiers::CTRL).normalized();
1840        let alt = Key::new(KeyCode::Char('c'), KeyModifiers::ALT).normalized();
1841        let ctrl_alt =
1842            Key::new(KeyCode::Char('c'), KeyModifiers::CTRL | KeyModifiers::ALT).normalized();
1843        assert_ne!(ctrl, alt);
1844        assert_ne!(ctrl, ctrl_alt);
1845    }
1846
1847    #[test]
1848    fn parsed_key_equals_key_with_lock_state() {
1849        let parsed: Key = "ctrl+c".parse().expect("parse");
1850        let live = Key::new(
1851            KeyCode::Char('c'),
1852            KeyModifiers::CTRL | KeyModifiers::CAPS_LOCK,
1853        )
1854        .normalized();
1855        assert_eq!(parsed, live);
1856    }
1857
1858    #[test]
1859    fn matches_simple_char() {
1860        let k = Key::new(KeyCode::Char('a'), KeyModifiers::empty()).normalized();
1861        assert!(k.matches("a"));
1862        assert!(!k.matches("b"));
1863    }
1864
1865    #[test]
1866    fn matches_modifier_combo() {
1867        let k = Key::new(KeyCode::Char('c'), KeyModifiers::CTRL).normalized();
1868        assert!(k.matches("ctrl+c"));
1869        assert!(!k.matches("alt+c"));
1870        assert!(!k.matches("ctrl+x"));
1871    }
1872
1873    #[test]
1874    fn matches_named_key() {
1875        let k = Key::new(KeyCode::F(5), KeyModifiers::SHIFT).normalized();
1876        assert!(k.matches("shift+f5"));
1877        assert!(!k.matches("f5"));
1878    }
1879
1880    #[test]
1881    fn matches_ignores_lock_state() {
1882        let k = Key::new(
1883            KeyCode::Char('a'),
1884            KeyModifiers::CTRL | KeyModifiers::CAPS_LOCK | KeyModifiers::NUM_LOCK,
1885        )
1886        .normalized();
1887        assert!(k.matches("ctrl+a"));
1888    }
1889
1890    #[test]
1891    fn matches_invalid_pattern_is_false() {
1892        let k = Key::new(KeyCode::Char('a'), KeyModifiers::empty()).normalized();
1893        assert!(!k.matches(""));
1894        assert!(!k.matches("frob+nargle"));
1895        assert!(!k.matches("ctrl+"));
1896    }
1897
1898    #[test]
1899    fn matches_any_basic() {
1900        let k = Key::new(KeyCode::Escape, KeyModifiers::empty()).normalized();
1901        assert!(k.matches_any(["esc", "ctrl+c", "q"]));
1902        assert!(!k.matches_any(["enter", "tab"]));
1903    }
1904
1905    #[test]
1906    fn matches_any_empty_is_false() {
1907        let k = Key::new(KeyCode::Char('a'), KeyModifiers::empty()).normalized();
1908        let none: [&str; 0] = [];
1909        assert!(!k.matches_any(none));
1910    }
1911
1912    #[test]
1913    fn matches_any_accepts_string_and_str() {
1914        let k = Key::new(KeyCode::Char('q'), KeyModifiers::empty()).normalized();
1915        let owned: Vec<String> = vec!["esc".into(), "q".into()];
1916        assert!(k.matches_any(&owned));
1917        let borrowed: Vec<&str> = vec!["esc", "q"];
1918        assert!(k.matches_any(borrowed));
1919    }
1920
1921    #[test]
1922    fn matches_is_case_sensitive_for_letters() {
1923        // Vim-style: g and G are distinct bindings.
1924        let plain_g = Key::new(KeyCode::Char('g'), KeyModifiers::empty()).normalized();
1925        assert!(plain_g.matches("g"));
1926        assert!(!plain_g.matches("G"));
1927        assert!(!plain_g.matches("shift+g"));
1928
1929        // `Key::new(Char('G'), empty).normalized()` normalizes to Char('g') + SHIFT,
1930        // matching what a decoder emits for a shift+g press.
1931        let big_g = Key::new(KeyCode::Char('G'), KeyModifiers::empty()).normalized();
1932        assert_eq!(big_g.code, KeyCode::Char('g'));
1933        assert!(big_g.modifiers.contains(KeyModifiers::SHIFT));
1934        assert!(big_g.matches("G"));
1935        assert!(big_g.matches("shift+g"));
1936        assert!(!big_g.matches("g"));
1937    }
1938
1939    #[test]
1940    fn matches_modifier_combos_with_letters() {
1941        let ctrl_g = Key::new(KeyCode::Char('g'), KeyModifiers::CTRL).normalized();
1942        assert!(ctrl_g.matches("ctrl+g"));
1943        assert!(!ctrl_g.matches("ctrl+G"));
1944        assert!(!ctrl_g.matches("ctrl+shift+g"));
1945        assert!(!ctrl_g.matches("g"));
1946        assert!(!ctrl_g.matches("alt+g"));
1947    }
1948
1949    #[test]
1950    fn matches_text_first_layout_independent() {
1951        // shift+1 on a US layout produces "!". Pattern "!" should hit
1952        // the key by its produced text even though the code is `1`.
1953        let mut shift_1 = Key::new(KeyCode::Char('1'), KeyModifiers::SHIFT).normalized();
1954        shift_1.text = Some("!".to_string());
1955        assert!(shift_1.matches("!"));
1956        // The physical-key spelling still matches via strict equality.
1957        assert!(shift_1.matches("shift+1"));
1958    }
1959
1960    #[test]
1961    fn matches_text_first_respects_binding_modifiers() {
1962        // ctrl+! must not silently match a key whose text happens to
1963        // be "!" — binding modifiers gate the text fallback.
1964        let mut shift_1 = Key::new(KeyCode::Char('1'), KeyModifiers::SHIFT).normalized();
1965        shift_1.text = Some("!".to_string());
1966        assert!(!shift_1.matches("ctrl+!"));
1967    }
1968
1969    #[test]
1970    fn matches_text_first_handles_layout_glyph() {
1971        // shift+/ produces "?" on a US layout.
1972        let mut shift_slash = Key::new(KeyCode::Char('/'), KeyModifiers::SHIFT).normalized();
1973        shift_slash.text = Some("?".to_string());
1974        assert!(shift_slash.matches("?"));
1975        assert!(shift_slash.matches("shift+/"));
1976    }
1977}