返回 CodeWhale
keymap.rs
1 //! Keymap: the bindings a view declares, which are also its hint list.
2 //!
3 //! A [`Binding`] is a chord (spelled by [`crate::keys`], so hints and
4 //! bindings share one spelling), a verb, a priority and an optional group.
5 //! A [`Keymap`] looks keys up and turns itself into [`KeyHints`] with
6 //! priority folding ([`KeyHints::from_keymap`]), so the footer cannot drift
7 //! from what the keys do (gitui's `CommandInfo`, and the engine's
8 //! `ActionHint` fed from one list).
9 //!
10 //! Priority runs the other way from the design draft: a **higher** number is
11 //! **more important** and is kept longest when the row is narrow.
12
13 use std::borrow::Cow;
14
15 use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
16
17 use crate::{
18 KeyHint, KeyHints, KeyHintsWords,
19 keys::{Platform, chord_label, pair_label},
20 };
21
22 /// One key and the modifiers held with it.
23 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24 pub struct KeyChord {
25 pub code: KeyCode,
26 pub modifiers: KeyModifiers,
27 }
28
29 impl From<KeyCode> for KeyChord {
30 fn from(code: KeyCode) -> Self {
31 Self::plain(code)
32 }
33 }
34
35 /// Fold what terminals disagree about: `Shift` on a lone character is already
36 /// in its case (and some terminals add it to `?`), `BackTab` always carries
37 /// `Shift`, and a letter held with a command modifier may arrive in either
38 /// case.
39 ///
40 /// `Shift` is kept once Control, Alt or Super is held: `Ctrl+Shift+E` and
41 /// `Ctrl+E` are two chords a keymap may bind to two actions, and their hints
42 /// already print differently.
43 fn normalize(code: KeyCode, modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
44 match code {
45 KeyCode::Char(c) => {
46 if modifiers.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER)
47 {
48 (KeyCode::Char(c.to_ascii_lowercase()), modifiers)
49 } else {
50 (code, modifiers & !KeyModifiers::SHIFT)
51 }
52 }
53 KeyCode::BackTab => (code, modifiers & !KeyModifiers::SHIFT),
54 _ => (code, modifiers),
55 }
56 }
57
58 impl KeyChord {
59 #[must_use]
60 pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
61 Self { code, modifiers }
62 }
63
64 /// A key with no modifiers.
65 #[must_use]
66 pub const fn plain(code: KeyCode) -> Self {
67 Self::new(code, KeyModifiers::NONE)
68 }
69
70 /// A character key with no modifiers: `?`, `r`, `/`.
71 #[must_use]
72 pub const fn char(c: char) -> Self {
73 Self::plain(KeyCode::Char(c))
74 }
75
76 /// `Ctrl+<c>`.
77 #[must_use]
78 pub const fn ctrl(c: char) -> Self {
79 Self::new(KeyCode::Char(c), KeyModifiers::CONTROL)
80 }
81
82 /// `Alt+<c>` (`⌥<c>` on macOS).
83 #[must_use]
84 pub const fn alt(c: char) -> Self {
85 Self::new(KeyCode::Char(c), KeyModifiers::ALT)
86 }
87
88 /// Whether `key` is this chord. Releases never match.
89 #[must_use]
90 pub fn matches(&self, key: &KeyEvent) -> bool {
91 key.kind != KeyEventKind::Release
92 && normalize(self.code, self.modifiers) == normalize(key.code, key.modifiers)
93 }
94
95 /// `Ctrl+O`, `⌥V`, `Enter`, `↑`: one spelling, from [`crate::keys`].
96 #[must_use]
97 pub fn label(&self, platform: Platform) -> String {
98 chord_label(&KeyEvent::new(self.code, self.modifiers), platform)
99 }
100 }
101
102 /// The key or keys that fire a binding.
103 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
104 pub enum BindingKeys {
105 One(KeyChord),
106 /// Two keys that do one thing in opposite directions, spelled `↑↓`
107 /// (`Up/Down` when ASCII-safe).
108 Pair(KeyChord, KeyChord),
109 }
110
111 impl BindingKeys {
112 #[must_use]
113 pub fn matches(&self, key: &KeyEvent) -> bool {
114 match self {
115 Self::One(a) => a.matches(key),
116 Self::Pair(a, b) => a.matches(key) || b.matches(key),
117 }
118 }
119
120 #[must_use]
121 pub fn label(&self, platform: Platform) -> String {
122 match self {
123 Self::One(a) => a.label(platform),
124 Self::Pair(a, b) if a.modifiers.is_empty() && b.modifiers.is_empty() => {
125 pair_label(a.code, b.code, platform)
126 }
127 Self::Pair(a, b) => format!("{}/{}", a.label(platform), b.label(platform)),
128 }
129 }
130 }
131
132 /// The priority a binding has until it is given one.
133 pub const DEFAULT_PRIORITY: u8 = 100;
134
135 /// One declared binding: keys, the verb shown for them, and the action a
136 /// host runs when they are pressed.
137 #[derive(Clone, Debug, PartialEq, Eq)]
138 pub struct Binding<A = ()> {
139 pub keys: BindingKeys,
140 /// Verb first, lower case: "move", "change", "reset". An empty verb
141 /// makes a binding that works but is not shown as a hint.
142 pub verb: Cow<'static, str>,
143 pub action: A,
144 /// Higher is more important and folds last. See [`DEFAULT_PRIORITY`].
145 pub priority: u8,
146 /// A heading for the full help list ("Navigation", "Editing").
147 pub group: Option<Cow<'static, str>>,
148 /// `false` dims the hint and the key does nothing right now.
149 pub enabled: bool,
150 /// The key that opens the full list: shown as `? more` when other hints
151 /// fold away, never as an ordinary hint.
152 pub is_help: bool,
153 }
154
155 impl<A> Binding<A> {
156 #[must_use]
157 pub fn new(keys: impl Into<KeyChord>, verb: impl Into<Cow<'static, str>>, action: A) -> Self {
158 Self {
159 keys: BindingKeys::One(keys.into()),
160 verb: verb.into(),
161 action,
162 priority: DEFAULT_PRIORITY,
163 group: None,
164 enabled: true,
165 is_help: false,
166 }
167 }
168
169 /// Two keys for one verb, spelled `↑↓`.
170 #[must_use]
171 pub fn pair(
172 a: impl Into<KeyChord>,
173 b: impl Into<KeyChord>,
174 verb: impl Into<Cow<'static, str>>,
175 action: A,
176 ) -> Self {
177 Self {
178 keys: BindingKeys::Pair(a.into(), b.into()),
179 ..Self::new(KeyCode::Null, verb, action)
180 }
181 }
182
183 /// The key that opens the full list of bindings.
184 #[must_use]
185 pub fn help(keys: impl Into<KeyChord>, action: A) -> Self {
186 Self {
187 is_help: true,
188 ..Self::new(keys, "", action)
189 }
190 }
191
192 #[must_use]
193 pub fn priority(mut self, priority: u8) -> Self {
194 self.priority = priority;
195 self
196 }
197
198 #[must_use]
199 pub fn group(mut self, group: impl Into<Cow<'static, str>>) -> Self {
200 self.group = Some(group.into());
201 self
202 }
203
204 #[must_use]
205 pub fn disabled(mut self) -> Self {
206 self.enabled = false;
207 self
208 }
209
210 /// The hint this binding shows, or `None` for a binding with no verb
211 /// and for the help binding.
212 #[must_use]
213 pub fn hint(&self, platform: Platform) -> Option<KeyHint> {
214 if self.is_help || self.verb.is_empty() {
215 return None;
216 }
217 let hint = KeyHint::new(self.keys.label(platform), self.verb.clone());
218 Some(if self.enabled { hint } else { hint.disabled() })
219 }
220 }
221
222 /// A declared set of bindings.
223 #[derive(Clone, Debug, PartialEq, Eq)]
224 pub struct Keymap<A = ()> {
225 bindings: Vec<Binding<A>>,
226 }
227
228 impl<A> Default for Keymap<A> {
229 fn default() -> Self {
230 Self {
231 bindings: Vec::new(),
232 }
233 }
234 }
235
236 impl<A> Keymap<A> {
237 #[must_use]
238 pub fn new() -> Self {
239 Self::default()
240 }
241
242 /// Add a binding. The first binding that matches a key wins.
243 #[must_use]
244 pub fn with(mut self, binding: Binding<A>) -> Self {
245 self.bindings.push(binding);
246 self
247 }
248
249 pub fn push(&mut self, binding: Binding<A>) {
250 self.bindings.push(binding);
251 }
252
253 #[must_use]
254 pub fn bindings(&self) -> &[Binding<A>] {
255 &self.bindings
256 }
257
258 #[must_use]
259 pub fn len(&self) -> usize {
260 self.bindings.len()
261 }
262
263 #[must_use]
264 pub fn is_empty(&self) -> bool {
265 self.bindings.is_empty()
266 }
267
268 /// The enabled binding `key` fires. Releases and disabled bindings match
269 /// nothing.
270 #[must_use]
271 pub fn find(&self, key: &KeyEvent) -> Option<&Binding<A>> {
272 self.bindings
273 .iter()
274 .find(|b| b.enabled && b.keys.matches(key))
275 }
276
277 /// The action `key` fires.
278 #[must_use]
279 pub fn lookup(&self, key: &KeyEvent) -> Option<&A> {
280 self.find(key).map(|b| &b.action)
281 }
282
283 /// The binding that opens the full list, if one is declared.
284 #[must_use]
285 pub fn help_binding(&self) -> Option<&Binding<A>> {
286 self.bindings.iter().find(|b| b.is_help)
287 }
288
289 /// Bindings gathered by group, in the order each group first appears.
290 /// Ungrouped bindings come first under `None`.
291 #[must_use]
292 pub fn grouped(&self) -> Vec<(Option<&str>, Vec<&Binding<A>>)> {
293 let mut out: Vec<(Option<&str>, Vec<&Binding<A>>)> = Vec::new();
294 for binding in &self.bindings {
295 let group = binding.group.as_deref();
296 match out.iter_mut().find(|(g, _)| *g == group) {
297 Some((_, members)) => members.push(binding),
298 None => out.push((group, vec![binding])),
299 }
300 }
301 out.sort_by_key(|(g, _)| g.is_some());
302 out
303 }
304
305 /// Every hint, in declaration order, with nothing folded away: the full
306 /// list a help view shows.
307 #[must_use]
308 pub fn all_hints(&self, platform: Platform) -> KeyHints {
309 KeyHints::new(
310 self.bindings
311 .iter()
312 .filter_map(|b| b.hint(platform))
313 .collect(),
314 )
315 }
316
317 /// The hints that fit one row `width` cells wide: lowest priority fold
318 /// first, `? more` says so. See [`KeyHints::from_keymap`].
319 #[must_use]
320 pub fn hints(&self, platform: Platform, width: u16, words: &KeyHintsWords) -> KeyHints {
321 KeyHints::from_keymap(self, platform, width, words)
322 }
323 }
324
325 #[cfg(test)]
326 mod tests {
327 use super::*;
328
329 const LINUX: Platform = Platform {
330 macos: false,
331 ascii: false,
332 };
333
334 fn press(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
335 KeyEvent::new(code, modifiers)
336 }
337
338 #[test]
339 fn chords_match_across_terminal_disagreements() {
340 let question = KeyChord::char('?');
341 assert!(question.matches(&press(KeyCode::Char('?'), KeyModifiers::NONE)));
342 assert!(question.matches(&press(KeyCode::Char('?'), KeyModifiers::SHIFT)));
343 let ctrl_o = KeyChord::ctrl('o');
344 assert!(ctrl_o.matches(&press(KeyCode::Char('O'), KeyModifiers::CONTROL)));
345 assert!(!ctrl_o.matches(&press(KeyCode::Char('o'), KeyModifiers::NONE)));
346 let back = KeyChord::plain(KeyCode::BackTab);
347 assert!(back.matches(&press(KeyCode::BackTab, KeyModifiers::SHIFT)));
348 let mut release = press(KeyCode::Char('?'), KeyModifiers::NONE);
349 release.kind = KeyEventKind::Release;
350 assert!(!question.matches(&release));
351 }
352
353 #[test]
354 fn a_pair_is_spelled_once_and_fires_on_either_key() {
355 let keys = BindingKeys::Pair(KeyChord::plain(KeyCode::Up), KeyChord::plain(KeyCode::Down));
356 assert_eq!(keys.label(LINUX), "↑↓");
357 assert!(keys.matches(&press(KeyCode::Down, KeyModifiers::NONE)));
358 assert!(!keys.matches(&press(KeyCode::Left, KeyModifiers::NONE)));
359 }
360
361 #[test]
362 fn disabled_bindings_fire_nothing_and_hint_dimmed() {
363 let map = Keymap::new()
364 .with(Binding::new(KeyChord::char('r'), "reset", 1).disabled())
365 .with(Binding::new(KeyChord::char('r'), "refresh", 2));
366 // The disabled binding does not shadow the enabled one behind it.
367 assert_eq!(
368 map.lookup(&press(KeyCode::Char('r'), KeyModifiers::NONE)),
369 Some(&2)
370 );
371 let hints = map.all_hints(LINUX);
372 assert!(!hints.items[0].enabled);
373 assert!(hints.items[1].enabled);
374 }
375
376 #[test]
377 fn groups_keep_first_seen_order_with_ungrouped_first() {
378 let map = Keymap::new()
379 .with(Binding::new(KeyCode::Enter, "select", ()).group("Choose"))
380 .with(Binding::new(KeyCode::Esc, "cancel", ()))
381 .with(Binding::new(KeyChord::char('r'), "reset", ()).group("Edit"))
382 .with(Binding::new(KeyChord::char('d'), "delete", ()).group("Choose"));
383 let groups = map.grouped();
384 let names: Vec<_> = groups.iter().map(|(g, _)| *g).collect();
385 assert_eq!(names, [None, Some("Choose"), Some("Edit")]);
386 assert_eq!(groups[1].1.len(), 2);
387 }
388
389 #[test]
390 fn the_help_binding_is_never_an_ordinary_hint() {
391 let map = Keymap::new()
392 .with(Binding::new(KeyCode::Enter, "select", ()))
393 .with(Binding::help(KeyChord::char('?'), ()));
394 assert_eq!(map.all_hints(LINUX).items.len(), 1);
395 assert!(map.help_binding().is_some());
396 }
397 }
398
398 lines RUST