keymap_core/keymap.rs
1//! The binding table.
2
3use std::collections::HashMap;
4
5use crate::input::KeyInput;
6
7/// A table mapping normalized [`KeyInput`]s to a caller-defined action `A`.
8///
9/// `Keymap` is state-free: it answers "what is bound to this input?" and nothing
10/// more. Deciding whether to *consume* the action or pass the input through, and
11/// tracking modes or multi-key sequences, is the caller's concern (and the job
12/// of later layers). A lookup miss is an absence ([`None`]), not an error — the
13/// caller treats it as "pass through".
14///
15/// The action type `A` is yours. The struct itself places no bound on `A`;
16/// bounds appear only on the methods that need them.
17#[derive(Debug, Clone)]
18#[non_exhaustive]
19pub struct Keymap<A> {
20 bindings: HashMap<KeyInput, A>,
21}
22
23impl<A> Default for Keymap<A> {
24 fn default() -> Self {
25 Keymap {
26 bindings: HashMap::new(),
27 }
28 }
29}
30
31impl<A> Keymap<A> {
32 /// Creates an empty keymap.
33 #[must_use]
34 pub fn new() -> Self {
35 Keymap::default()
36 }
37
38 /// Binds `input` to `action`, returning the action previously bound to that
39 /// input, if any.
40 pub fn bind(&mut self, input: KeyInput, action: A) -> Option<A> {
41 self.bindings.insert(input, action)
42 }
43
44 /// Removes any binding for `input`, returning the action that was bound.
45 pub fn unbind(&mut self, input: &KeyInput) -> Option<A> {
46 self.bindings.remove(input)
47 }
48
49 /// Returns the action bound to `input`, or [`None`] if the input is unbound.
50 #[must_use]
51 pub fn get(&self, input: &KeyInput) -> Option<&A> {
52 self.bindings.get(input)
53 }
54
55 /// Returns `true` if `input` has a binding.
56 #[must_use]
57 pub fn contains(&self, input: &KeyInput) -> bool {
58 self.bindings.contains_key(input)
59 }
60
61 /// The number of bindings in the table.
62 #[must_use]
63 pub fn len(&self) -> usize {
64 self.bindings.len()
65 }
66
67 /// Returns `true` if the table has no bindings.
68 #[must_use]
69 pub fn is_empty(&self) -> bool {
70 self.bindings.is_empty()
71 }
72
73 /// Iterates over every `(input, action)` binding.
74 ///
75 /// Order is unspecified. This is the data source for listing and search
76 /// (the discovery layer) built on top of `keymap-core`.
77 pub fn iter(&self) -> impl Iterator<Item = (&KeyInput, &A)> {
78 self.bindings.iter()
79 }
80}
81
82/// Resolves `input` against an ordered list of keymap layers, returning the
83/// first match (earlier layers win).
84///
85/// This is how context-dependent bindings are expressed without the library
86/// knowing anything about "contexts": the *caller* decides, from its own state,
87/// which layers are active and in what priority order, then passes them here.
88/// A binding present only in a high-priority layer overrides the base; a binding
89/// only in the base falls through and still resolves. The library never sees a
90/// mode or context type — only an ordered sequence of plain [`Keymap`]s.
91///
92/// This is a lexical scope chain (`block → panel → global`, innermost-first):
93/// the caller flattens its context *tree* into this ordered list per event, and
94/// the list stays fixed during resolution, so the resolver is state-free. A
95/// miss in every layer ([`None`]) is the "pass through" signal — in a terminal
96/// multiplexer that is the key flowing to the PTY, which lives *past the end of
97/// the chain* as a sink, never as a layer within it.
98///
99/// ```
100/// use keymap_core::{resolve_layered, Key, KeyInput, Keymap, Modifiers};
101///
102/// #[derive(PartialEq, Debug)]
103/// enum Action { Save, Split, Quit }
104///
105/// let ctrl = |c| KeyInput::new(Key::Char(c), Modifiers::CTRL);
106/// let mut base = Keymap::new();
107/// base.bind(ctrl('s'), Action::Save);
108/// base.bind(ctrl('q'), Action::Quit);
109/// let mut panel = Keymap::new();
110/// panel.bind(ctrl('s'), Action::Split);
111///
112/// // In "panel" context, the panel layer overrides ctrl+s …
113/// assert_eq!(resolve_layered([&panel, &base], &ctrl('s')), Some(&Action::Split));
114/// // … but ctrl+q falls through to the base layer.
115/// assert_eq!(resolve_layered([&panel, &base], &ctrl('q')), Some(&Action::Quit));
116/// // In "editor" context, only the base layer is active.
117/// assert_eq!(resolve_layered([&base], &ctrl('s')), Some(&Action::Save));
118/// ```
119pub fn resolve_layered<'a, A, L>(layers: L, input: &KeyInput) -> Option<&'a A>
120where
121 L: IntoIterator<Item = &'a Keymap<A>>,
122 A: 'a,
123{
124 layers.into_iter().find_map(|layer| layer.get(input))
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::input::{Key, Modifiers};
131
132 #[derive(Debug, Clone, PartialEq)]
133 enum Action {
134 Quit,
135 Save,
136 Split,
137 }
138
139 fn ctrl(c: char) -> KeyInput {
140 KeyInput::new(Key::Char(c), Modifiers::CTRL)
141 }
142
143 #[test]
144 fn bind_then_get() {
145 let mut map = Keymap::new();
146 assert!(map.is_empty());
147 assert_eq!(map.bind(ctrl('q'), Action::Quit), None);
148 assert_eq!(map.get(&ctrl('q')), Some(&Action::Quit));
149 assert_eq!(map.len(), 1);
150 }
151
152 #[test]
153 fn miss_is_none_not_error() {
154 let map: Keymap<Action> = Keymap::new();
155 assert_eq!(map.get(&ctrl('q')), None);
156 assert!(!map.contains(&ctrl('q')));
157 }
158
159 #[test]
160 fn rebinding_returns_previous_action() {
161 let mut map = Keymap::new();
162 map.bind(ctrl('s'), Action::Save);
163 let previous = map.bind(ctrl('s'), Action::Quit);
164 assert_eq!(previous, Some(Action::Save));
165 assert_eq!(map.get(&ctrl('s')), Some(&Action::Quit));
166 }
167
168 #[test]
169 fn unbind_removes() {
170 let mut map = Keymap::new();
171 map.bind(ctrl('q'), Action::Quit);
172 assert_eq!(map.unbind(&ctrl('q')), Some(Action::Quit));
173 assert_eq!(map.get(&ctrl('q')), None);
174 }
175
176 #[test]
177 fn modifiers_are_part_of_the_key() {
178 let mut map = Keymap::new();
179 map.bind(ctrl('q'), Action::Quit);
180 // Same character, no modifier -> different binding slot.
181 let plain = KeyInput::new(Key::Char('q'), Modifiers::NONE);
182 assert_eq!(map.get(&plain), None);
183 }
184
185 #[test]
186 fn layered_resolution_prefers_earlier_layers_and_falls_through() {
187 let mut base = Keymap::new();
188 base.bind(ctrl('s'), Action::Save);
189 base.bind(ctrl('q'), Action::Quit);
190 let mut overlay = Keymap::new();
191 overlay.bind(ctrl('s'), Action::Split);
192
193 // Overlay wins for ctrl+s …
194 assert_eq!(
195 resolve_layered([&overlay, &base], &ctrl('s')),
196 Some(&Action::Split)
197 );
198 // … ctrl+q falls through to base …
199 assert_eq!(
200 resolve_layered([&overlay, &base], &ctrl('q')),
201 Some(&Action::Quit)
202 );
203 // … and without the overlay, base resolves ctrl+s.
204 assert_eq!(resolve_layered([&base], &ctrl('s')), Some(&Action::Save));
205 }
206
207 #[test]
208 fn layered_resolution_misses_when_no_layer_binds() {
209 let base: Keymap<Action> = Keymap::new();
210 assert_eq!(resolve_layered([&base], &ctrl('x')), None);
211 // No layers at all is a miss, not a panic.
212 assert_eq!(
213 resolve_layered(std::iter::empty::<&Keymap<Action>>(), &ctrl('x')),
214 None
215 );
216 }
217}