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 pub fn iter(&self) -> impl Iterator<Item = (Scope, &str, &[KeyChord])> {
143 self.bindings.iter().map(|((scope, action), chords)| (*scope, action.as_str(), chords.as_slice()))
144 }
145
146 #[must_use]
148 pub fn conflicts(&self) -> Vec<Diagnostic> {
149 let mut owners: BTreeMap<(Scope, KeyChord), Vec<&str>> = BTreeMap::new();
150 for ((scope, action), chords) in &self.bindings {
151 for chord in chords {
152 owners.entry((*scope, *chord)).or_default().push(action);
153 }
154 }
155 owners
156 .into_iter()
157 .filter(|(_, actions)| actions.len() > 1)
158 .map(|((scope, chord), actions)| {
159 Diagnostic::warning(
160 None,
161 format!("`{chord}` is bound to several [{}] actions: {}", scope.table(), actions.join(", ")),
162 )
163 })
164 .collect()
165 }
166}
167
168fn parse_binding(
171 doc: &Doc<'_>,
172 scope: Scope,
173 action: &str,
174 binding: &Value<'_>,
175 report: &mut Vec<Diagnostic>,
176) -> Option<Vec<KeyChord>> {
177 let texts: Vec<(&str, Range<usize>)> = if let Some(text) = binding.get_ref().as_str() {
178 vec![(text, binding.span())]
179 } else if let Some(items) = binding.get_ref().as_array() {
180 items
181 .iter()
182 .filter_map(|item| match doc.string(item, &format!("{}.{action}", scope.table())) {
183 Ok(text) => Some((text, item.span())),
184 Err(diagnostic) => {
185 report.push(diagnostic);
186 None
187 }
188 })
189 .collect()
190 } else {
191 report.push(doc.error(&binding.span(), format!("`{action}` must be a key like \"ctrl+s\" or a list of keys")));
192 return None;
193 };
194 let mut chords = Vec::new();
195 for (text, span) in texts {
196 match text.parse::<KeyChord>() {
197 Ok(chord) => chords.push(chord),
198 Err(message) => report.push(doc.error(&span, message)),
199 }
200 }
201 Some(chords)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn chord(text: &str) -> KeyChord {
209 text.parse().expect("valid chord")
210 }
211
212 #[test]
213 fn builtin_binds_quit() {
214 let keymap = Keymap::builtin();
215 assert_eq!(keymap.action_for(chord("ctrl+q")), Some((Scope::Global, "quit")));
216 assert_eq!(keymap.chords_for(Scope::Global, "debug"), &[chord("f12")]);
217 }
218
219 #[test]
220 fn builtin_chords_keep_their_meaning_with_uppercase_letters_as_shift() {
221 let keymap = Keymap::builtin();
222 let shifted: Vec<String> = keymap
223 .iter()
224 .flat_map(|(_, action, chords)| {
225 chords.iter().filter(|c| c.mods.shift).map(move |c| format!("{action} {c}"))
226 })
227 .collect();
228 assert_eq!(shifted, ["focus-prev shift+tab"]);
229 assert_eq!(keymap.action_for(chord("?")), Some((Scope::Global, "help")));
230 let mut report = Vec::new();
231 let user = Keymap::parse("user.toml", "[app]\nsave = \"S\"\nsearch = \"s\"\n", &mut report);
232 assert!(report.is_empty(), "{report:?}");
233 assert_eq!(user.action_for(chord("shift+s")), Some((Scope::App, "save")));
234 assert_eq!(user.action_for(chord("s")), Some((Scope::App, "search")));
235 }
236
237 #[test]
238 fn parses_lists_and_reports_bad_entries() {
239 let mut report = Vec::new();
240 let keymap = Keymap::parse(
241 "app.toml",
242 "[app]\nsave = [\"ctrl+s\", \"f2\"]\nbroken = \"ctrl+banana\"\nweird = 5\n[extra]\n",
243 &mut report,
244 );
245 assert_eq!(keymap.chords_for(Scope::App, "save"), &[chord("ctrl+s"), chord("f2")]);
246 assert_eq!(report.len(), 3, "{report:?}");
247 assert_eq!(report[0].location.as_ref().map(|l| l.line), Some(3));
248 }
249
250 #[test]
251 fn a_list_keeps_its_good_keys() {
252 let mut report = Vec::new();
253 let keymap = Keymap::parse("app.toml", "[app]\nsave = [\"ctrl+s\", 3, \"ctrl+banana\"]\n", &mut report);
254 assert_eq!(keymap.chords_for(Scope::App, "save"), &[chord("ctrl+s")]);
255 let messages: Vec<&str> = report.iter().map(|d| d.message.as_str()).collect();
256 assert_eq!(messages.len(), 2, "{messages:?}");
257 assert_eq!(messages[0], "app.save must be a string, found integer");
258 }
259
260 #[test]
261 fn app_bindings_win_and_overlay_replaces_actions() {
262 let mut keymap = Keymap::builtin();
263 let mut report = Vec::new();
264 let user = Keymap::parse("user.toml", "[global]\nquit = \"ctrl+w\"\n[app]\nclose = \"ctrl+q\"\n", &mut report);
265 keymap.overlay(&user);
266 assert_eq!(keymap.action_for(chord("ctrl+q")), Some((Scope::App, "close")));
267 assert_eq!(keymap.action_for(chord("ctrl+w")), Some((Scope::Global, "quit")));
268 assert_eq!(Scope::App.label_key("close"), "keys.close");
269 assert_eq!(Scope::Global.label_key("quit"), "quvyta.keys.quit");
270 }
271
272 #[test]
273 fn reports_conflicts_within_a_scope() {
274 let mut keymap = Keymap::default();
275 keymap.bind(Scope::App, "save", &[chord("ctrl+s")]);
276 keymap.bind(Scope::App, "search", &[chord("ctrl+s")]);
277 keymap.bind(Scope::Global, "other", &[chord("ctrl+s")]);
278 let conflicts = keymap.conflicts();
279 assert_eq!(conflicts.len(), 1);
280 assert!(conflicts[0].message.contains("save, search"));
281 }
282}