1use std::fmt;
58use std::str::FromStr;
59
60use iced::Subscription;
61use iced::keyboard::{self, Key, Modifiers, key::Named};
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Event {
66 pub key: Key,
67 pub modifiers: Modifiers,
68}
69
70pub fn subscription() -> Subscription<Event> {
72 keyboard::listen().filter_map(|event| match event {
73 keyboard::Event::KeyPressed { key, modifiers, .. } => Some(Event { key, modifiers }),
74 _ => None,
75 })
76}
77
78const NAMED: &[(Named, &[&str])] = &[
80 (Named::Enter, &["Enter", "Return"]),
81 (Named::Tab, &["Tab"]),
82 (Named::Space, &["Space", "Spacebar"]),
83 (Named::Escape, &["Escape", "Esc"]),
84 (Named::Backspace, &["Backspace"]),
85 (Named::Delete, &["Delete", "Del"]),
86 (Named::Insert, &["Insert", "Ins"]),
87 (Named::ArrowUp, &["ArrowUp", "Up"]),
88 (Named::ArrowDown, &["ArrowDown", "Down"]),
89 (Named::ArrowLeft, &["ArrowLeft", "Left"]),
90 (Named::ArrowRight, &["ArrowRight", "Right"]),
91 (Named::Home, &["Home"]),
92 (Named::End, &["End"]),
93 (Named::PageUp, &["PageUp", "PgUp"]),
94 (Named::PageDown, &["PageDown", "PgDn"]),
95 (Named::ContextMenu, &["Menu", "ContextMenu", "Apps"]),
96 (Named::F1, &["F1"]),
97 (Named::F2, &["F2"]),
98 (Named::F3, &["F3"]),
99 (Named::F4, &["F4"]),
100 (Named::F5, &["F5"]),
101 (Named::F6, &["F6"]),
102 (Named::F7, &["F7"]),
103 (Named::F8, &["F8"]),
104 (Named::F9, &["F9"]),
105 (Named::F10, &["F10"]),
106 (Named::F11, &["F11"]),
107 (Named::F12, &["F12"]),
108];
109
110const LOGO: &str = if cfg!(target_os = "macos") {
112 "Cmd"
113} else {
114 "Super"
115};
116
117#[derive(Debug, Clone, PartialEq, Eq, Hash)]
135pub struct Chord {
136 key: Key,
137 modifiers: Modifiers,
138}
139
140impl Chord {
141 pub fn new(key: Key, modifiers: Modifiers) -> Self {
143 Self {
144 key: normalise(key),
145 modifiers,
146 }
147 }
148
149 pub fn named(key: Named) -> Self {
151 Self::new(Key::Named(key), Modifiers::empty())
152 }
153
154 pub fn character(key: char) -> Self {
156 Self::new(Key::Character(key.to_string().into()), Modifiers::empty())
157 }
158
159 pub fn ctrl(self) -> Self {
160 self.with(Modifiers::CTRL)
161 }
162
163 pub fn shift(self) -> Self {
164 self.with(Modifiers::SHIFT)
165 }
166
167 pub fn alt(self) -> Self {
168 self.with(Modifiers::ALT)
169 }
170
171 pub fn logo(self) -> Self {
173 self.with(Modifiers::LOGO)
174 }
175
176 pub fn command(self) -> Self {
178 self.with(Modifiers::COMMAND)
179 }
180
181 fn with(mut self, modifiers: Modifiers) -> Self {
182 self.modifiers |= modifiers;
183 self
184 }
185
186 pub fn key(&self) -> &Key {
187 &self.key
188 }
189
190 pub fn modifiers(&self) -> Modifiers {
191 self.modifiers
192 }
193
194 pub fn matches(&self, key: &Key, modifiers: Modifiers) -> bool {
196 self.modifiers == modifiers && same_key(&self.key, key)
197 }
198}
199
200fn normalise(key: Key) -> Key {
201 match key {
202 Key::Character(text) => Key::Character(text.to_lowercase().into()),
203 key => key,
204 }
205}
206
207fn same_key(chord: &Key, pressed: &Key) -> bool {
208 match (chord, pressed) {
209 (Key::Character(a), Key::Character(b)) => a.as_str() == b.to_lowercase(),
210 (a, b) => a == b,
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum ParseChordError {
217 Empty,
218 MissingKey,
220 UnknownModifier(String),
221 UnknownKey(String),
222}
223
224impl fmt::Display for ParseChordError {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 match self {
227 Self::Empty => write!(f, "empty key chord"),
228 Self::MissingKey => write!(f, "key chord has an empty part"),
229 Self::UnknownModifier(name) => write!(f, "unknown modifier \"{name}\""),
230 Self::UnknownKey(name) => write!(f, "unknown key \"{name}\""),
231 }
232 }
233}
234
235impl std::error::Error for ParseChordError {}
236
237impl FromStr for Chord {
238 type Err = ParseChordError;
239
240 fn from_str(value: &str) -> Result<Self, Self::Err> {
241 let value = value.trim();
242 if value.is_empty() {
243 return Err(ParseChordError::Empty);
244 }
245
246 let (modifiers, key) = match value.strip_suffix("++") {
248 Some(rest) => (Some(rest), "+"),
249 None if value == "+" => (None, "+"),
250 None => match value.rsplit_once('+') {
251 Some((rest, key)) => (Some(rest), key),
252 None => (None, value),
253 },
254 };
255
256 let mut chord = Chord::new(parse_key(key.trim())?, Modifiers::empty());
257 let Some(modifiers) = modifiers else {
258 return Ok(chord);
259 };
260 for name in modifiers.split('+') {
261 chord = chord.with(parse_modifier(name.trim())?);
262 }
263 Ok(chord)
264 }
265}
266
267impl TryFrom<&str> for Chord {
268 type Error = ParseChordError;
269
270 fn try_from(value: &str) -> Result<Self, Self::Error> {
271 value.parse()
272 }
273}
274
275fn parse_key(name: &str) -> Result<Key, ParseChordError> {
276 if name.is_empty() {
277 return Err(ParseChordError::MissingKey);
278 }
279 if let Some((named, _)) = NAMED
280 .iter()
281 .find(|(_, names)| names.iter().any(|alias| alias.eq_ignore_ascii_case(name)))
282 {
283 return Ok(Key::Named(*named));
284 }
285 let mut chars = name.chars();
286 match (chars.next(), chars.next()) {
287 (Some(_), None) => Ok(Key::Character(name.into())),
288 _ => Err(ParseChordError::UnknownKey(name.to_owned())),
289 }
290}
291
292fn parse_modifier(name: &str) -> Result<Modifiers, ParseChordError> {
293 if name.is_empty() {
294 return Err(ParseChordError::MissingKey);
295 }
296 let modifiers = match name.to_ascii_lowercase().as_str() {
297 "ctrl" | "control" => Modifiers::CTRL,
298 "shift" => Modifiers::SHIFT,
299 "alt" | "option" | "opt" => Modifiers::ALT,
300 "cmd" | "command" | "super" | "meta" | "logo" | "win" => Modifiers::LOGO,
301 "mod" => Modifiers::COMMAND,
302 _ => return Err(ParseChordError::UnknownModifier(name.to_owned())),
303 };
304 Ok(modifiers)
305}
306
307impl fmt::Display for Chord {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 for (modifier, name) in [
310 (Modifiers::CTRL, "Ctrl"),
311 (Modifiers::ALT, "Alt"),
312 (Modifiers::SHIFT, "Shift"),
313 (Modifiers::LOGO, LOGO),
314 ] {
315 if self.modifiers.contains(modifier) {
316 write!(f, "{name}+")?;
317 }
318 }
319 match &self.key {
320 Key::Named(named) => match NAMED.iter().find(|(key, _)| key == named) {
321 Some((_, names)) => write!(f, "{}", names.first().copied().unwrap_or_default()),
322 None => write!(f, "{named:?}"),
323 },
324 Key::Character(text) => write!(f, "{}", text.to_uppercase()),
325 Key::Unidentified => write!(f, "Unidentified"),
326 }
327 }
328}
329
330pub trait Action: Copy + PartialEq + fmt::Debug + 'static {
332 const ALL: &'static [Self];
334
335 fn defaults() -> Keymap<Self>;
337
338 fn name(self) -> &'static str;
340
341 fn description(self) -> &'static str;
343}
344
345#[derive(Debug, Clone, PartialEq)]
348pub struct Keymap<A> {
349 bindings: Vec<(Chord, A)>,
350}
351
352impl<A> Default for Keymap<A> {
353 fn default() -> Self {
354 Self {
355 bindings: Vec::new(),
356 }
357 }
358}
359
360impl<A> Keymap<A> {
361 pub fn new() -> Self {
363 Self::default()
364 }
365
366 pub fn bind(mut self, chord: Chord, action: A) -> Self {
368 match self.bindings.iter_mut().find(|(bound, _)| *bound == chord) {
369 Some(binding) => binding.1 = action,
370 None => self.bindings.push((chord, action)),
371 }
372 self
373 }
374
375 pub fn unbind(mut self, chord: &Chord) -> Self {
377 self.bindings.retain(|(bound, _)| bound != chord);
378 self
379 }
380
381 pub fn clear(mut self) -> Self {
383 self.bindings.clear();
384 self
385 }
386
387 pub fn bindings(&self) -> impl Iterator<Item = (&Chord, &A)> {
389 self.bindings.iter().map(|(chord, action)| (chord, action))
390 }
391
392 pub fn len(&self) -> usize {
393 self.bindings.len()
394 }
395
396 pub fn is_empty(&self) -> bool {
397 self.bindings.is_empty()
398 }
399}
400
401impl<A: Clone> Keymap<A> {
402 pub fn resolve(&self, key: &Key, modifiers: Modifiers) -> Option<A> {
404 self.bindings
405 .iter()
406 .find(|(chord, _)| chord.matches(key, modifiers))
407 .map(|(_, action)| action.clone())
408 }
409
410 pub fn resolve_event(&self, event: &Event) -> Option<A> {
412 self.resolve(&event.key, event.modifiers)
413 }
414}
415
416impl<A: Clone + PartialEq> Keymap<A> {
417 pub fn chords(&self, action: &A) -> Vec<&Chord> {
419 self.bindings
420 .iter()
421 .filter(|(_, bound)| bound == action)
422 .map(|(chord, _)| chord)
423 .collect()
424 }
425
426 pub fn unbind_action(mut self, action: &A) -> Self {
428 self.bindings.retain(|(_, bound)| bound != action);
429 self
430 }
431
432 pub fn grouped(&self) -> Vec<(A, Vec<Chord>)> {
434 let mut groups: Vec<(A, Vec<Chord>)> = Vec::new();
435 for (chord, action) in &self.bindings {
436 match groups.iter_mut().find(|(bound, _)| bound == action) {
437 Some((_, chords)) => chords.push(chord.clone()),
438 None => groups.push((action.clone(), vec![chord.clone()])),
439 }
440 }
441 groups
442 }
443}
444
445impl<A: Action> Keymap<A> {
446 pub fn defaults() -> Self {
448 A::defaults()
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn chord(value: &str) -> Chord {
457 value.parse().unwrap()
458 }
459
460 fn character(value: &str) -> Key {
461 Key::Character(value.into())
462 }
463
464 #[test]
465 fn parses_named_keys_with_modifiers() {
466 let parsed = chord("Ctrl+Shift+Tab");
467 assert_eq!(parsed.key(), &Key::Named(Named::Tab));
468 assert_eq!(parsed.modifiers(), Modifiers::CTRL | Modifiers::SHIFT);
469 assert_eq!(chord("Escape"), Chord::named(Named::Escape));
470 assert_eq!(chord("ArrowRight"), Chord::named(Named::ArrowRight));
471 }
472
473 #[test]
474 fn parsing_ignores_case_and_accepts_aliases() {
475 assert_eq!(chord("ctrl+k"), chord("Ctrl+K"));
476 assert_eq!(chord("CONTROL + K"), chord("Ctrl+K"));
477 assert_eq!(chord("esc"), Chord::named(Named::Escape));
478 assert_eq!(chord("right"), Chord::named(Named::ArrowRight));
479 assert_eq!(chord("Option+PgDn"), Chord::named(Named::PageDown).alt());
480 assert_eq!(chord("Meta+S"), Chord::character('s').logo());
481 assert_eq!(chord("ContextMenu"), Chord::named(Named::ContextMenu));
482 assert_eq!(chord("Menu").to_string(), "Menu");
483 }
484
485 #[test]
486 fn mod_is_the_platform_command_key() {
487 let parsed = chord("Mod+K");
488 assert_eq!(parsed.modifiers(), Modifiers::COMMAND);
489 if cfg!(target_os = "macos") {
490 assert_eq!(parsed, chord("Cmd+K"));
491 } else {
492 assert_eq!(parsed, chord("Ctrl+K"));
493 }
494 }
495
496 #[test]
497 fn plus_can_be_the_key() {
498 assert_eq!(chord("+").key(), &character("+"));
499 let zoom = chord("Ctrl++");
500 assert_eq!(zoom.key(), &character("+"));
501 assert_eq!(zoom.modifiers(), Modifiers::CTRL);
502 }
503
504 #[test]
505 fn rejects_invalid_chords() {
506 assert_eq!("".parse::<Chord>(), Err(ParseChordError::Empty));
507 assert_eq!(" ".parse::<Chord>(), Err(ParseChordError::Empty));
508 assert_eq!("Ctrl+".parse::<Chord>(), Err(ParseChordError::MissingKey));
509 assert_eq!("Ctrl++K".parse::<Chord>(), Err(ParseChordError::MissingKey));
510 assert_eq!(
511 "Hyper+K".parse::<Chord>(),
512 Err(ParseChordError::UnknownModifier("Hyper".into()))
513 );
514 assert_eq!(
515 "Ctrl+Banana".parse::<Chord>(),
516 Err(ParseChordError::UnknownKey("Banana".into()))
517 );
518 }
519
520 #[test]
521 fn displays_in_canonical_form() {
522 assert_eq!(chord("shift+ctrl+tab").to_string(), "Ctrl+Shift+Tab");
523 assert_eq!(chord("ctrl+k").to_string(), "Ctrl+K");
524 assert_eq!(chord("esc").to_string(), "Escape");
525 assert_eq!(chord("alt+up").to_string(), "Alt+ArrowUp");
526 assert_eq!(chord("Ctrl++").to_string(), "Ctrl++");
527 }
528
529 #[test]
530 fn display_round_trips() {
531 for value in ["Ctrl+Alt+Shift+Cmd+F5", "Space", "Mod+/", "Shift+End", "+"] {
532 let parsed = chord(value);
533 assert_eq!(chord(&parsed.to_string()), parsed, "{value}");
534 }
535 }
536
537 #[test]
538 fn matches_exact_modifiers_and_any_case() {
539 let save = chord("Ctrl+S");
540 assert!(save.matches(&character("s"), Modifiers::CTRL));
541 assert!(save.matches(&character("S"), Modifiers::CTRL));
542 assert!(!save.matches(&character("s"), Modifiers::empty()));
543 assert!(!save.matches(&character("s"), Modifiers::CTRL | Modifiers::SHIFT));
544 assert!(!save.matches(&Key::Named(Named::Tab), Modifiers::CTRL));
545 assert!(chord("Tab").matches(&Key::Named(Named::Tab), Modifiers::empty()));
546 }
547
548 #[derive(Debug, Clone, Copy, PartialEq)]
549 enum Test {
550 Open,
551 Close,
552 }
553
554 fn keymap() -> Keymap<Test> {
555 Keymap::new()
556 .bind(chord("Ctrl+O"), Test::Open)
557 .bind(chord("Enter"), Test::Open)
558 .bind(chord("Escape"), Test::Close)
559 }
560
561 #[test]
562 fn resolves_bound_chords_only() {
563 let keymap = keymap();
564 assert_eq!(
565 keymap.resolve(&character("o"), Modifiers::CTRL),
566 Some(Test::Open)
567 );
568 assert_eq!(
569 keymap.resolve(&Key::Named(Named::Escape), Modifiers::empty()),
570 Some(Test::Close)
571 );
572 assert_eq!(keymap.resolve(&character("o"), Modifiers::empty()), None);
573 let event = Event {
574 key: Key::Named(Named::Enter),
575 modifiers: Modifiers::empty(),
576 };
577 assert_eq!(keymap.resolve_event(&event), Some(Test::Open));
578 }
579
580 #[test]
581 fn bind_replaces_an_existing_chord() {
582 let keymap = keymap().bind(chord("Enter"), Test::Close);
583 assert_eq!(keymap.len(), 3);
584 assert_eq!(
585 keymap.resolve(&Key::Named(Named::Enter), Modifiers::empty()),
586 Some(Test::Close)
587 );
588 }
589
590 #[test]
591 fn unbind_and_clear() {
592 let keymap = keymap().unbind(&chord("Escape"));
593 assert_eq!(
594 keymap.resolve(&Key::Named(Named::Escape), Modifiers::empty()),
595 None
596 );
597 assert_eq!(keymap.len(), 2);
598 let unchanged = keymap.clone().unbind(&chord("F1"));
599 assert_eq!(unchanged, keymap);
600 assert!(keymap.clear().is_empty());
601 }
602
603 #[test]
604 fn chords_grouped_and_unbound_by_action() {
605 let keymap = keymap();
606 assert_eq!(keymap.chords(&Test::Open).len(), 2);
607 let grouped = keymap.grouped();
608 assert_eq!(grouped.len(), 2);
609 assert_eq!(grouped[0].0, Test::Open);
610 assert_eq!(grouped[0].1, vec![chord("Ctrl+O"), chord("Enter")]);
611 assert_eq!(grouped[1], (Test::Close, vec![chord("Escape")]));
612 let closed = keymap.unbind_action(&Test::Open);
613 assert_eq!(closed.len(), 1);
614 }
615}