Skip to main content

tui_pages/input/
registry.rs

1use crate::input::{InputHint, KeyChord};
2use std::collections::{HashMap, HashSet};
3
4#[derive(Debug, Clone)]
5pub struct KeyMap<A> {
6    pub id: String,
7    pub bindings: HashMap<Vec<KeyChord>, A>,
8}
9
10impl<A> KeyMap<A> {
11    pub fn new(id: impl Into<String>) -> Self {
12        Self {
13            id: id.into(),
14            bindings: HashMap::new(),
15        }
16    }
17
18    pub fn bind(&mut self, sequence: impl Into<Vec<KeyChord>>, action: A) {
19        self.bindings.insert(sequence.into(), action);
20    }
21
22    /// Remove the binding for an exact chord sequence, returning the action it
23    /// was mapped to (if any). Pair with [`crate::input::try_parse_binding`] to
24    /// drive a remap UI: `map.unbind(&try_parse_binding(old)?)`.
25    pub fn unbind(&mut self, sequence: &[KeyChord]) -> Option<A> {
26        self.bindings.remove(sequence)
27    }
28}
29
30impl<A: PartialEq> KeyMap<A> {
31    /// Every chord sequence currently bound to `action` in this map. Useful for
32    /// a help screen ("what fires this action?") or to find what to unbind
33    /// before rebinding.
34    pub fn bindings_for(&self, action: &A) -> Vec<&[KeyChord]> {
35        self.bindings
36            .iter()
37            .filter(|(_, bound)| *bound == action)
38            .map(|(sequence, _)| sequence.as_slice())
39            .collect()
40    }
41
42    /// Remove every binding mapped to `action`, returning how many were
43    /// removed. Lets a remap UI clear an action's old keys before assigning new
44    /// ones, regardless of how many sequences pointed at it.
45    pub fn unbind_action(&mut self, action: &A) -> usize {
46        let before = self.bindings.len();
47        self.bindings.retain(|_, bound| bound != action);
48        before - self.bindings.len()
49    }
50}
51
52#[derive(Debug, Clone)]
53pub struct InputRegistry<A> {
54    pub maps: HashMap<String, KeyMap<A>>,
55}
56
57impl<A> Default for InputRegistry<A> {
58    fn default() -> Self {
59        Self::empty()
60    }
61}
62
63impl<A> InputRegistry<A> {
64    pub fn empty() -> Self {
65        Self {
66            maps: HashMap::new(),
67        }
68    }
69
70    pub fn add_map(&mut self, map: KeyMap<A>) {
71        self.maps.insert(map.id.clone(), map);
72    }
73
74    pub fn map_mut(&mut self, id: impl Into<String>) -> &mut KeyMap<A> {
75        let id = id.into();
76        self.maps
77            .entry(id.clone())
78            .or_insert_with(|| KeyMap::new(id))
79    }
80
81    pub fn total_bindings(&self) -> usize {
82        self.maps.values().map(|map| map.bindings.len()).sum()
83    }
84}
85
86impl<A: Clone> InputRegistry<A> {
87    pub fn match_action(&self, sequence: &[KeyChord], modes: &[&str]) -> Option<A> {
88        for mode in modes {
89            if let Some(map) = self.maps.get(*mode) {
90                if let Some(action) = map.bindings.get(sequence) {
91                    return Some(action.clone());
92                }
93            }
94        }
95        None
96    }
97
98    pub fn get_hints(&self, prefix: &[KeyChord], modes: &[&str]) -> Vec<InputHint<A>> {
99        let mut hints = Vec::new();
100        let mut seen_keys = HashSet::new();
101
102        for mode in modes {
103            if let Some(map) = self.maps.get(*mode) {
104                for (sequence, action) in &map.bindings {
105                    if sequence.len() > prefix.len()
106                        && sequence.starts_with(prefix)
107                        && seen_keys.insert(sequence[prefix.len()])
108                    {
109                        hints.push(InputHint {
110                            key: sequence[prefix.len()],
111                            action: action.clone(),
112                        });
113                    }
114                }
115            }
116        }
117
118        hints
119    }
120
121    pub fn starts_sequence(&self, chord: &KeyChord, modes: &[&str]) -> bool {
122        let prefix = [*chord];
123        modes.iter().any(|mode| {
124            self.maps.get(*mode).is_some_and(|map| {
125                map.bindings
126                    .keys()
127                    .any(|sequence| sequence.starts_with(&prefix))
128            })
129        })
130    }
131
132    pub fn is_prefix(&self, sequence: &[KeyChord], modes: &[&str]) -> bool {
133        if sequence.is_empty() {
134            return false;
135        }
136
137        modes.iter().any(|mode| {
138            self.maps.get(*mode).is_some_and(|map| {
139                map.bindings.keys().any(|candidate| {
140                    candidate.len() > sequence.len() && candidate.starts_with(sequence)
141                })
142            })
143        })
144    }
145}