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