Skip to main content

keymap_core/
input.rs

1//! The normalized key-input vocabulary.
2//!
3//! These types are backend-neutral on purpose: `crossterm`/`termion`/etc. event
4//! types never appear in this module's always-compiled surface. Conversions from
5//! a specific backend live in feature-gated submodules (e.g. [`crossterm`]).
6
7#[cfg(feature = "crossterm")]
8pub(crate) mod crossterm;
9
10mod grammar;
11
12pub use grammar::ParseKeyInputError;
13
14/// A physical key, independent of which terminal or OS produced it.
15///
16/// Only keys whose identity is unambiguous across environments are enumerated
17/// today. Keys whose meaning is environment-dependent (media keys, keypad
18/// distinctions, etc.) are intentionally omitted and will be added later; this
19/// enum is `#[non_exhaustive]`, so adding them is not a breaking change. Because
20/// of that, downstream `match` expressions must include a `_` arm.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[non_exhaustive]
23pub enum Key {
24    /// A character-producing key, carrying the *resolved* glyph. See the
25    /// normalization note on [`KeyInput`] for how shift interacts with this.
26    Char(char),
27    /// A function key, `F(1)` through `F(n)`.
28    F(u8),
29    /// The Enter / Return key.
30    Enter,
31    /// The Escape key.
32    Esc,
33    /// The Tab key. `Shift+Tab` is represented as `Tab` with [`Modifiers::SHIFT`].
34    Tab,
35    /// The Backspace key.
36    Backspace,
37    /// The forward Delete key.
38    Delete,
39    /// The Insert key.
40    Insert,
41    /// The Home key.
42    Home,
43    /// The End key.
44    End,
45    /// The Page Up key.
46    PageUp,
47    /// The Page Down key.
48    PageDown,
49    /// The Up arrow.
50    Up,
51    /// The Down arrow.
52    Down,
53    /// The Left arrow.
54    Left,
55    /// The Right arrow.
56    Right,
57}
58
59/// A set of held modifier keys.
60///
61/// Backed by a private bit set so the in-memory representation can grow (e.g. to
62/// `u16`) without changing the public API, and so no third-party bit-flags type
63/// leaks into this crate's semver surface.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
65pub struct Modifiers(u8);
66
67impl Modifiers {
68    /// No modifiers held.
69    pub const NONE: Modifiers = Modifiers(0);
70    /// The Control modifier.
71    pub const CTRL: Modifiers = Modifiers(0b0001);
72    /// The Alt / Option modifier.
73    pub const ALT: Modifiers = Modifiers(0b0010);
74    /// The Shift modifier.
75    pub const SHIFT: Modifiers = Modifiers(0b0100);
76    /// The Super / Command / Windows modifier.
77    pub const SUPER: Modifiers = Modifiers(0b1000);
78
79    /// Returns the empty modifier set (alias for [`Modifiers::NONE`]).
80    #[must_use]
81    pub const fn empty() -> Self {
82        Modifiers::NONE
83    }
84
85    /// Returns `true` if no modifiers are held.
86    #[must_use]
87    pub const fn is_empty(self) -> bool {
88        self.0 == 0
89    }
90
91    /// Returns `true` if every modifier in `other` is also held in `self`.
92    #[must_use]
93    pub const fn contains(self, other: Modifiers) -> bool {
94        (self.0 & other.0) == other.0
95    }
96
97    /// Returns the union of two modifier sets.
98    #[must_use]
99    pub const fn union(self, other: Modifiers) -> Self {
100        Modifiers(self.0 | other.0)
101    }
102
103    /// Returns `self` with every modifier in `other` removed.
104    #[must_use]
105    pub const fn difference(self, other: Modifiers) -> Self {
106        Modifiers(self.0 & !other.0)
107    }
108}
109
110impl core::ops::BitOr for Modifiers {
111    type Output = Modifiers;
112
113    fn bitor(self, rhs: Modifiers) -> Modifiers {
114        self.union(rhs)
115    }
116}
117
118impl core::ops::BitOrAssign for Modifiers {
119    fn bitor_assign(&mut self, rhs: Modifiers) {
120        self.0 |= rhs.0;
121    }
122}
123
124/// A normalized key press: a [`Key`] plus the [`Modifiers`] held with it.
125///
126/// This is the unit a [`Keymap`](crate::Keymap) is keyed on, so its equality is
127/// the equality used for binding lookup.
128///
129/// # Normalization
130///
131/// For character-producing keys, the terminal has already resolved which glyph
132/// the keypress produces (`A`, `!`, `あ`, …). That resolution depends on the
133/// keyboard layout, which this crate cannot reconstruct from the glyph alone, so
134/// `keymap-core` does *not* re-case or re-map it.
135///
136/// [`Modifiers::SHIFT`] is cleared for a character key **only on a bare press**
137/// (when Shift is the sole modifier), because there it is redundant with the
138/// resolved glyph: `Char('A')` with Shift held normalizes to `Char('A')`, and
139/// `"shift+a"` parses to the same `KeyInput` as `"a"`. When `Ctrl`, `Alt`, or
140/// `Super` is *also* held, terminals send the base glyph plus modifier bits
141/// (e.g. `Ctrl+Shift+s` arrives as `Char('s')` + `CTRL` + `SHIFT`), so Shift is
142/// the only signal distinguishing the chord from `Ctrl+s` and is **kept**. This
143/// is what makes `cmd+shift+s` usable as a binding distinct from `cmd+s`.
144/// `Ctrl`, `Alt`, and `Super` are always preserved; for non-character keys
145/// (e.g. `Tab`, arrows) `Shift` is kept as a modifier.
146///
147/// Two consequences left to a later capability-aware layer (not handled here):
148/// if a terminal reports the *base-layout* key (`Char('a')` + Shift) instead of
149/// the shifted glyph, a bare press normalizes to `Char('a')` and won't match a
150/// `Char('A')` binding; and symbol chords (`shift+1` → `!` vs `1`+Shift) cannot
151/// be reconciled across terminals without layout knowledge.
152///
153/// Constructing a `KeyInput` directly via [`KeyInput::new`] does not apply this
154/// normalization — it is applied at backend boundaries (e.g. the `crossterm`
155/// conversion). Pass already-normalized values to `new`.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157#[non_exhaustive]
158pub struct KeyInput {
159    key: Key,
160    mods: Modifiers,
161}
162
163impl KeyInput {
164    /// Builds a `KeyInput` from an already-normalized key and modifier set.
165    #[must_use]
166    pub const fn new(key: Key, mods: Modifiers) -> Self {
167        KeyInput { key, mods }
168    }
169
170    /// Builds a `KeyInput`, applying the shared [`normalize`] rule first.
171    ///
172    /// This is the constructor every boundary that turns *raw* key parts into a
173    /// `KeyInput` should use — the string grammar, the `crossterm` adapter, and
174    /// the `keymap-term` byte decoder all funnel through it. Routing them through
175    /// one normalizing constructor is what guarantees a binding parsed from text
176    /// and an event produced at runtime land on the *same* `KeyInput`, so lookup
177    /// can't silently miss. Prefer this over [`new`](KeyInput::new) unless the
178    /// parts are already known to be normalized.
179    #[must_use]
180    pub fn normalized(key: Key, mods: Modifiers) -> Self {
181        let (key, mods) = normalize(key, mods);
182        KeyInput { key, mods }
183    }
184
185    /// The key that was pressed.
186    #[must_use]
187    pub const fn key(&self) -> Key {
188        self.key
189    }
190
191    /// The modifiers held during the press.
192    #[must_use]
193    pub const fn modifiers(&self) -> Modifiers {
194        self.mods
195    }
196}
197
198/// The single input-normalization rule, shared by every boundary that produces a
199/// [`KeyInput`] (the backend conversion and the string grammar). Keeping it in
200/// one place is what guarantees a binding parsed from text and an event produced
201/// at runtime normalize to the *same* `KeyInput`, so lookup can't silently miss.
202///
203/// For character keys, [`Modifiers::SHIFT`] is cleared only when it is the sole
204/// modifier (a bare press, where Shift is redundant with the resolved glyph).
205/// When any other modifier is held, Shift is the only thing distinguishing the
206/// chord (e.g. `cmd+shift+s` from `cmd+s`) and is kept. See [`KeyInput`].
207///
208/// Must be applied once, on the complete modifier set (it inspects the whole set
209/// rather than clearing a single bit unconditionally).
210#[must_use]
211pub(crate) fn normalize(key: Key, mods: Modifiers) -> (Key, Modifiers) {
212    match key {
213        Key::Char(_) if mods.difference(Modifiers::SHIFT).is_empty() => (key, Modifiers::NONE),
214        _ => (key, mods),
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn modifiers_union_and_contains() {
224        let cs = Modifiers::CTRL | Modifiers::SHIFT;
225        assert!(cs.contains(Modifiers::CTRL));
226        assert!(cs.contains(Modifiers::SHIFT));
227        assert!(!cs.contains(Modifiers::ALT));
228        assert!(cs.contains(Modifiers::CTRL | Modifiers::SHIFT));
229    }
230
231    #[test]
232    fn modifiers_empty_contains_only_empty() {
233        assert!(Modifiers::NONE.is_empty());
234        assert!(Modifiers::NONE.contains(Modifiers::NONE));
235        assert!(!Modifiers::NONE.contains(Modifiers::CTRL));
236        assert!(Modifiers::CTRL.contains(Modifiers::NONE));
237    }
238
239    #[test]
240    fn modifiers_difference_clears_bits() {
241        let cs = Modifiers::CTRL | Modifiers::SHIFT;
242        assert_eq!(cs.difference(Modifiers::SHIFT), Modifiers::CTRL);
243        assert_eq!(cs.difference(Modifiers::ALT), cs);
244    }
245
246    #[test]
247    fn key_input_is_hash_eq_by_value() {
248        let a = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
249        let b = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
250        let c = KeyInput::new(Key::Char('q'), Modifiers::NONE);
251        assert_eq!(a, b);
252        assert_ne!(a, c);
253    }
254}