1mod chord;
20
21use std::collections::BTreeMap;
22use std::ops::Range;
23
24pub use chord::{Key, KeyChord, Modifiers};
25
26use crate::assets;
27use crate::diagnostics::Diagnostic;
28use crate::doc::{Doc, Value};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
32pub enum Scope {
33 Global,
35 App,
37}
38
39impl Scope {
40 fn table(self) -> &'static str {
41 match self {
42 Self::Global => "global",
43 Self::App => "app",
44 }
45 }
46
47 #[must_use]
49 pub fn label_key(self, action: &str) -> String {
50 match self {
51 Self::Global => format!("quvyta.keys.{action}"),
52 Self::App => format!("keys.{action}"),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Default, PartialEq, Eq)]
59pub struct Keymap {
60 bindings: BTreeMap<(Scope, String), Vec<KeyChord>>,
61}
62
63impl Keymap {
64 #[must_use]
66 pub fn builtin() -> Self {
67 let mut report = Vec::new();
68 let keymap = Self::parse("default.toml", assets::KEYMAP, &mut report);
69 debug_assert!(report.is_empty(), "built-in keymap must be valid: {report:?}");
70 keymap
71 }
72
73 #[must_use]
75 pub fn parse(file: &str, text: &str, report: &mut Vec<Diagnostic>) -> Self {
76 let doc = Doc::new(file, text);
77 let mut keymap = Self::default();
78 let root = match doc.parse() {
79 Ok(root) => root,
80 Err(diagnostic) => {
81 report.push(diagnostic);
82 return keymap;
83 }
84 };
85 for (section, value) in &root {
86 let scope = match section.get_ref().as_ref() {
87 "global" => Scope::Global,
88 "app" => Scope::App,
89 other => {
90 report.push(doc.error(&value.span(), format!("unknown section `{other}`; use [global] or [app]")));
91 continue;
92 }
93 };
94 let table = match doc.table(value, scope.table()) {
95 Ok(table) => table,
96 Err(diagnostic) => {
97 report.push(diagnostic);
98 continue;
99 }
100 };
101 for (action, binding) in table {
102 let action = action.get_ref().to_string();
103 if let Some(chords) = parse_binding(&doc, scope, &action, binding, report) {
104 keymap.bindings.insert((scope, action), chords);
105 }
106 }
107 }
108 keymap
109 }
110
111 pub fn overlay(&mut self, other: &Self) {
114 for (key, chords) in &other.bindings {
115 self.bindings.insert(key.clone(), chords.clone());
116 }
117 }
118
119 pub fn bind(&mut self, scope: Scope, action: &str, chords: &[KeyChord]) {
121 self.bindings.insert((scope, action.to_owned()), chords.to_vec());
122 }
123
124 #[must_use]
126 pub fn action_for(&self, chord: KeyChord) -> Option<(Scope, &str)> {
127 [Scope::App, Scope::Global].into_iter().find_map(|scope| {
128 self.bindings
129 .iter()
130 .find(|((s, _), chords)| *s == scope && chords.contains(&chord))
131 .map(|((s, action), _)| (*s, action.as_str()))
132 })
133 }
134
135 #[must_use]
137 pub fn chords_for(&self, scope: Scope, action: &str) -> &[KeyChord] {
138 self.bindings.get(&(scope, action.to_owned())).map_or(&[], Vec::as_slice)
139 }
140
141 #[must_use]
146 pub fn label_for(&self, scope: Scope, action: &str) -> Option<String> {
147 self.chords_for(scope, action).first().map(KeyChord::label)
148 }
149
150 pub fn iter(&self) -> impl Iterator<Item = (Scope, &str, &[KeyChord])> {
152 self.bindings.iter().map(|((scope, action), chords)| (*scope, action.as_str(), chords.as_slice()))
153 }
154
155 #[must_use]
157 pub fn conflicts(&self) -> Vec<Diagnostic> {
158 let mut owners: BTreeMap<(Scope, KeyChord), Vec<&str>> = BTreeMap::new();
159 for ((scope, action), chords) in &self.bindings {
160 for chord in chords {
161 owners.entry((*scope, *chord)).or_default().push(action);
162 }
163 }
164 owners
165 .into_iter()
166 .filter(|(_, actions)| actions.len() > 1)
167 .map(|((scope, chord), actions)| {
168 Diagnostic::warning(
169 None,
170 format!("`{chord}` is bound to several [{}] actions: {}", scope.table(), actions.join(", ")),
171 )
172 })
173 .collect()
174 }
175}
176
177fn parse_binding(
180 doc: &Doc<'_>,
181 scope: Scope,
182 action: &str,
183 binding: &Value<'_>,
184 report: &mut Vec<Diagnostic>,
185) -> Option<Vec<KeyChord>> {
186 let texts: Vec<(&str, Range<usize>)> = if let Some(text) = binding.get_ref().as_str() {
187 vec![(text, binding.span())]
188 } else if let Some(items) = binding.get_ref().as_array() {
189 items
190 .iter()
191 .filter_map(|item| match doc.string(item, &format!("{}.{action}", scope.table())) {
192 Ok(text) => Some((text, item.span())),
193 Err(diagnostic) => {
194 report.push(diagnostic);
195 None
196 }
197 })
198 .collect()
199 } else {
200 report.push(doc.error(&binding.span(), format!("`{action}` must be a key like \"ctrl+s\" or a list of keys")));
201 return None;
202 };
203 let mut chords = Vec::new();
204 for (text, span) in texts {
205 match text.parse::<KeyChord>() {
206 Ok(chord) => chords.push(chord),
207 Err(message) => report.push(doc.error(&span, message)),
208 }
209 }
210 Some(chords)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn chord(text: &str) -> KeyChord {
218 text.parse().expect("valid chord")
219 }
220
221 #[test]
222 fn builtin_binds_quit() {
223 let keymap = Keymap::builtin();
224 assert_eq!(keymap.action_for(chord("ctrl+q")), Some((Scope::Global, "quit")));
225 assert_eq!(keymap.chords_for(Scope::Global, "debug"), &[chord("f12")]);
226 }
227
228 #[test]
229 fn builtin_chords_keep_their_meaning_with_uppercase_letters_as_shift() {
230 let keymap = Keymap::builtin();
231 let shifted: Vec<String> = keymap
232 .iter()
233 .flat_map(|(_, action, chords)| {
234 chords.iter().filter(|c| c.mods.shift).map(move |c| format!("{action} {c}"))
235 })
236 .collect();
237 assert_eq!(shifted, ["focus-prev shift+tab"]);
238 assert_eq!(keymap.action_for(chord("?")), Some((Scope::Global, "help")));
239 let mut report = Vec::new();
240 let user = Keymap::parse("user.toml", "[app]\nsave = \"S\"\nsearch = \"s\"\n", &mut report);
241 assert!(report.is_empty(), "{report:?}");
242 assert_eq!(user.action_for(chord("shift+s")), Some((Scope::App, "save")));
243 assert_eq!(user.action_for(chord("s")), Some((Scope::App, "search")));
244 }
245
246 #[test]
247 fn parses_lists_and_reports_bad_entries() {
248 let mut report = Vec::new();
249 let keymap = Keymap::parse(
250 "app.toml",
251 "[app]\nsave = [\"ctrl+s\", \"f2\"]\nbroken = \"ctrl+banana\"\nweird = 5\n[extra]\n",
252 &mut report,
253 );
254 assert_eq!(keymap.chords_for(Scope::App, "save"), &[chord("ctrl+s"), chord("f2")]);
255 assert_eq!(report.len(), 3, "{report:?}");
256 assert_eq!(report[0].location.as_ref().map(|l| l.line), Some(3));
257 }
258
259 #[test]
260 fn a_list_keeps_its_good_keys() {
261 let mut report = Vec::new();
262 let keymap = Keymap::parse("app.toml", "[app]\nsave = [\"ctrl+s\", 3, \"ctrl+banana\"]\n", &mut report);
263 assert_eq!(keymap.chords_for(Scope::App, "save"), &[chord("ctrl+s")]);
264 let messages: Vec<&str> = report.iter().map(|d| d.message.as_str()).collect();
265 assert_eq!(messages.len(), 2, "{messages:?}");
266 assert_eq!(messages[0], "app.save must be a string, found integer");
267 }
268
269 #[test]
270 fn app_bindings_win_and_overlay_replaces_actions() {
271 let mut keymap = Keymap::builtin();
272 let mut report = Vec::new();
273 let user = Keymap::parse("user.toml", "[global]\nquit = \"ctrl+w\"\n[app]\nclose = \"ctrl+q\"\n", &mut report);
274 keymap.overlay(&user);
275 assert_eq!(keymap.action_for(chord("ctrl+q")), Some((Scope::App, "close")));
276 assert_eq!(keymap.action_for(chord("ctrl+w")), Some((Scope::Global, "quit")));
277 assert_eq!(Scope::App.label_key("close"), "keys.close");
278 assert_eq!(Scope::Global.label_key("quit"), "quvyta.keys.quit");
279 }
280
281 #[test]
282 fn reports_conflicts_within_a_scope() {
283 let mut keymap = Keymap::default();
284 keymap.bind(Scope::App, "save", &[chord("ctrl+s")]);
285 keymap.bind(Scope::App, "search", &[chord("ctrl+s")]);
286 keymap.bind(Scope::Global, "other", &[chord("ctrl+s")]);
287 let conflicts = keymap.conflicts();
288 assert_eq!(conflicts.len(), 1);
289 assert!(conflicts[0].message.contains("save, search"));
290 }
291
292 #[test]
293 fn the_label_of_an_action_follows_its_first_chord() {
294 let mut keymap = Keymap::default();
295 assert_eq!(keymap.label_for(Scope::App, "undo"), None, "an action without a chord names none");
296 keymap.bind(Scope::App, "undo", &["ctrl+z".parse().expect("chord"), "u".parse().expect("chord")]);
297 assert_eq!(keymap.label_for(Scope::App, "undo").as_deref(), Some("ctrl z"));
298 keymap.bind(Scope::App, "undo", &["alt+backspace".parse().expect("chord")]);
299 let chord: KeyChord = "alt+backspace".parse().expect("chord");
300 assert_eq!(keymap.label_for(Scope::App, "undo"), Some(chord.label()), "a rebinding moves it");
301 }
302}