| 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 |