Skip to main content

rich_interact/
keymap.rs

1//! The keymap registry: what each key does, declared rather than matched
2//! inline, so it can be rebound and listed (0.0.14 workstream 1).
3//!
4//! A component declares its [`Binding`]s in a [`Keymap`]: an action name
5//! (`down`), the keys that do it (`down`, `ctrl+n`), a description for
6//! people (`move down`) and a context (`select`) that tells one
7//! component's `down` from another's. It then asks the keymap what a key
8//! means ([`Keymap::action`]) instead of matching the key itself.
9//!
10//! Keys are rebound at two levels, the first that has the action winning:
11//!
12//! 1. on one keymap, with [`Keymap::rebind`];
13//! 2. for the whole process, with [`install`]ed [`Overrides`]: the hook a
14//!    configuration file (the CLI's, say) uses, one `context.action = keys`
15//!    per line.
16//!
17//! [`Component::keymap`](crate::Component::keymap) returns the bindings a
18//! component has right now; a container's includes its focused child's.
19//! That is the query a help overlay, a shortcut list or a status bar's
20//! hints read, so a component you write gets them by declaring its keys.
21//!
22//! ```
23//! use rich_interact::keymap::{keys, Keymap};
24//! use rich_interact::Key;
25//!
26//! let mut keymap = Keymap::new("counter")
27//!     .bind("up", keys("up k"), "count up")
28//!     .bind("done", keys("enter"), "finish");
29//! assert_eq!(keymap.action(Key::char('k')), Some("up"));
30//! keymap.rebind("up", keys("+"));
31//! assert_eq!(keymap.action(Key::char('k')), None);
32//! assert_eq!(keymap.action(Key::char('+')), Some("up"));
33//! ```
34
35use std::collections::HashMap;
36use std::fmt;
37use std::sync::RwLock;
38
39use crate::event::Key;
40
41/// Keys from their names separated by spaces (see [`Key::parse`]).
42///
43/// # Panics
44/// On a name that does not parse: bindings are written in code. Use
45/// [`try_keys`] for text from a user.
46pub fn keys(names: &str) -> Vec<Key> {
47    try_keys(names).unwrap_or_else(|name| panic!("unknown key {name:?}"))
48}
49
50/// Keys from names separated by spaces or commas, or the first name that
51/// does not parse. The comma key is named `comma` (`ctrl+comma`). A name in
52/// quotes (`"#"`, `','`, `"ctrl+,"`) is taken as it is, which is the other
53/// way to write the comma key, and how to write the hash key in a file
54/// where `#` starts a comment. Text that holds only separators (a bare `,`)
55/// is an error, not "no keys"; text with nothing in it is no keys.
56///
57/// ```
58/// use rich_interact::keymap::try_keys;
59/// use rich_interact::Key;
60///
61/// assert_eq!(try_keys("comma ctrl+n").unwrap(), [Key::char(','), Key::ctrl('n')]);
62/// assert_eq!(try_keys("\",\" ctrl+n").unwrap(), [Key::char(','), Key::ctrl('n')]);
63/// assert_eq!(try_keys("").unwrap(), []);
64/// assert!(try_keys(",").is_err());
65/// ```
66pub fn try_keys(names: &str) -> Result<Vec<Key>, String> {
67    let names = key_names(names)?;
68    names
69        .iter()
70        .map(|name| Key::parse(name).ok_or_else(|| name.clone()))
71        .collect()
72}
73
74/// The key names in `text`: separated by whitespace or commas, or quoted.
75/// An unclosed quote, or separators with no name, is an error (the text).
76fn key_names(text: &str) -> Result<Vec<String>, String> {
77    let mut names = Vec::new();
78    let mut name = String::new();
79    let mut chars = text.chars();
80    while let Some(c) = chars.next() {
81        match c {
82            '"' | '\'' if name.is_empty() => {
83                let mut quoted = String::new();
84                let mut closed = false;
85                for d in chars.by_ref() {
86                    if d == c {
87                        closed = true;
88                        break;
89                    }
90                    quoted.push(d);
91                }
92                if !closed || quoted.is_empty() {
93                    return Err(text.trim().to_string());
94                }
95                names.push(quoted);
96            }
97            c if c.is_whitespace() || c == ',' => {
98                if !name.is_empty() {
99                    names.push(std::mem::take(&mut name));
100                }
101            }
102            c => name.push(c),
103        }
104    }
105    if !name.is_empty() {
106        names.push(name);
107    }
108    if names.is_empty() && !text.trim().is_empty() {
109        return Err(text.trim().to_string());
110    }
111    Ok(names)
112}
113
114/// One thing a key does: an action, in a context, with a description.
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct Binding {
117    /// Which component or container it belongs to: `select`, `split`.
118    pub context: String,
119    /// What it does, for code and configuration: `down`, `focus-next`.
120    pub action: String,
121    /// The keys that do it, first the one to show.
122    pub keys: Vec<Key>,
123    /// What it does, for people: `move down`.
124    pub description: String,
125    /// The keys are pressed one after another (`g g`), rather than being
126    /// alternatives: a key sequence an app runs itself, listed in a help
127    /// or a palette. A keymap does not look these up.
128    pub sequence: bool,
129    /// Why it cannot be done now, when it cannot: a help or a palette
130    /// lists it greyed, with the reason, and a palette does not run it.
131    pub unavailable: Option<String>,
132}
133
134impl Binding {
135    pub fn new(
136        context: impl Into<String>,
137        action: impl Into<String>,
138        keys: impl IntoIterator<Item = Key>,
139        description: impl Into<String>,
140    ) -> Binding {
141        Binding {
142            context: context.into(),
143            action: action.into(),
144            keys: keys.into_iter().collect(),
145            description: description.into(),
146            sequence: false,
147            unavailable: None,
148        }
149    }
150
151    /// A binding of keys pressed one after another (`g` then `g`), for a
152    /// help or a palette to list: shown as `g g`.
153    ///
154    /// ```
155    /// use rich_interact::keymap::{keys, Binding};
156    ///
157    /// let top = Binding::sequence("files", "top", keys("g g"), "go to the top");
158    /// assert_eq!(top.keys_label(), "g g");
159    /// ```
160    pub fn sequence(
161        context: impl Into<String>,
162        action: impl Into<String>,
163        keys: impl IntoIterator<Item = Key>,
164        description: impl Into<String>,
165    ) -> Binding {
166        Binding {
167            sequence: true,
168            ..Binding::new(context, action, keys, description)
169        }
170    }
171
172    /// Listed but not available now, saying `reason` (see
173    /// [`unavailable`](Self::unavailable)).
174    ///
175    /// ```
176    /// use rich_interact::keymap::{keys, Binding};
177    ///
178    /// let save = Binding::new("editor", "save", keys("ctrl+s"), "save").unavailable("read-only");
179    /// assert_eq!(save.unavailable.as_deref(), Some("read-only"));
180    /// ```
181    #[must_use]
182    pub fn unavailable(mut self, reason: impl Into<String>) -> Binding {
183        self.unavailable = Some(reason.into());
184        self
185    }
186
187    /// `context.action`: how configuration names it.
188    pub fn id(&self) -> String {
189        format!("{}.{}", self.context, self.action)
190    }
191
192    /// The keys as people read them: `up/ctrl+p`, or `g g` for a
193    /// [sequence](Self::sequence).
194    pub fn keys_label(&self) -> String {
195        self.keys
196            .iter()
197            .map(Key::to_string)
198            .collect::<Vec<_>>()
199            .join(if self.sequence { " " } else { "/" })
200    }
201
202    /// The keys as a hint shows them: `↑/ctrl+p`, or `g g` for a
203    /// [sequence](Self::sequence).
204    pub fn hint(&self) -> String {
205        crate::chrome::keys_hint_of(&self.keys, self.sequence)
206    }
207}
208
209impl fmt::Display for Binding {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "{}  {}", self.keys_label(), self.description)
212    }
213}
214
215/// A component's bindings, and the keys rebound on it.
216#[derive(Clone, Debug, Default)]
217pub struct Keymap {
218    context: String,
219    bindings: Vec<Binding>,
220    /// Actions rebound on this keymap, by `context.action`.
221    rebound: HashMap<String, Vec<Key>>,
222}
223
224impl Keymap {
225    /// An empty keymap whose bindings are in `context`.
226    pub fn new(context: impl Into<String>) -> Keymap {
227        Keymap {
228            context: context.into(),
229            ..Keymap::default()
230        }
231    }
232
233    /// The context [`bind`](Self::bind) puts bindings in.
234    pub fn context(&self) -> &str {
235        &self.context
236    }
237
238    /// Declare that `keys` do `action`, described as `description`.
239    #[must_use]
240    pub fn bind(
241        mut self,
242        action: impl Into<String>,
243        keys: impl IntoIterator<Item = Key>,
244        description: impl Into<String>,
245    ) -> Keymap {
246        let binding = Binding::new(self.context.clone(), action, keys, description);
247        self.add(binding);
248        self
249    }
250
251    /// Add a binding, in whatever context it has. A binding for an action
252    /// already declared in that context replaces it.
253    pub fn add(&mut self, binding: Binding) {
254        match self
255            .bindings
256            .iter_mut()
257            .find(|b| b.context == binding.context && b.action == binding.action)
258        {
259            Some(existing) => *existing = binding,
260            None => self.bindings.push(binding),
261        }
262    }
263
264    /// Add every binding of `other` after this one's, keeping what `other`
265    /// rebound: how a container lists its focused child's keys with its
266    /// own.
267    pub fn extend(&mut self, other: Keymap) {
268        for (id, keys) in other.rebound {
269            self.rebound.entry(id).or_insert(keys);
270        }
271        for binding in other.bindings {
272            if !self
273                .bindings
274                .iter()
275                .any(|b| b.context == binding.context && b.action == binding.action)
276            {
277                self.bindings.push(binding);
278            }
279        }
280    }
281
282    /// Make `keys` do `action` (in this keymap's context) instead of what
283    /// was declared or installed. No keys unbinds it.
284    pub fn rebind(&mut self, action: &str, keys: impl IntoIterator<Item = Key>) {
285        let id = format!("{}.{action}", self.context);
286        self.rebound.insert(id, keys.into_iter().collect());
287    }
288
289    /// Builder form of [`rebind`](Self::rebind).
290    #[must_use]
291    pub fn rebound(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Keymap {
292        self.rebind(action, keys);
293        self
294    }
295
296    /// Apply `overrides` on this keymap only, over anything installed.
297    pub fn apply(&mut self, overrides: &Overrides) {
298        for (id, keys) in &overrides.keys {
299            self.rebound.insert(id.clone(), keys.clone());
300        }
301    }
302
303    /// The keys set for `binding`'s action, and where they come from:
304    /// 2 rebound on this keymap, 1 installed, 0 declared.
305    fn source(&self, binding: &Binding) -> (u8, Vec<Key>) {
306        if self.plain() {
307            return (0, binding.keys.clone());
308        }
309        let id = binding.id();
310        if let Some(keys) = self.rebound.get(&id) {
311            return (2, keys.clone());
312        }
313        if let Some(keys) = installed_keys(&id) {
314            return (1, keys);
315        }
316        (0, binding.keys.clone())
317    }
318
319    /// Nothing rebound here and nothing installed: each binding's keys are
320    /// the ones it declared.
321    fn plain(&self) -> bool {
322        self.rebound.is_empty() && !any_installed()
323    }
324
325    /// The keys that do `binding`'s action now: what was set for it, less
326    /// any key a closer level gave another action in the same context (a
327    /// key rebound onto `down` is no longer the declared `next`'s).
328    fn effective(&self, binding: &Binding) -> Vec<Key> {
329        if self.plain() {
330            // Nothing rebound or installed: every key is the declared one.
331            return binding.keys.clone();
332        }
333        let (level, mut keys) = self.source(binding);
334        if level < 2 {
335            for other in &self.bindings {
336                if other.context != binding.context || other.action == binding.action {
337                    continue;
338                }
339                let (other_level, taken) = self.source(other);
340                if other_level > level {
341                    keys.retain(|key| !taken.contains(key));
342                }
343            }
344        }
345        keys
346    }
347
348    /// The index of the binding `key` triggers: one whose keys were rebound
349    /// or installed before one declared, then the first. A binding that
350    /// names the key wins over one it only [`matches`](Key::matches), so a
351    /// Tab from a legacy terminal does `tab`'s action before `ctrl+i`'s.
352    fn find(&self, key: Key) -> Option<usize> {
353        self.find_by(|keys| keys.contains(&key))
354            .or_else(|| self.find_by(|keys| key.matches_any(keys)))
355    }
356
357    fn find_by(&self, fires: impl Fn(&[Key]) -> bool) -> Option<usize> {
358        if self.plain() {
359            // Every binding at the declared level: the first that fires,
360            // with no key list built (a key press looks every binding up).
361            return (self.bindings.iter())
362                .position(|binding| !binding.sequence && fires(&binding.keys));
363        }
364        let mut found: Option<(u8, usize)> = None;
365        for (index, binding) in self.bindings.iter().enumerate() {
366            if binding.sequence {
367                continue;
368            }
369            let (level, _) = self.source(binding);
370            if found.is_some_and(|(best, _)| best >= level) {
371                continue;
372            }
373            if fires(&self.effective(binding)) {
374                found = Some((level, index));
375            }
376        }
377        found.map(|(_, index)| index)
378    }
379
380    /// The binding `key` triggers, if any: see [`action`](Self::action).
381    pub fn lookup(&self, key: Key) -> Option<Binding> {
382        self.find(key).map(|index| {
383            let binding = &self.bindings[index];
384            Binding {
385                keys: self.effective(binding),
386                ..binding.clone()
387            }
388        })
389    }
390
391    /// The action `key` triggers, if any: a binding whose keys were rebound
392    /// (or installed) wins over one with its declared keys, so a key moved
393    /// onto an action does it even when an earlier action declared it;
394    /// otherwise the first declared.
395    pub fn action(&self, key: Key) -> Option<&str> {
396        self.find(key)
397            .map(|index| self.bindings[index].action.as_str())
398    }
399
400    /// Whether `key` triggers `action`.
401    pub fn is(&self, key: Key, action: &str) -> bool {
402        key.matches_any(&self.keys(action))
403    }
404
405    /// The keys that trigger `action` (in any context) now.
406    pub fn keys(&self, action: &str) -> Vec<Key> {
407        self.bindings
408            .iter()
409            .find(|binding| binding.action == action)
410            .map(|binding| self.effective(binding))
411            .unwrap_or_default()
412    }
413
414    /// The first key for `action`, to show in a hint.
415    pub fn key(&self, action: &str) -> Option<Key> {
416        self.keys(action).first().copied()
417    }
418
419    /// Every binding, with the keys that do it now.
420    pub fn bindings(&self) -> Vec<Binding> {
421        self.bindings
422            .iter()
423            .map(|binding| Binding {
424                keys: self.effective(binding),
425                ..binding.clone()
426            })
427            .collect()
428    }
429
430    /// Every binding as declared, before rebinding.
431    pub fn declared(&self) -> &[Binding] {
432        &self.bindings
433    }
434
435    pub fn is_empty(&self) -> bool {
436        self.bindings.is_empty()
437    }
438
439    pub fn len(&self) -> usize {
440        self.bindings.len()
441    }
442}
443
444/// Keys rebound by `context.action`: what a configuration file sets.
445#[derive(Clone, Debug, Default, PartialEq, Eq)]
446pub struct Overrides {
447    keys: HashMap<String, Vec<Key>>,
448}
449
450impl Overrides {
451    pub fn new() -> Overrides {
452        Overrides::default()
453    }
454
455    /// Make `keys` do `context.action`. No keys unbinds it.
456    pub fn set(
457        &mut self,
458        context: &str,
459        action: &str,
460        keys: impl IntoIterator<Item = Key>,
461    ) -> &mut Self {
462        self.keys
463            .insert(format!("{context}.{action}"), keys.into_iter().collect());
464        self
465    }
466
467    /// The keys set for `context.action`, if any.
468    pub fn get(&self, id: &str) -> Option<&[Key]> {
469        self.keys.get(id).map(Vec::as_slice)
470    }
471
472    pub fn is_empty(&self) -> bool {
473        self.keys.is_empty()
474    }
475
476    /// `(context.action, keys)` pairs, as a configuration table holds them
477    /// (`"select.down" = "ctrl+n, down"`).
478    pub fn from_pairs<I, A, K>(pairs: I) -> Result<Overrides, String>
479    where
480        I: IntoIterator<Item = (A, K)>,
481        A: AsRef<str>,
482        K: AsRef<str>,
483    {
484        let mut overrides = Overrides::new();
485        for (id, names) in pairs {
486            let id = id.as_ref().trim();
487            let Some((context, action)) = id.rsplit_once('.') else {
488                return Err(format!("{id:?} is not context.action"));
489            };
490            if context.is_empty() || action.is_empty() {
491                return Err(format!("{id:?} is not context.action"));
492            }
493            let keys = try_keys(names.as_ref()).map_err(|name| {
494                if name.contains([',', '"', '\'']) {
495                    format!("{id}: no key in {name:?} (the comma key is `comma`, or quoted: \",\")")
496                } else {
497                    format!("{id}: unknown key {name:?}")
498                }
499            })?;
500            overrides.set(context, action, keys);
501        }
502        Ok(overrides)
503    }
504
505    /// One `context.action = key, key` per line; blank lines are skipped,
506    /// and `#` at the start of a line or after a space starts a comment
507    /// (`ctrl+#` is a key). Quote a key that is a separator or a comment:
508    /// `"#"`, `","`. `none` unbinds the action; a line with no keys is an
509    /// error, so a stray `#` or `,` never unbinds one by accident.
510    ///
511    /// ```
512    /// use rich_interact::keymap::Overrides;
513    /// use rich_interact::Key;
514    ///
515    /// let overrides = Overrides::parse("select.down = \"#\" j # keys\nselect.up = none").unwrap();
516    /// assert_eq!(overrides.get("select.down").unwrap(), [Key::char('#'), Key::char('j')]);
517    /// assert_eq!(overrides.get("select.up").unwrap(), []);
518    /// assert!(Overrides::parse("select.down = #").is_err());
519    /// ```
520    pub fn parse(text: &str) -> Result<Overrides, String> {
521        let mut pairs = Vec::new();
522        for (number, line) in text.lines().enumerate() {
523            let line = strip_comment(line).trim();
524            if line.is_empty() {
525                continue;
526            }
527            let Some((id, names)) = line.split_once('=') else {
528                return Err(format!(
529                    "line {}: expected context.action = keys",
530                    number + 1
531                ));
532            };
533            let (id, names) = (id.trim(), names.trim());
534            if names.is_empty() {
535                return Err(format!(
536                    "line {}: {id} has no keys (quote a \"#\" key; `none` unbinds)",
537                    number + 1
538                ));
539            }
540            let names = if names.eq_ignore_ascii_case("none") {
541                ""
542            } else {
543                names
544            };
545            pairs.push((id.to_string(), names.to_string()));
546        }
547        Overrides::from_pairs(pairs)
548    }
549}
550
551/// `line` without its comment: from a `#` at the start or after
552/// whitespace, outside quotes.
553fn strip_comment(line: &str) -> &str {
554    let mut quote = None;
555    let mut previous = ' ';
556    for (index, c) in line.char_indices() {
557        match quote {
558            Some(open) if c == open => quote = None,
559            Some(_) => {}
560            None if c == '"' || c == '\'' => quote = Some(c),
561            None if c == '#' && previous.is_whitespace() => return &line[..index],
562            None => {}
563        }
564        previous = c;
565    }
566    line
567}
568
569static INSTALLED: RwLock<Option<Overrides>> = RwLock::new(None);
570
571/// Rebind keys for every keymap in the process, from now on: the hook for
572/// a configuration file. Replaces what was installed before.
573pub fn install(overrides: Overrides) {
574    let mut installed = INSTALLED.write().unwrap_or_else(|e| e.into_inner());
575    *installed = (!overrides.is_empty()).then_some(overrides);
576}
577
578/// What [`install`] set, if anything.
579pub fn installed() -> Overrides {
580    INSTALLED
581        .read()
582        .unwrap_or_else(|e| e.into_inner())
583        .clone()
584        .unwrap_or_default()
585}
586
587/// Whether any keys are installed for any binding.
588fn any_installed() -> bool {
589    let installed = INSTALLED.read().unwrap_or_else(|e| e.into_inner());
590    installed.as_ref().is_some_and(|keys| !keys.is_empty())
591}
592
593fn installed_keys(id: &str) -> Option<Vec<Key>> {
594    let installed = INSTALLED.read().unwrap_or_else(|e| e.into_inner());
595    installed.as_ref()?.get(id).map(<[Key]>::to_vec)
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn parses_overrides() {
604        let overrides = Overrides::parse(
605            "# mine\nselect.down = ctrl+n, down\n\ntabs.next = alt+l # comment\nx.y = none\n",
606        )
607        .unwrap();
608        assert_eq!(
609            overrides.get("select.down").unwrap(),
610            [Key::ctrl('n'), Key::new(crate::KeyCode::Down)]
611        );
612        assert_eq!(overrides.get("x.y").unwrap(), []);
613        assert!(Overrides::parse("nodot = up").is_err());
614        assert!(Overrides::parse("a.b = nokey").is_err());
615        assert!(Overrides::parse("a.b up").is_err());
616    }
617
618    #[test]
619    fn installed_overrides_apply_below_local_ones() {
620        let keymap = Keymap::new("keymap-test-context").bind("go", keys("g"), "go");
621        let mut overrides = Overrides::new();
622        overrides.set("keymap-test-context", "go", keys("h"));
623        install(overrides);
624        assert_eq!(keymap.action(Key::char('h')), Some("go"));
625        assert_eq!(keymap.action(Key::char('g')), None);
626        let local = keymap.clone().rebound("go", keys("j"));
627        assert_eq!(local.action(Key::char('j')), Some("go"));
628        install(Overrides::new());
629        assert_eq!(keymap.action(Key::char('g')), Some("go"));
630    }
631
632    #[test]
633    fn extends_and_lists() {
634        let mut outer = Keymap::new("outer").bind("quit", keys("q"), "quit");
635        let inner = Keymap::new("inner")
636            .bind("down", keys("down j"), "move down")
637            .rebound("down", keys("n"));
638        outer.extend(inner);
639        let listed: Vec<String> = outer.bindings().iter().map(ToString::to_string).collect();
640        assert_eq!(listed, ["q  quit", "n  move down"]);
641        assert_eq!(outer.lookup(Key::char('n')).unwrap().id(), "inner.down");
642    }
643
644    #[test]
645    fn a_legacy_key_does_the_action_of_any_key_it_could_be() {
646        use crate::KeyCode;
647        let keymap = Keymap::new("legacy-test")
648            .bind("indent", keys("ctrl+i"), "indent")
649            .bind("submit", keys("ctrl+m"), "submit");
650        let tab = Key::new(KeyCode::Tab);
651        assert_eq!(keymap.action(tab), Some("indent"));
652        assert!(keymap.is(tab, "indent"));
653        assert_eq!(keymap.action(Key::new(KeyCode::Enter)), Some("submit"));
654        // With the kitty protocol, Tab is only Tab and Ctrl+I only Ctrl+I.
655        assert_eq!(keymap.action(tab.exact()), None);
656        assert!(!keymap.is(tab.exact(), "indent"));
657        assert_eq!(keymap.action(Key::ctrl('i').exact()), Some("indent"));
658        // A binding that names the key wins over one it could be.
659        let keymap = keymap.bind("next", keys("tab"), "next field");
660        assert_eq!(keymap.action(tab), Some("next"));
661        assert_eq!(keymap.action(Key::ctrl('i')), Some("indent"));
662    }
663
664    #[test]
665    fn a_rebound_key_moves_from_an_earlier_action() {
666        let keymap = Keymap::new("move-test")
667            .bind("next", keys("n"), "next match")
668            .bind("down", keys("down j"), "scroll down")
669            .rebound("down", keys("n"));
670        assert_eq!(keymap.action(Key::char('n')), Some("down"));
671        assert_eq!(keymap.lookup(Key::char('n')).unwrap().action, "down");
672        // `next` lost the key it had: nothing else is listed against `n`.
673        assert_eq!(keymap.keys("next"), []);
674        assert!(!keymap.is(Key::char('n'), "next"));
675        // A key no one rebound keeps its declared action.
676        let keymap = keymap.rebound("next", keys("m"));
677        assert_eq!(keymap.action(Key::char('m')), Some("next"));
678    }
679}