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}