1#![allow(clippy::module_name_repetitions)]
2
3use std::error::Error;
4use std::str::CharIndices;
5use std::string::ToString;
6use std::{fmt::Display, iter::Peekable};
7
8use ahash::HashMapExt;
9use serde::{Deserialize, Serialize};
10use tuirealm::event as tuievents;
11
12mod conflict;
13pub use conflict::KeyConflictError;
14use conflict::{CheckConflict, KeyHashMap, KeyHashMapOwned, KeyPath};
15
16use crate::once_chain;
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct KeysCheckError {
20 pub errored_keys: Vec<KeyConflictError>,
21}
22
23impl Display for KeysCheckError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 writeln!(
26 f,
27 "There are {} Key Conflict Errors: [",
28 self.errored_keys.len()
29 )?;
30
31 for err in &self.errored_keys {
32 writeln!(f, " {err},")?;
33 }
34
35 write!(f, "]")
36 }
37}
38
39impl Error for KeysCheckError {}
40
41impl From<Vec<KeyConflictError>> for KeysCheckError {
42 fn from(value: Vec<KeyConflictError>) -> Self {
43 Self {
44 errored_keys: value,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
50#[serde(default)] pub struct Keys {
52 pub escape: KeyBinding,
57 pub quit: KeyBinding,
61
62 #[serde(rename = "view")]
64 pub select_view_keys: KeysSelectView,
65 #[serde(rename = "navigation")]
66 pub navigation_keys: KeysNavigation,
67 #[serde(rename = "global_player")]
68 pub player_keys: KeysPlayer,
69 #[serde(rename = "global_lyric")]
70 pub lyric_keys: KeysLyric,
71 #[serde(rename = "library")]
72 pub library_keys: KeysLibrary,
73 #[serde(rename = "playlist")]
74 pub playlist_keys: KeysPlaylist,
75 #[serde(rename = "database")]
76 pub database_keys: KeysDatabase,
77 #[serde(rename = "podcast")]
78 pub podcast_keys: KeysPodcast,
79 #[serde(rename = "adjust_cover_art")]
80 pub move_cover_art_keys: KeysMoveCoverArt,
81 #[serde(rename = "config")]
82 pub config_keys: KeysConfigEditor,
83}
84
85impl Keys {
86 pub fn check_keys(&self) -> Result<(), KeysCheckError> {
88 let mut key_path = KeyPath::new_with_toplevel("keys");
89 let mut global_keys = KeyHashMapOwned::new();
90
91 self.check_conflict(&mut key_path, &mut global_keys)
92 .map_err(KeysCheckError::from)
93 }
94}
95
96impl Default for Keys {
97 fn default() -> Self {
98 Self {
99 escape: tuievents::Key::Esc.into(),
100 quit: tuievents::Key::Char('q').into(),
101 select_view_keys: KeysSelectView::default(),
102 navigation_keys: KeysNavigation::default(),
103 player_keys: KeysPlayer::default(),
104 lyric_keys: KeysLyric::default(),
105 library_keys: KeysLibrary::default(),
106 playlist_keys: KeysPlaylist::default(),
107 database_keys: KeysDatabase::default(),
108 podcast_keys: KeysPodcast::default(),
109 move_cover_art_keys: KeysMoveCoverArt::default(),
110 config_keys: KeysConfigEditor::default(),
111 }
112 }
113}
114
115impl CheckConflict for Keys {
116 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
117 once_chain! {
118 (&self.escape, "escape"),
119 (&self.quit, "quit"),
120 }
121 }
122
123 fn check_conflict(
124 &self,
125 key_path: &mut KeyPath,
126 global_keys: &mut KeyHashMapOwned,
127 ) -> Result<(), Vec<KeyConflictError>> {
128 let mut conflicts: Vec<KeyConflictError> = Vec::new();
129 let mut current_keys = KeyHashMap::new();
130
131 for (key, path) in self.iter() {
133 if let Some(existing_path) = global_keys.get(key) {
135 conflicts.push(KeyConflictError {
136 key_path_first: existing_path.clone(),
137 key_path_second: key_path.join_with_field(path),
138 key: key.clone(),
139 });
140 continue;
141 }
142
143 if let Some(existing_path) = current_keys.get(key) {
144 conflicts.push(KeyConflictError {
145 key_path_first: key_path.join_with_field(existing_path),
146 key_path_second: key_path.join_with_field(path),
147 key: key.clone(),
148 });
149 continue;
150 }
151
152 global_keys.insert(key.clone(), key_path.join_with_field(path));
153 current_keys.insert(key, path);
154 }
155
156 let init_len = global_keys.len(); key_path.push("config");
160 if let Err(new) = self.config_keys.check_conflict(key_path, global_keys) {
161 conflicts.extend(new);
162 }
163 key_path.pop();
164 assert_eq!(global_keys.len(), init_len); key_path.push("view");
175 if let Err(new) = self.select_view_keys.check_conflict(key_path, global_keys) {
176 conflicts.extend(new);
177 }
178 key_path.pop();
179 key_path.push("global_player");
180 if let Err(new) = self.player_keys.check_conflict(key_path, global_keys) {
181 conflicts.extend(new);
182 }
183 key_path.pop();
184 key_path.push("global_lyric");
185 if let Err(new) = self.lyric_keys.check_conflict(key_path, global_keys) {
186 conflicts.extend(new);
187 }
188 key_path.pop();
189 key_path.push("adjust_cover_art");
190 if let Err(new) = self
191 .move_cover_art_keys
192 .check_conflict(key_path, global_keys)
193 {
194 conflicts.extend(new);
195 }
196 key_path.pop();
197
198 key_path.push("navigation");
201 if let Err(new) = self.navigation_keys.check_conflict(key_path, global_keys) {
202 conflicts.extend(new);
203 }
204 key_path.pop();
205 key_path.push("library");
206 if let Err(new) = self.library_keys.check_conflict(key_path, global_keys) {
207 conflicts.extend(new);
208 }
209 key_path.pop();
210 key_path.push("playlist");
211 if let Err(new) = self.playlist_keys.check_conflict(key_path, global_keys) {
212 conflicts.extend(new);
213 }
214 key_path.pop();
215 key_path.push("podcast");
216 if let Err(new) = self.podcast_keys.check_conflict(key_path, global_keys) {
217 conflicts.extend(new);
218 }
219 key_path.pop();
220
221 if !conflicts.is_empty() {
223 return Err(conflicts);
224 }
225
226 Ok(())
227 }
228}
229
230#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
232#[serde(default)] pub struct KeysSelectView {
234 pub view_library: KeyBinding,
236 pub view_database: KeyBinding,
238 pub view_podcasts: KeyBinding,
240
241 pub open_config: KeyBinding,
243 pub open_help: KeyBinding,
245}
246
247impl Default for KeysSelectView {
248 fn default() -> Self {
249 Self {
250 view_library: tuievents::Key::Char('1').into(),
251 view_database: tuievents::Key::Char('2').into(),
252 view_podcasts: tuievents::Key::Char('3').into(),
253 open_config: tuievents::KeyEvent::new(
254 tuievents::Key::Char('C'),
255 tuievents::KeyModifiers::SHIFT,
256 )
257 .into(),
258 open_help: tuievents::KeyEvent::new(
259 tuievents::Key::Char('h'),
260 tuievents::KeyModifiers::CONTROL,
261 )
262 .into(),
263 }
264 }
265}
266
267impl CheckConflict for KeysSelectView {
268 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
269 once_chain! {
270 (&self.view_library, "view_library"),
271 (&self.view_database, "view_database"),
272 (&self.view_podcasts, "view_podcasts"),
273
274 (&self.open_config, "open_config"),
275 (&self.open_help, "open_help")
276 }
277 }
278
279 fn check_conflict(
280 &self,
281 key_path: &mut KeyPath,
282 global_keys: &mut KeyHashMapOwned,
283 ) -> Result<(), Vec<KeyConflictError>> {
284 let mut conflicts: Vec<KeyConflictError> = Vec::new();
285 let mut current_keys = KeyHashMap::new();
286
287 for (key, path) in self.iter() {
288 if let Some(existing_path) = global_keys.get(key) {
290 conflicts.push(KeyConflictError {
291 key_path_first: existing_path.clone(),
292 key_path_second: key_path.join_with_field(path),
293 key: key.clone(),
294 });
295 continue;
296 }
297
298 if let Some(existing_path) = current_keys.get(key) {
299 conflicts.push(KeyConflictError {
300 key_path_first: key_path.join_with_field(existing_path),
301 key_path_second: key_path.join_with_field(path),
302 key: key.clone(),
303 });
304 continue;
305 }
306
307 global_keys.insert(key.clone(), key_path.join_with_field(path));
308 current_keys.insert(key, path);
309 }
310
311 if !conflicts.is_empty() {
312 return Err(conflicts);
313 }
314
315 Ok(())
316 }
317}
318
319#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
321#[serde(default)] pub struct KeysPlayer {
323 pub toggle_pause: KeyBinding,
327 pub next_track: KeyBinding,
331 pub previous_track: KeyBinding,
335 pub volume_up: KeyBinding,
339 pub volume_down: KeyBinding,
343 pub seek_forward: KeyBinding,
347 pub seek_backward: KeyBinding,
351 pub speed_up: KeyBinding,
355 pub speed_down: KeyBinding,
359 pub toggle_prefetch: KeyBinding,
364
365 pub save_playlist: KeyBinding,
367}
368
369impl Default for KeysPlayer {
370 fn default() -> Self {
371 Self {
372 toggle_pause: tuievents::Key::Char(' ').into(),
373 next_track: tuievents::Key::Char('n').into(),
374 previous_track: tuievents::KeyEvent::new(
375 tuievents::Key::Char('N'),
376 tuievents::KeyModifiers::SHIFT,
377 )
378 .into(),
379 volume_up: tuievents::Key::Char('+').into(),
380 volume_down: tuievents::Key::Char('-').into(),
381 seek_forward: tuievents::Key::Char('f').into(),
382 seek_backward: tuievents::Key::Char('b').into(),
383 speed_up: tuievents::KeyEvent::new(
384 tuievents::Key::Char('f'),
385 tuievents::KeyModifiers::CONTROL,
386 )
387 .into(),
388 speed_down: tuievents::KeyEvent::new(
389 tuievents::Key::Char('b'),
390 tuievents::KeyModifiers::CONTROL,
391 )
392 .into(),
393 toggle_prefetch: tuievents::KeyEvent::new(
394 tuievents::Key::Char('g'),
395 tuievents::KeyModifiers::CONTROL,
396 )
397 .into(),
398 save_playlist: tuievents::KeyEvent::new(
399 tuievents::Key::Char('s'),
400 tuievents::KeyModifiers::CONTROL,
401 )
402 .into(),
403 }
404 }
405}
406
407impl CheckConflict for KeysPlayer {
408 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
409 once_chain! {
410 (&self.toggle_pause, "toggle_pause"),
411 (&self.next_track, "next_track"),
412 (&self.previous_track, "previous_track"),
413 (&self.volume_up, "volume_up"),
414 (&self.volume_down, "volume_down"),
415 (&self.seek_forward, "seek_forward"),
416 (&self.seek_backward, "seek_backward"),
417 (&self.speed_up, "speed_up"),
418 (&self.speed_down, "speed_down"),
419 (&self.toggle_prefetch, "toggle_prefetch"),
420
421 (&self.save_playlist, "save_playlist"),
422 }
423 }
424
425 fn check_conflict(
426 &self,
427 key_path: &mut KeyPath,
428 global_keys: &mut KeyHashMapOwned,
429 ) -> Result<(), Vec<KeyConflictError>> {
430 let mut conflicts: Vec<KeyConflictError> = Vec::new();
431 let mut current_keys = KeyHashMap::new();
432
433 for (key, path) in self.iter() {
434 if let Some(existing_path) = global_keys.get(key) {
436 conflicts.push(KeyConflictError {
437 key_path_first: existing_path.clone(),
438 key_path_second: key_path.join_with_field(path),
439 key: key.clone(),
440 });
441 continue;
442 }
443
444 if let Some(existing_path) = current_keys.get(key) {
445 conflicts.push(KeyConflictError {
446 key_path_first: key_path.join_with_field(existing_path),
447 key_path_second: key_path.join_with_field(path),
448 key: key.clone(),
449 });
450 continue;
451 }
452
453 global_keys.insert(key.clone(), key_path.join_with_field(path));
454 current_keys.insert(key, path);
455 }
456
457 if !conflicts.is_empty() {
458 return Err(conflicts);
459 }
460
461 Ok(())
462 }
463}
464
465#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
467#[serde(default)] pub struct KeysLyric {
469 pub adjust_offset_forwards: KeyBinding,
473 pub adjust_offset_backwards: KeyBinding,
477 pub cycle_frames: KeyBinding,
481}
482
483impl Default for KeysLyric {
484 fn default() -> Self {
485 Self {
486 adjust_offset_forwards: tuievents::KeyEvent::new(
487 tuievents::Key::Char('F'),
488 tuievents::KeyModifiers::SHIFT,
489 )
490 .into(),
491 adjust_offset_backwards: tuievents::KeyEvent::new(
492 tuievents::Key::Char('B'),
493 tuievents::KeyModifiers::SHIFT,
494 )
495 .into(),
496 cycle_frames: tuievents::KeyEvent::new(
497 tuievents::Key::Char('T'),
498 tuievents::KeyModifiers::SHIFT,
499 )
500 .into(),
501 }
502 }
503}
504
505impl CheckConflict for KeysLyric {
506 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
507 once_chain! {
508 (&self.adjust_offset_forwards, "adjust_offset_forwards"),
509 (&self.adjust_offset_backwards, "adjust_offset_backwards"),
510 (&self.cycle_frames, "cycle_frames"),
511 }
512 }
513
514 fn check_conflict(
515 &self,
516 key_path: &mut KeyPath,
517 global_keys: &mut KeyHashMapOwned,
518 ) -> Result<(), Vec<KeyConflictError>> {
519 let mut conflicts: Vec<KeyConflictError> = Vec::new();
520 let mut current_keys = KeyHashMap::new();
521
522 for (key, path) in self.iter() {
523 if let Some(existing_path) = global_keys.get(key) {
525 conflicts.push(KeyConflictError {
526 key_path_first: existing_path.clone(),
527 key_path_second: key_path.join_with_field(path),
528 key: key.clone(),
529 });
530 continue;
531 }
532
533 if let Some(existing_path) = current_keys.get(key) {
534 conflicts.push(KeyConflictError {
535 key_path_first: key_path.join_with_field(existing_path),
536 key_path_second: key_path.join_with_field(path),
537 key: key.clone(),
538 });
539 continue;
540 }
541
542 global_keys.insert(key.clone(), key_path.join_with_field(path));
543 current_keys.insert(key, path);
544 }
545
546 if !conflicts.is_empty() {
547 return Err(conflicts);
548 }
549
550 Ok(())
551 }
552}
553
554#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
556#[serde(default)] pub struct KeysNavigation {
558 pub up: KeyBinding,
561 pub down: KeyBinding,
563 pub left: KeyBinding,
565 pub right: KeyBinding,
567 pub goto_top: KeyBinding,
569 pub goto_bottom: KeyBinding,
571}
572
573impl Default for KeysNavigation {
574 fn default() -> Self {
575 Self {
577 up: tuievents::Key::Char('k').into(),
578 down: tuievents::Key::Char('j').into(),
579 left: tuievents::Key::Char('h').into(),
580 right: tuievents::Key::Char('l').into(),
581 goto_top: tuievents::Key::Char('g').into(),
582 goto_bottom: tuievents::KeyEvent::new(
583 tuievents::Key::Char('G'),
584 tuievents::KeyModifiers::SHIFT,
585 )
586 .into(),
587 }
588 }
589}
590
591impl CheckConflict for KeysNavigation {
592 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
593 once_chain! {
594 (&self.up, "up"),
595 (&self.down, "down"),
596 (&self.left, "left"),
597 (&self.right, "right"),
598 (&self.goto_top, "goto_top"),
599 (&self.goto_bottom, "goto_bottom"),
600 }
601 }
602
603 fn check_conflict(
604 &self,
605 key_path: &mut KeyPath,
606 global_keys: &mut KeyHashMapOwned,
607 ) -> Result<(), Vec<KeyConflictError>> {
608 let mut conflicts: Vec<KeyConflictError> = Vec::new();
609 let mut current_keys = KeyHashMap::new();
610
611 for (key, path) in self.iter() {
612 if let Some(existing_path) = global_keys.get(key) {
614 conflicts.push(KeyConflictError {
615 key_path_first: existing_path.clone(),
616 key_path_second: key_path.join_with_field(path),
617 key: key.clone(),
618 });
619 continue;
620 }
621
622 if let Some(existing_path) = current_keys.get(key) {
623 conflicts.push(KeyConflictError {
624 key_path_first: key_path.join_with_field(existing_path),
625 key_path_second: key_path.join_with_field(path),
626 key: key.clone(),
627 });
628 continue;
629 }
630
631 current_keys.insert(key, path);
632 }
633
634 if !conflicts.is_empty() {
635 return Err(conflicts);
636 }
637
638 Ok(())
639 }
640}
641
642#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
643#[serde(default)] pub struct KeysLibrary {
645 pub load_track: KeyBinding,
647 pub load_dir: KeyBinding,
649 pub delete: KeyBinding,
651 pub yank: KeyBinding,
653 pub paste: KeyBinding,
655 pub cycle_root: KeyBinding,
657 pub add_root: KeyBinding,
659 pub remove_root: KeyBinding,
661
662 pub search: KeyBinding,
664 pub youtube_search: KeyBinding,
666 pub open_tag_editor: KeyBinding,
668}
669
670impl Default for KeysLibrary {
671 fn default() -> Self {
672 Self {
673 load_track: tuievents::Key::Char('l').into(),
674 load_dir: tuievents::KeyEvent::new(
675 tuievents::Key::Char('L'),
676 tuievents::KeyModifiers::SHIFT,
677 )
678 .into(),
679 delete: tuievents::Key::Char('d').into(),
680 yank: tuievents::Key::Char('y').into(),
681 paste: tuievents::Key::Char('p').into(),
682 cycle_root: tuievents::Key::Char('o').into(),
683 add_root: tuievents::Key::Char('a').into(),
684 remove_root: tuievents::KeyEvent::new(
685 tuievents::Key::Char('A'),
686 tuievents::KeyModifiers::SHIFT,
687 )
688 .into(),
689 search: tuievents::Key::Char('/').into(),
690 youtube_search: tuievents::Key::Char('s').into(),
691 open_tag_editor: tuievents::Key::Char('t').into(),
692 }
693 }
694}
695
696impl CheckConflict for KeysLibrary {
697 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
698 once_chain! {
699 (&self.load_track, "load_track"),
700 (&self.load_dir, "load_dir"),
701 (&self.delete, "delete"),
702 (&self.yank, "yank"),
703 (&self.paste, "paste"),
704 (&self.cycle_root, "cycle_root"),
705 (&self.add_root, "add_root"),
706 (&self.remove_root, "remove_root"),
707
708 (&self.search, "search"),
709 (&self.youtube_search, "youtube_search"),
710 (&self.open_tag_editor, "open_tag_editor"),
711 }
712 }
713
714 fn check_conflict(
715 &self,
716 key_path: &mut KeyPath,
717 global_keys: &mut KeyHashMapOwned,
718 ) -> Result<(), Vec<KeyConflictError>> {
719 let mut conflicts: Vec<KeyConflictError> = Vec::new();
720 let mut current_keys = KeyHashMap::new();
721
722 for (key, path) in self.iter() {
723 if let Some(existing_path) = global_keys.get(key) {
725 conflicts.push(KeyConflictError {
726 key_path_first: existing_path.clone(),
727 key_path_second: key_path.join_with_field(path),
728 key: key.clone(),
729 });
730 continue;
731 }
732
733 if let Some(existing_path) = current_keys.get(key) {
734 conflicts.push(KeyConflictError {
735 key_path_first: key_path.join_with_field(existing_path),
736 key_path_second: key_path.join_with_field(path),
737 key: key.clone(),
738 });
739 continue;
740 }
741
742 current_keys.insert(key, path);
743 }
744
745 if !conflicts.is_empty() {
746 return Err(conflicts);
747 }
748
749 Ok(())
750 }
751}
752
753#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
754#[serde(default)] pub struct KeysPlaylist {
756 pub delete: KeyBinding,
758 pub delete_all: KeyBinding,
760 pub shuffle: KeyBinding,
762 pub cycle_loop_mode: KeyBinding,
764 pub play_selected: KeyBinding,
766 pub search: KeyBinding,
768 pub swap_up: KeyBinding,
770 pub swap_down: KeyBinding,
772
773 pub add_random_songs: KeyBinding,
777 pub add_random_album: KeyBinding,
782}
783
784impl Default for KeysPlaylist {
785 fn default() -> Self {
786 Self {
787 delete: tuievents::Key::Char('d').into(),
788 delete_all: tuievents::KeyEvent::new(
789 tuievents::Key::Char('D'),
790 tuievents::KeyModifiers::SHIFT,
791 )
792 .into(),
793 shuffle: tuievents::Key::Char('r').into(),
794 cycle_loop_mode: tuievents::Key::Char('m').into(),
795 play_selected: tuievents::Key::Char('l').into(),
796 search: tuievents::Key::Char('/').into(),
797 swap_up: tuievents::KeyEvent::new(
798 tuievents::Key::Char('K'),
799 tuievents::KeyModifiers::SHIFT,
800 )
801 .into(),
802 swap_down: tuievents::KeyEvent::new(
803 tuievents::Key::Char('J'),
804 tuievents::KeyModifiers::SHIFT,
805 )
806 .into(),
807 add_random_songs: tuievents::Key::Char('s').into(),
808 add_random_album: tuievents::KeyEvent::new(
809 tuievents::Key::Char('S'),
810 tuievents::KeyModifiers::SHIFT,
811 )
812 .into(),
813 }
814 }
815}
816
817impl CheckConflict for KeysPlaylist {
818 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
819 once_chain! {
820 (&self.delete, "delete"),
821 (&self.delete_all, "delete_all"),
822 (&self.shuffle, "shuffle"),
823 (&self.cycle_loop_mode, "cycle_loop_mode"),
824 (&self.play_selected, "play_selected"),
825 (&self.search, "search"),
826 (&self.swap_up, "swap_up"),
827 (&self.swap_down, "swap_down"),
828
829 (&self.add_random_songs, "add_random_songs"),
830 (&self.add_random_album, "add_random_album"),
831 }
832 }
833
834 fn check_conflict(
835 &self,
836 key_path: &mut KeyPath,
837 global_keys: &mut KeyHashMapOwned,
838 ) -> Result<(), Vec<KeyConflictError>> {
839 let mut conflicts: Vec<KeyConflictError> = Vec::new();
840 let mut current_keys = KeyHashMap::new();
841
842 for (key, path) in self.iter() {
843 if let Some(existing_path) = global_keys.get(key) {
845 conflicts.push(KeyConflictError {
846 key_path_first: existing_path.clone(),
847 key_path_second: key_path.join_with_field(path),
848 key: key.clone(),
849 });
850 continue;
851 }
852
853 if let Some(existing_path) = current_keys.get(key) {
854 conflicts.push(KeyConflictError {
855 key_path_first: key_path.join_with_field(existing_path),
856 key_path_second: key_path.join_with_field(path),
857 key: key.clone(),
858 });
859 continue;
860 }
861
862 current_keys.insert(key, path);
863 }
864
865 if !conflicts.is_empty() {
866 return Err(conflicts);
867 }
868
869 Ok(())
870 }
871}
872
873#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
874#[serde(default)] pub struct KeysPodcast {
876 pub search: KeyBinding,
878 pub mark_played: KeyBinding,
880 pub mark_all_played: KeyBinding,
882 pub refresh_feed: KeyBinding,
884 pub refresh_all_feeds: KeyBinding,
886 pub download_episode: KeyBinding,
888 pub delete_local_episode: KeyBinding,
890 pub delete_feed: KeyBinding,
892 pub delete_all_feeds: KeyBinding,
894}
895
896impl Default for KeysPodcast {
897 fn default() -> Self {
898 Self {
899 search: tuievents::Key::Char('s').into(),
900 mark_played: tuievents::Key::Char('m').into(),
901 mark_all_played: tuievents::KeyEvent::new(
902 tuievents::Key::Char('M'),
903 tuievents::KeyModifiers::SHIFT,
904 )
905 .into(),
906 refresh_feed: tuievents::Key::Char('r').into(),
907 refresh_all_feeds: tuievents::KeyEvent::new(
908 tuievents::Key::Char('R'),
909 tuievents::KeyModifiers::SHIFT,
910 )
911 .into(),
912 download_episode: tuievents::Key::Char('d').into(),
913 delete_local_episode: tuievents::KeyEvent::new(
914 tuievents::Key::Char('D'),
915 tuievents::KeyModifiers::SHIFT,
916 )
917 .into(),
918 delete_feed: tuievents::Key::Char('x').into(),
919 delete_all_feeds: tuievents::KeyEvent::new(
920 tuievents::Key::Char('X'),
921 tuievents::KeyModifiers::SHIFT,
922 )
923 .into(),
924 }
925 }
926}
927
928impl CheckConflict for KeysPodcast {
929 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
930 once_chain! {
931 (&self.search, "search"),
932 (&self.mark_played, "mark_played"),
933 (&self.mark_all_played, "mark_all_played"),
934 (&self.refresh_feed, "refresh_feed"),
935 (&self.refresh_all_feeds, "refresh_all_feeds"),
936 (&self.download_episode, "download_episode"),
937 (&self.delete_local_episode, "delete_local_episode"),
938 (&self.delete_feed, "delete_feed"),
939 (&self.delete_all_feeds, "delete_all_feeds"),
940 }
941 }
942
943 fn check_conflict(
944 &self,
945 key_path: &mut KeyPath,
946 global_keys: &mut KeyHashMapOwned,
947 ) -> Result<(), Vec<KeyConflictError>> {
948 let mut conflicts: Vec<KeyConflictError> = Vec::new();
949 let mut current_keys = KeyHashMap::new();
950
951 for (key, path) in self.iter() {
952 if let Some(existing_path) = global_keys.get(key) {
954 conflicts.push(KeyConflictError {
955 key_path_first: existing_path.clone(),
956 key_path_second: key_path.join_with_field(path),
957 key: key.clone(),
958 });
959 continue;
960 }
961
962 if let Some(existing_path) = current_keys.get(key) {
963 conflicts.push(KeyConflictError {
964 key_path_first: key_path.join_with_field(existing_path),
965 key_path_second: key_path.join_with_field(path),
966 key: key.clone(),
967 });
968 continue;
969 }
970
971 current_keys.insert(key, path);
972 }
973
974 if !conflicts.is_empty() {
975 return Err(conflicts);
976 }
977
978 Ok(())
979 }
980}
981
982#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
984#[serde(default)] pub struct KeysMoveCoverArt {
986 pub move_left: KeyBinding,
988 pub move_right: KeyBinding,
990 pub move_up: KeyBinding,
992 pub move_down: KeyBinding,
994
995 pub increase_size: KeyBinding,
997 pub decrease_size: KeyBinding,
999
1000 pub toggle_hide: KeyBinding,
1002}
1003
1004impl Default for KeysMoveCoverArt {
1005 fn default() -> Self {
1006 Self {
1007 move_left: tuievents::KeyEvent::new(
1008 tuievents::Key::Left,
1009 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1010 )
1011 .into(),
1012 move_right: tuievents::KeyEvent::new(
1013 tuievents::Key::Right,
1014 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1015 )
1016 .into(),
1017 move_up: tuievents::KeyEvent::new(
1018 tuievents::Key::Up,
1019 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1020 )
1021 .into(),
1022 move_down: tuievents::KeyEvent::new(
1023 tuievents::Key::Down,
1024 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1025 )
1026 .into(),
1027 increase_size: tuievents::KeyEvent::new(
1028 tuievents::Key::PageUp,
1029 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1030 )
1031 .into(),
1032 decrease_size: tuievents::KeyEvent::new(
1033 tuievents::Key::PageDown,
1034 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1035 )
1036 .into(),
1037 toggle_hide: tuievents::KeyEvent::new(
1038 tuievents::Key::End,
1039 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
1040 )
1041 .into(),
1042 }
1043 }
1044}
1045
1046impl CheckConflict for KeysMoveCoverArt {
1047 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
1048 once_chain! {
1049 (&self.move_left, "move_left"),
1050 (&self.move_right, "move_right"),
1051 (&self.move_up, "move_up"),
1052 (&self.move_down, "move_down"),
1053
1054 (&self.increase_size, "increase_size"),
1055 (&self.decrease_size, "decrease_size"),
1056
1057 (&self.toggle_hide, "toggle_hide"),
1058 }
1059 }
1060
1061 fn check_conflict(
1062 &self,
1063 key_path: &mut KeyPath,
1064 global_keys: &mut KeyHashMapOwned,
1065 ) -> Result<(), Vec<KeyConflictError>> {
1066 let mut conflicts: Vec<KeyConflictError> = Vec::new();
1067 let mut current_keys = KeyHashMap::new();
1068
1069 for (key, path) in self.iter() {
1070 if let Some(existing_path) = global_keys.get(key) {
1072 conflicts.push(KeyConflictError {
1073 key_path_first: existing_path.clone(),
1074 key_path_second: key_path.join_with_field(path),
1075 key: key.clone(),
1076 });
1077 continue;
1078 }
1079
1080 if let Some(existing_path) = current_keys.get(key) {
1081 conflicts.push(KeyConflictError {
1082 key_path_first: key_path.join_with_field(existing_path),
1083 key_path_second: key_path.join_with_field(path),
1084 key: key.clone(),
1085 });
1086 continue;
1087 }
1088
1089 global_keys.insert(key.clone(), key_path.join_with_field(path));
1090 current_keys.insert(key, path);
1091 }
1092
1093 if !conflicts.is_empty() {
1094 return Err(conflicts);
1095 }
1096
1097 Ok(())
1098 }
1099}
1100
1101#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1103#[serde(default)] pub struct KeysConfigEditor {
1105 pub save: KeyBinding,
1107}
1108
1109impl Default for KeysConfigEditor {
1110 fn default() -> Self {
1111 Self {
1112 save: tuievents::KeyEvent::new(
1113 tuievents::Key::Char('s'),
1114 tuievents::KeyModifiers::CONTROL,
1115 )
1116 .into(),
1117 }
1118 }
1119}
1120
1121impl CheckConflict for KeysConfigEditor {
1122 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
1123 once_chain! {
1124 (&self.save, "save"),
1125 }
1126 }
1127
1128 fn check_conflict(
1129 &self,
1130 key_path: &mut KeyPath,
1131 global_keys: &mut KeyHashMapOwned,
1132 ) -> Result<(), Vec<KeyConflictError>> {
1133 let mut conflicts: Vec<KeyConflictError> = Vec::new();
1134 let mut current_keys = KeyHashMap::new();
1135
1136 for (key, path) in self.iter() {
1137 if let Some(existing_path) = global_keys.get(key) {
1139 conflicts.push(KeyConflictError {
1140 key_path_first: existing_path.clone(),
1141 key_path_second: key_path.join_with_field(path),
1142 key: key.clone(),
1143 });
1144 continue;
1145 }
1146
1147 if let Some(existing_path) = current_keys.get(key) {
1148 conflicts.push(KeyConflictError {
1149 key_path_first: key_path.join_with_field(existing_path),
1150 key_path_second: key_path.join_with_field(path),
1151 key: key.clone(),
1152 });
1153 continue;
1154 }
1155
1156 current_keys.insert(key, path);
1157 }
1158
1159 if !conflicts.is_empty() {
1160 return Err(conflicts);
1161 }
1162
1163 Ok(())
1164 }
1165}
1166
1167#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1169#[serde(default)] pub struct KeysDatabase {
1171 pub add_selected: KeyBinding,
1173 pub add_all: KeyBinding,
1175}
1176
1177impl Default for KeysDatabase {
1178 fn default() -> Self {
1179 Self {
1180 add_selected: tuievents::Key::Char('l').into(),
1181 add_all: tuievents::KeyEvent::new(
1182 tuievents::Key::Char('L'),
1183 tuievents::KeyModifiers::SHIFT,
1184 )
1185 .into(),
1186 }
1187 }
1188}
1189
1190impl CheckConflict for KeysDatabase {
1191 fn iter(&self) -> impl Iterator<Item = (&KeyBinding, &'static str)> {
1192 once_chain! {
1193 (&self.add_all, "add_all"),
1194 }
1195 }
1196
1197 fn check_conflict(
1198 &self,
1199 key_path: &mut KeyPath,
1200 global_keys: &mut KeyHashMapOwned,
1201 ) -> Result<(), Vec<KeyConflictError>> {
1202 let mut conflicts: Vec<KeyConflictError> = Vec::new();
1203 let mut current_keys = KeyHashMap::new();
1204
1205 for (key, path) in self.iter() {
1206 if let Some(existing_path) = global_keys.get(key) {
1208 conflicts.push(KeyConflictError {
1209 key_path_first: existing_path.clone(),
1210 key_path_second: key_path.join_with_field(path),
1211 key: key.clone(),
1212 });
1213 continue;
1214 }
1215
1216 if let Some(existing_path) = current_keys.get(key) {
1217 conflicts.push(KeyConflictError {
1218 key_path_first: key_path.join_with_field(existing_path),
1219 key_path_second: key_path.join_with_field(path),
1220 key: key.clone(),
1221 });
1222 continue;
1223 }
1224
1225 current_keys.insert(key, path);
1226 }
1227
1228 if !conflicts.is_empty() {
1229 return Err(conflicts);
1230 }
1231
1232 Ok(())
1233 }
1234}
1235
1236#[derive(Debug, Clone, PartialEq, thiserror::Error)]
1239pub enum KeyParseError {
1240 #[error("Failed to parse Key because no key was found in the mapping, input: {0:#?}")]
1244 NoKeyFound(String),
1245 #[error("Failed to parse Key because of a trailing delimiter in input: {0:#?}")]
1249 TrailingDelimiter(String),
1250 #[error(
1254 "Failed to parse Key because multiple non-modifier keys were found, keys: [{old_key}, {new_key}], input: {input:#?}"
1255 )]
1256 MultipleKeys {
1257 input: String,
1258 old_key: String,
1259 new_key: String,
1260 },
1261 #[error("Failed to parse Key because of unknown key in mapping: {0:#?}")]
1268 UnknownKey(String),
1269}
1270
1271#[derive(Debug)]
1275struct SplitAtPlus<'a> {
1276 text: &'a str,
1277 chars: Peekable<CharIndices<'a>>,
1278 last_char_was_returned_delim: bool,
1280 last_char_was_delim: bool,
1288}
1289
1290impl<'a> SplitAtPlus<'a> {
1291 const DELIM: char = '+';
1293
1294 fn new(text: &'a str) -> Self {
1295 Self {
1296 text,
1297 chars: text.char_indices().peekable(),
1298 last_char_was_returned_delim: false,
1299 last_char_was_delim: false,
1300 }
1301 }
1302}
1303
1304impl<'a> Iterator for SplitAtPlus<'a> {
1305 type Item = &'a str;
1306
1307 fn next(&mut self) -> Option<Self::Item> {
1308 let (start, mut prior_char) = loop {
1310 break match self.chars.next() {
1311 None => {
1313 if self.last_char_was_delim {
1314 self.last_char_was_delim = false;
1315 return Some("");
1316 }
1317
1318 return None;
1319 }
1320 Some((i, c)) if c == Self::DELIM => {
1321 if self.last_char_was_returned_delim {
1326 self.last_char_was_returned_delim = false;
1327 self.last_char_was_delim = true;
1328 continue;
1329 } else if i == 0 && self.chars.peek().is_some_and(|v| v.1 != Self::DELIM) {
1330 self.last_char_was_returned_delim = false;
1333 self.last_char_was_delim = true;
1334 return Some("");
1335 }
1336
1337 self.last_char_was_returned_delim = true;
1338 self.last_char_was_delim = false;
1339 return Some("+");
1340 }
1341 Some(v) => v,
1345 };
1346 };
1347
1348 self.last_char_was_delim = false;
1352
1353 loop {
1354 prior_char = match self.chars.next() {
1355 None => return Some(&self.text[start..]),
1358 Some((end, c)) if c == Self::DELIM && prior_char != Self::DELIM => {
1362 self.last_char_was_delim = true;
1363 return Some(&self.text[start..end]);
1364 }
1365 Some((_, c)) => c,
1367 }
1368 }
1369 }
1370}
1371
1372#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
1374#[serde(try_from = "String")]
1375#[serde(into = "String")]
1376pub struct KeyBinding {
1377 pub key_event: tuievents::KeyEvent,
1378}
1379
1380impl KeyBinding {
1381 pub fn try_from_str(input: &str) -> Result<Self, KeyParseError> {
1385 let input = input.to_lowercase();
1386 let mut modifiers = tuievents::KeyModifiers::empty();
1387 let mut key_opt: Option<tuievents::Key> = None;
1388
1389 for val in SplitAtPlus::new(&input) {
1390 if val.is_empty() {
1392 return Err(KeyParseError::TrailingDelimiter(input));
1393 }
1394
1395 if let Ok(new_key) = KeyWrap::try_from(val) {
1396 let opt: &mut Option<tuievents::Key> = &mut key_opt;
1397 if let Some(existing_key) = opt {
1398 return Err(KeyParseError::MultipleKeys {
1399 input,
1400 old_key: KeyWrap::from(*existing_key).to_string(),
1401 new_key: new_key.to_string(),
1402 });
1403 }
1404
1405 *opt = Some(new_key.0);
1406
1407 continue;
1408 }
1409
1410 if let Ok(new_modifier) = SupportedModifiers::try_from(val) {
1411 modifiers |= new_modifier.into();
1412
1413 continue;
1414 }
1415
1416 return Err(KeyParseError::UnknownKey(val.into()));
1417 }
1418
1419 let Some(mut code) = key_opt else {
1420 return Err(KeyParseError::NoKeyFound(input));
1421 };
1422
1423 if modifiers.intersects(tuievents::KeyModifiers::SHIFT) {
1425 if let tuievents::Key::Char(v) = code {
1426 code = tuievents::Key::Char(v.to_ascii_uppercase());
1427 }
1428 }
1429
1430 Ok(Self {
1431 key_event: tuievents::KeyEvent::new(code, modifiers),
1432 })
1433 }
1434
1435 #[inline]
1437 #[must_use]
1438 pub fn get(&self) -> tuievents::KeyEvent {
1439 self.key_event
1440 }
1441
1442 #[inline]
1444 #[must_use]
1445 pub fn mod_key(&self) -> (tuievents::KeyModifiers, String) {
1446 (
1447 self.key_event.modifiers,
1448 KeyWrap::from(self.key_event.code).to_string(),
1449 )
1450 }
1451}
1452
1453impl Display for KeyBinding {
1454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1456 let key = KeyWrap::from(self.key_event.code);
1457 for res in SupportedModifiers::from_keymodifiers(self.key_event.modifiers)
1458 .into_iter()
1459 .map(Into::<&str>::into)
1460 .map(|v| write!(f, "{v}+"))
1461 {
1462 res?;
1463 }
1464
1465 write!(f, "{key}")
1466 }
1467}
1468
1469impl TryFrom<&str> for KeyBinding {
1470 type Error = KeyParseError;
1471
1472 fn try_from(value: &str) -> Result<Self, Self::Error> {
1473 Self::try_from_str(value)
1474 }
1475}
1476
1477impl TryFrom<String> for KeyBinding {
1478 type Error = KeyParseError;
1479
1480 fn try_from(value: String) -> Result<Self, Self::Error> {
1481 Self::try_from_str(&value)
1482 }
1483}
1484
1485impl From<KeyBinding> for String {
1486 fn from(value: KeyBinding) -> Self {
1487 value.to_string()
1488 }
1489}
1490
1491impl From<KeyWrap> for KeyBinding {
1493 fn from(value: KeyWrap) -> Self {
1494 Self {
1495 key_event: tuievents::KeyEvent::new(value.0, tuievents::KeyModifiers::empty()),
1496 }
1497 }
1498}
1499
1500impl From<tuievents::Key> for KeyBinding {
1502 fn from(value: tuievents::Key) -> Self {
1503 Self::from(KeyWrap(value))
1504 }
1505}
1506
1507impl From<tuievents::KeyEvent> for KeyBinding {
1509 fn from(value: tuievents::KeyEvent) -> Self {
1510 Self { key_event: value }
1511 }
1512}
1513
1514#[derive(Debug, Clone, PartialEq)]
1516enum KeyWrapParseError {
1517 Empty,
1518 UnknownKey(String),
1519}
1520
1521#[derive(Debug, PartialEq)]
1523struct KeyWrap(tuievents::Key);
1524
1525pub mod const_keys {
1527 #[macro_export]
1544 macro_rules! const_str {
1545 (
1546 $(#[$outer:meta])*
1547 $name:ident, $content:expr
1548 ) => {
1549 $(#[$outer])*
1550 pub const $name: &str = $content;
1551 };
1552 (
1553 $(
1554 $(#[$outer:meta])*
1555 $name:ident $content:expr
1556 ),+ $(,)?
1557 ) => {
1558 $(const_str!{ $(#[$outer])* $name, $content })+
1559 }
1560 }
1561
1562 const_str! {
1563 BACKSPACE "backspace",
1564 ENTER "enter",
1565 TAB "tab",
1566 BACKTAB "backtab",
1567 DELETE "delete",
1568 INSERT "insert",
1569 HOME "home",
1570 END "end",
1571 ESCAPE "escape",
1572
1573 PAGEUP "pageup",
1574 PAGEDOWN "pagedown",
1575
1576 ARROWUP "arrowup",
1577 ARROWDOWN "arrowdown",
1578 ARROWLEFT "arrowleft",
1579 ARROWRIGHT "arrowright",
1580
1581 CAPSLOCK "capslock",
1583 SCROLLLOCK "scrolllock",
1584 NUMLOCK "numlock",
1585 PRINTSCREEN "printscreen",
1586 PAUSE "pause",
1588
1589 NULL "null",
1592 MENU "menu",
1594
1595 SPACE "space"
1597 }
1598
1599 const_str! {
1600 CONTROL "control",
1601 ALT "alt",
1602 SHIFT "shift",
1603 }
1604}
1605
1606impl TryFrom<&str> for KeyWrap {
1608 type Error = KeyWrapParseError;
1609
1610 fn try_from(value: &str) -> Result<Self, Self::Error> {
1611 use tuievents::Key as TKey;
1613 if value.is_empty() {
1614 return Err(KeyWrapParseError::Empty);
1615 }
1616
1617 if value.len() == 1 {
1618 return Ok(Self(tuievents::Key::Char(value.chars().next().unwrap())));
1620 }
1621
1622 if value.len() <= 4 {
1624 if let Some(val) = value.strip_prefix('f') {
1625 if let Ok(parsed) = val.parse::<u8>() {
1626 return Ok(Self(tuievents::Key::Function(parsed)));
1628 }
1629 }
1631 }
1632
1633 let ret = match value {
1634 const_keys::BACKSPACE => Self(TKey::Backspace),
1635 const_keys::ENTER => Self(TKey::Enter),
1636 const_keys::TAB => Self(TKey::Tab),
1637 const_keys::BACKTAB => Self(TKey::BackTab),
1638 const_keys::DELETE => Self(TKey::Delete),
1639 const_keys::INSERT => Self(TKey::Insert),
1640 const_keys::HOME => Self(TKey::Home),
1641 const_keys::END => Self(TKey::End),
1642 const_keys::ESCAPE => Self(TKey::Esc),
1643
1644 const_keys::PAGEUP => Self(TKey::PageUp),
1645 const_keys::PAGEDOWN => Self(TKey::PageDown),
1646
1647 const_keys::ARROWUP => Self(TKey::Up),
1648 const_keys::ARROWDOWN => Self(TKey::Down),
1649 const_keys::ARROWLEFT => Self(TKey::Left),
1650 const_keys::ARROWRIGHT => Self(TKey::Right),
1651
1652 const_keys::CAPSLOCK => Self(TKey::CapsLock),
1653 const_keys::SCROLLLOCK => Self(TKey::ScrollLock),
1654 const_keys::NUMLOCK => Self(TKey::NumLock),
1655 const_keys::PRINTSCREEN => Self(TKey::PrintScreen),
1656 const_keys::PAUSE => Self(TKey::Pause),
1657
1658 const_keys::NULL => Self(TKey::Null),
1659 const_keys::MENU => Self(TKey::Menu),
1660
1661 const_keys::SPACE => Self(TKey::Char(' ')),
1663
1664 v => return Err(KeyWrapParseError::UnknownKey(v.to_owned())),
1665 };
1666
1667 Ok(ret)
1668 }
1669}
1670
1671impl Display for KeyWrap {
1672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1673 match self.0 {
1674 tuievents::Key::Backspace => const_keys::BACKSPACE.fmt(f),
1675 tuievents::Key::Enter => const_keys::ENTER.fmt(f),
1676 tuievents::Key::Tab => const_keys::TAB.fmt(f),
1677 tuievents::Key::BackTab => const_keys::BACKTAB.fmt(f),
1678 tuievents::Key::Delete => const_keys::DELETE.fmt(f),
1679 tuievents::Key::Insert => const_keys::INSERT.fmt(f),
1680 tuievents::Key::Home => const_keys::HOME.fmt(f),
1681 tuievents::Key::End => const_keys::END.fmt(f),
1682 tuievents::Key::Esc => const_keys::ESCAPE.fmt(f),
1683
1684 tuievents::Key::PageUp => const_keys::PAGEUP.fmt(f),
1685 tuievents::Key::PageDown => const_keys::PAGEDOWN.fmt(f),
1686
1687 tuievents::Key::Up => const_keys::ARROWUP.fmt(f),
1688 tuievents::Key::Down => const_keys::ARROWDOWN.fmt(f),
1689 tuievents::Key::Left => const_keys::ARROWLEFT.fmt(f),
1690 tuievents::Key::Right => const_keys::ARROWRIGHT.fmt(f),
1691
1692 tuievents::Key::CapsLock => const_keys::CAPSLOCK.fmt(f),
1693 tuievents::Key::ScrollLock => const_keys::SCROLLLOCK.fmt(f),
1694 tuievents::Key::NumLock => const_keys::NUMLOCK.fmt(f),
1695 tuievents::Key::PrintScreen => const_keys::PRINTSCREEN.fmt(f),
1696 tuievents::Key::Pause => const_keys::PAUSE.fmt(f),
1697
1698 tuievents::Key::Null => const_keys::NULL.fmt(f),
1699 tuievents::Key::Menu => const_keys::MENU.fmt(f),
1700
1701 tuievents::Key::Function(v) => write!(f, "f{v}"),
1702 tuievents::Key::Char(v) => {
1703 if v == ' ' {
1704 write!(f, "{}", const_keys::SPACE)
1705 } else {
1706 v.fmt(f)
1707 }
1708 }
1709
1710 tuievents::Key::Media(_) => unimplemented!(),
1712
1713 tuievents::Key::KeypadBegin => unimplemented!(),
1715
1716 tuievents::Key::ShiftLeft
1718 | tuievents::Key::AltLeft
1719 | tuievents::Key::CtrlLeft
1720 | tuievents::Key::ShiftRight
1721 | tuievents::Key::AltRight
1722 | tuievents::Key::CtrlRight
1723 | tuievents::Key::ShiftUp
1724 | tuievents::Key::AltUp
1725 | tuievents::Key::CtrlUp
1726 | tuievents::Key::ShiftDown
1727 | tuievents::Key::AltDown
1728 | tuievents::Key::CtrlDown
1729 | tuievents::Key::CtrlHome
1730 | tuievents::Key::CtrlEnd => unimplemented!(),
1731 }
1732 }
1733}
1734
1735impl From<tuievents::Key> for KeyWrap {
1737 fn from(value: tuievents::Key) -> Self {
1738 Self(value)
1739 }
1740}
1741
1742#[derive(Debug, Clone, Copy)]
1746enum SupportedModifiers {
1747 Control,
1748 Shift,
1749 Alt,
1750}
1751
1752impl From<SupportedModifiers> for &'static str {
1753 fn from(value: SupportedModifiers) -> Self {
1754 match value {
1755 SupportedModifiers::Control => const_keys::CONTROL,
1756 SupportedModifiers::Shift => const_keys::SHIFT,
1757 SupportedModifiers::Alt => const_keys::ALT,
1758 }
1759 }
1760}
1761
1762impl TryFrom<&str> for SupportedModifiers {
1764 type Error = KeyWrapParseError;
1765
1766 fn try_from(value: &str) -> Result<Self, Self::Error> {
1767 if value.is_empty() {
1768 return Err(KeyWrapParseError::Empty);
1769 }
1770
1771 let val = match value {
1772 const_keys::CONTROL => Self::Control,
1773 const_keys::ALT => Self::Alt,
1774 const_keys::SHIFT => Self::Shift,
1775 v => return Err(KeyWrapParseError::UnknownKey(v.to_owned())),
1776 };
1777
1778 Ok(val)
1779 }
1780}
1781
1782impl SupportedModifiers {
1783 fn from_keymodifiers(modifiers: tuievents::KeyModifiers) -> Vec<Self> {
1785 let mut ret = Vec::with_capacity(3);
1786
1787 if modifiers.contains(tuievents::KeyModifiers::CONTROL) {
1788 ret.push(Self::Control);
1789 }
1790 if modifiers.contains(tuievents::KeyModifiers::ALT) {
1791 ret.push(Self::Alt);
1792 }
1793 if modifiers.contains(tuievents::KeyModifiers::SHIFT) {
1794 ret.push(Self::Shift);
1795 }
1796
1797 ret
1798 }
1799}
1800
1801impl From<SupportedModifiers> for tuievents::KeyModifiers {
1802 fn from(value: SupportedModifiers) -> Self {
1803 match value {
1804 SupportedModifiers::Control => Self::CONTROL,
1805 SupportedModifiers::Shift => Self::SHIFT,
1806 SupportedModifiers::Alt => Self::ALT,
1807 }
1808 }
1809}
1810
1811mod v1_interop {
1812 use super::{
1813 KeyBinding, Keys, KeysConfigEditor, KeysDatabase, KeysLibrary, KeysLyric, KeysMoveCoverArt,
1814 KeysNavigation, KeysPlayer, KeysPlaylist, KeysPodcast, KeysSelectView, tuievents,
1815 };
1816 use crate::config::v1;
1817
1818 impl From<v1::BindingForEvent> for KeyBinding {
1819 fn from(value: v1::BindingForEvent) -> Self {
1820 let code = if let tuievents::Key::Char(char) = value.code {
1821 if value.modifier.intersects(tuievents::KeyModifiers::SHIFT) {
1823 tuievents::Key::Char(char.to_ascii_uppercase())
1824 } else {
1825 tuievents::Key::Char(char.to_ascii_lowercase())
1826 }
1827 } else {
1828 value.code
1829 };
1830 Self::from(tuievents::KeyEvent {
1831 code,
1832 modifiers: value.modifier,
1833 })
1834 }
1835 }
1836
1837 impl From<v1::Keys> for Keys {
1838 #[allow(clippy::too_many_lines)]
1839 fn from(value: v1::Keys) -> Self {
1840 let podcast_delete_feed_key =
1842 if value.podcast_episode_download == value.podcast_delete_feed {
1843 KeysPodcast::default().delete_feed
1844 } else {
1845 value.podcast_delete_feed.into()
1846 };
1847 let podcast_delete_delete_all_eq = match (
1848 value.podcast_delete_all_feeds.code,
1849 value.podcast_delete_feed.code,
1850 ) {
1851 (tuievents::Key::Char(left), tuievents::Key::Char(right)) => {
1853 left.eq_ignore_ascii_case(&right)
1854 }
1855 (left, right) => left == right,
1856 };
1857 let podcast_delete_all_feeds_key = if value.podcast_episode_download
1858 == value.podcast_delete_feed
1859 && podcast_delete_delete_all_eq
1860 {
1861 KeysPodcast::default().delete_all_feeds
1862 } else {
1863 value.podcast_delete_all_feeds.into()
1864 };
1865 let podcast_delete_episode_key = if podcast_delete_feed_key
1867 == value.podcast_episode_delete_file.key_event().into()
1868 {
1869 KeysPodcast::default().delete_local_episode
1870 } else {
1871 value.podcast_episode_delete_file.into()
1872 };
1873
1874 let player_volume_down_key = {
1877 let old = value.global_player_volume_minus_2;
1878 if old.code == tuievents::Key::Char('_')
1879 && old.modifier.intersects(tuievents::KeyModifiers::SHIFT)
1880 {
1881 KeyBinding::from(tuievents::KeyEvent::new(
1882 tuievents::Key::Char('_'),
1883 tuievents::KeyModifiers::NONE,
1884 ))
1885 } else {
1886 old.into()
1887 }
1888 };
1889
1890 Self {
1891 escape: value.global_esc.into(),
1892 quit: value.global_quit.into(),
1893 select_view_keys: KeysSelectView {
1894 view_library: value.global_layout_treeview.into(),
1895 view_database: value.global_layout_database.into(),
1896 view_podcasts: value.global_layout_podcast.into(),
1897 open_config: value.global_config_open.into(),
1898 open_help: value.global_help.into(),
1899 },
1900 navigation_keys: KeysNavigation {
1901 up: value.global_up.into(),
1902 down: value.global_down.into(),
1903 left: value.global_left.into(),
1904 right: value.global_right.into(),
1905 goto_top: value.global_goto_top.into(),
1906 goto_bottom: value.global_goto_bottom.into(),
1907 },
1908 player_keys: KeysPlayer {
1909 toggle_pause: value.global_player_toggle_pause.into(),
1910 next_track: value.global_player_next.into(),
1911 previous_track: value.global_player_previous.into(),
1912 volume_up: value.global_player_volume_plus_2.into(),
1913 volume_down: player_volume_down_key,
1914 seek_forward: value.global_player_seek_forward.into(),
1915 seek_backward: value.global_player_seek_backward.into(),
1916 speed_up: value.global_player_speed_up.into(),
1917 speed_down: value.global_player_speed_down.into(),
1918 toggle_prefetch: value.global_player_toggle_gapless.into(),
1919 save_playlist: value.global_save_playlist.into(),
1920 },
1921 lyric_keys: KeysLyric {
1922 adjust_offset_forwards: value.global_lyric_adjust_forward.into(),
1923 adjust_offset_backwards: value.global_lyric_adjust_backward.into(),
1924 cycle_frames: value.global_lyric_cycle.into(),
1925 },
1926 library_keys: KeysLibrary {
1927 load_track: value.global_right.into(),
1929 load_dir: value.library_load_dir.into(),
1930 delete: value.library_delete.into(),
1931 yank: value.library_yank.into(),
1932 paste: value.library_paste.into(),
1933 cycle_root: value.library_switch_root.into(),
1934 add_root: value.library_add_root.into(),
1935 remove_root: value.library_remove_root.into(),
1936 search: value.library_search.into(),
1937 youtube_search: value.library_search_youtube.into(),
1938 open_tag_editor: value.library_tag_editor_open.into(),
1939 },
1940 playlist_keys: KeysPlaylist {
1941 delete: value.playlist_delete.into(),
1942 delete_all: value.playlist_delete_all.into(),
1943 shuffle: value.playlist_shuffle.into(),
1944 cycle_loop_mode: value.playlist_mode_cycle.into(),
1945 play_selected: value.playlist_play_selected.into(),
1946 search: value.playlist_search.into(),
1947 swap_up: value.playlist_swap_up.into(),
1948 swap_down: value.playlist_swap_down.into(),
1949 add_random_songs: value.playlist_add_random_tracks.into(),
1950 add_random_album: value.playlist_add_random_album.into(),
1951 },
1952 database_keys: KeysDatabase {
1953 add_selected: value.global_right.into(),
1955 add_all: value.database_add_all.into(),
1956 },
1957 podcast_keys: KeysPodcast {
1958 search: value.podcast_search_add_feed.into(),
1959 mark_played: value.podcast_mark_played.into(),
1960 mark_all_played: value.podcast_mark_all_played.into(),
1961 refresh_feed: value.podcast_refresh_feed.into(),
1962 refresh_all_feeds: value.podcast_refresh_all_feeds.into(),
1963 download_episode: value.podcast_episode_download.into(),
1964 delete_local_episode: podcast_delete_episode_key,
1965 delete_feed: podcast_delete_feed_key,
1966 delete_all_feeds: podcast_delete_all_feeds_key,
1967 },
1968 move_cover_art_keys: KeysMoveCoverArt {
1969 move_left: value.global_xywh_move_left.into(),
1970 move_right: value.global_xywh_move_right.into(),
1971 move_up: value.global_xywh_move_up.into(),
1972 move_down: value.global_xywh_move_down.into(),
1973 increase_size: value.global_xywh_zoom_in.into(),
1974 decrease_size: value.global_xywh_zoom_out.into(),
1975 toggle_hide: value.global_xywh_hide.into(),
1976 },
1977 config_keys: KeysConfigEditor {
1978 save: value.config_save.into(),
1979 },
1980 }
1981 }
1982 }
1983
1984 #[cfg(test)]
1985 mod test {
1986 use super::*;
1987 use pretty_assertions::assert_eq;
1988 use v1::BindingForEvent;
1989
1990 #[allow(clippy::too_many_lines)] #[test]
1992 fn should_convert_default_without_error() {
1993 let converted: Keys = v1::Keys::default().into();
1994
1995 let expected_select_view_keys = KeysSelectView {
1997 view_library: tuievents::Key::Char('1').into(),
1998 view_database: tuievents::Key::Char('2').into(),
1999 view_podcasts: tuievents::Key::Char('3').into(),
2000 open_config: tuievents::KeyEvent::new(
2001 tuievents::Key::Char('C'),
2002 tuievents::KeyModifiers::SHIFT,
2003 )
2004 .into(),
2005 open_help: tuievents::KeyEvent::new(
2006 tuievents::Key::Char('h'),
2007 tuievents::KeyModifiers::CONTROL,
2008 )
2009 .into(),
2010 };
2011 assert_eq!(converted.select_view_keys, expected_select_view_keys);
2012
2013 let expected_navigation_keys = KeysNavigation {
2014 up: tuievents::Key::Char('k').into(),
2015 down: tuievents::Key::Char('j').into(),
2016 left: tuievents::Key::Char('h').into(),
2017 right: tuievents::Key::Char('l').into(),
2018 goto_top: tuievents::Key::Char('g').into(),
2019 goto_bottom: tuievents::KeyEvent::new(
2020 tuievents::Key::Char('G'),
2021 tuievents::KeyModifiers::SHIFT,
2022 )
2023 .into(),
2024 };
2025 assert_eq!(converted.navigation_keys, expected_navigation_keys);
2026
2027 let expected_player_keys = KeysPlayer {
2028 toggle_pause: tuievents::Key::Char(' ').into(),
2029 next_track: tuievents::Key::Char('n').into(),
2030 previous_track: tuievents::KeyEvent::new(
2031 tuievents::Key::Char('N'),
2032 tuievents::KeyModifiers::SHIFT,
2033 )
2034 .into(),
2035 volume_up: tuievents::KeyEvent::new(
2037 tuievents::Key::Char('='),
2038 tuievents::KeyModifiers::NONE,
2039 )
2040 .into(),
2041 volume_down: tuievents::KeyEvent::new(
2042 tuievents::Key::Char('_'),
2043 tuievents::KeyModifiers::NONE,
2044 )
2045 .into(),
2046 seek_forward: tuievents::Key::Char('f').into(),
2047 seek_backward: tuievents::Key::Char('b').into(),
2048 speed_up: tuievents::KeyEvent::new(
2049 tuievents::Key::Char('f'),
2050 tuievents::KeyModifiers::CONTROL,
2051 )
2052 .into(),
2053 speed_down: tuievents::KeyEvent::new(
2054 tuievents::Key::Char('b'),
2055 tuievents::KeyModifiers::CONTROL,
2056 )
2057 .into(),
2058 toggle_prefetch: tuievents::KeyEvent::new(
2059 tuievents::Key::Char('g'),
2060 tuievents::KeyModifiers::CONTROL,
2061 )
2062 .into(),
2063 save_playlist: tuievents::KeyEvent::new(
2064 tuievents::Key::Char('s'),
2065 tuievents::KeyModifiers::CONTROL,
2066 )
2067 .into(),
2068 };
2069 assert_eq!(converted.player_keys, expected_player_keys);
2070
2071 let expected_lyric_keys = KeysLyric {
2072 adjust_offset_forwards: tuievents::KeyEvent::new(
2073 tuievents::Key::Char('F'),
2074 tuievents::KeyModifiers::SHIFT,
2075 )
2076 .into(),
2077 adjust_offset_backwards: tuievents::KeyEvent::new(
2078 tuievents::Key::Char('B'),
2079 tuievents::KeyModifiers::SHIFT,
2080 )
2081 .into(),
2082 cycle_frames: tuievents::KeyEvent::new(
2083 tuievents::Key::Char('T'),
2084 tuievents::KeyModifiers::SHIFT,
2085 )
2086 .into(),
2087 };
2088 assert_eq!(converted.lyric_keys, expected_lyric_keys);
2089
2090 let expected_library_keys = KeysLibrary {
2091 load_track: tuievents::Key::Char('l').into(),
2092 load_dir: tuievents::KeyEvent::new(
2093 tuievents::Key::Char('L'),
2094 tuievents::KeyModifiers::SHIFT,
2095 )
2096 .into(),
2097 delete: tuievents::Key::Char('d').into(),
2098 yank: tuievents::Key::Char('y').into(),
2099 paste: tuievents::Key::Char('p').into(),
2100 cycle_root: tuievents::Key::Char('o').into(),
2101 add_root: tuievents::Key::Char('a').into(),
2102 remove_root: tuievents::KeyEvent::new(
2103 tuievents::Key::Char('A'),
2104 tuievents::KeyModifiers::SHIFT,
2105 )
2106 .into(),
2107 search: tuievents::Key::Char('/').into(),
2108 youtube_search: tuievents::Key::Char('s').into(),
2109 open_tag_editor: tuievents::Key::Char('t').into(),
2110 };
2111 assert_eq!(converted.library_keys, expected_library_keys);
2112
2113 let expected_playlist_keys = KeysPlaylist {
2114 delete: tuievents::Key::Char('d').into(),
2115 delete_all: tuievents::KeyEvent::new(
2116 tuievents::Key::Char('D'),
2117 tuievents::KeyModifiers::SHIFT,
2118 )
2119 .into(),
2120 shuffle: tuievents::Key::Char('r').into(),
2121 cycle_loop_mode: tuievents::Key::Char('m').into(),
2122 play_selected: tuievents::Key::Char('l').into(),
2123 search: tuievents::Key::Char('/').into(),
2124 swap_up: tuievents::KeyEvent::new(
2125 tuievents::Key::Char('K'),
2126 tuievents::KeyModifiers::SHIFT,
2127 )
2128 .into(),
2129 swap_down: tuievents::KeyEvent::new(
2130 tuievents::Key::Char('J'),
2131 tuievents::KeyModifiers::SHIFT,
2132 )
2133 .into(),
2134 add_random_songs: tuievents::Key::Char('s').into(),
2135 add_random_album: tuievents::KeyEvent::new(
2136 tuievents::Key::Char('S'),
2137 tuievents::KeyModifiers::SHIFT,
2138 )
2139 .into(),
2140 };
2141 assert_eq!(converted.playlist_keys, expected_playlist_keys);
2142
2143 let expected_database_keys = KeysDatabase {
2144 add_selected: tuievents::Key::Char('l').into(),
2145 add_all: tuievents::KeyEvent::new(
2146 tuievents::Key::Char('L'),
2147 tuievents::KeyModifiers::SHIFT,
2148 )
2149 .into(),
2150 };
2151 assert_eq!(converted.database_keys, expected_database_keys);
2152
2153 let expected_podcast_keys = KeysPodcast {
2154 search: tuievents::Key::Char('s').into(),
2155 mark_played: tuievents::Key::Char('m').into(),
2156 mark_all_played: tuievents::KeyEvent::new(
2157 tuievents::Key::Char('M'),
2158 tuievents::KeyModifiers::SHIFT,
2159 )
2160 .into(),
2161 refresh_feed: tuievents::Key::Char('r').into(),
2162 refresh_all_feeds: tuievents::KeyEvent::new(
2163 tuievents::Key::Char('R'),
2164 tuievents::KeyModifiers::SHIFT,
2165 )
2166 .into(),
2167 download_episode: tuievents::Key::Char('d').into(),
2168 delete_local_episode: tuievents::KeyEvent::new(
2169 tuievents::Key::Char('D'),
2170 tuievents::KeyModifiers::SHIFT,
2171 )
2172 .into(),
2173 delete_feed: tuievents::Key::Char('x').into(),
2174 delete_all_feeds: tuievents::KeyEvent::new(
2175 tuievents::Key::Char('X'),
2176 tuievents::KeyModifiers::SHIFT,
2177 )
2178 .into(),
2179 };
2180 assert_eq!(converted.podcast_keys, expected_podcast_keys);
2181
2182 let expected_move_cover_art_keys = KeysMoveCoverArt {
2183 move_left: tuievents::KeyEvent::new(
2184 tuievents::Key::Left,
2185 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2186 )
2187 .into(),
2188 move_right: tuievents::KeyEvent::new(
2189 tuievents::Key::Right,
2190 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2191 )
2192 .into(),
2193 move_up: tuievents::KeyEvent::new(
2194 tuievents::Key::Up,
2195 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2196 )
2197 .into(),
2198 move_down: tuievents::KeyEvent::new(
2199 tuievents::Key::Down,
2200 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2201 )
2202 .into(),
2203 increase_size: tuievents::KeyEvent::new(
2204 tuievents::Key::PageUp,
2205 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2206 )
2207 .into(),
2208 decrease_size: tuievents::KeyEvent::new(
2209 tuievents::Key::PageDown,
2210 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2211 )
2212 .into(),
2213 toggle_hide: tuievents::KeyEvent::new(
2214 tuievents::Key::End,
2215 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2216 )
2217 .into(),
2218 };
2219 assert_eq!(converted.move_cover_art_keys, expected_move_cover_art_keys);
2220
2221 let expected_config_editor_keys = KeysConfigEditor {
2222 save: tuievents::KeyEvent::new(
2223 tuievents::Key::Char('s'),
2224 tuievents::KeyModifiers::CONTROL,
2225 )
2226 .into(),
2227 };
2228 assert_eq!(converted.config_keys, expected_config_editor_keys);
2229
2230 let expected_keys = Keys {
2231 escape: tuievents::Key::Esc.into(),
2232 quit: tuievents::Key::Char('q').into(),
2233 select_view_keys: expected_select_view_keys,
2234 navigation_keys: expected_navigation_keys,
2235 player_keys: expected_player_keys,
2236 lyric_keys: expected_lyric_keys,
2237 library_keys: expected_library_keys,
2238 playlist_keys: expected_playlist_keys,
2239 database_keys: expected_database_keys,
2240 podcast_keys: expected_podcast_keys,
2241 move_cover_art_keys: expected_move_cover_art_keys,
2242 config_keys: expected_config_editor_keys,
2243 };
2244
2245 assert_eq!(converted, expected_keys);
2246
2247 assert_eq!(Ok(()), expected_keys.check_keys());
2248 }
2249
2250 #[test]
2251 fn should_fixup_old_volume_default() {
2252 let converted: Keys = {
2253 let v1 = v1::Keys {
2254 global_player_volume_minus_2: BindingForEvent {
2255 code: tuievents::Key::Char('_'),
2256 modifier: tuievents::KeyModifiers::SHIFT,
2257 },
2258 ..v1::Keys::default()
2259 };
2260
2261 v1.into()
2262 };
2263
2264 let expected_player_keys = KeysPlayer {
2265 toggle_pause: tuievents::Key::Char(' ').into(),
2266 next_track: tuievents::Key::Char('n').into(),
2267 previous_track: tuievents::KeyEvent::new(
2268 tuievents::Key::Char('N'),
2269 tuievents::KeyModifiers::SHIFT,
2270 )
2271 .into(),
2272 volume_up: tuievents::KeyEvent::new(
2274 tuievents::Key::Char('='),
2275 tuievents::KeyModifiers::NONE,
2276 )
2277 .into(),
2278 volume_down: tuievents::KeyEvent::new(
2279 tuievents::Key::Char('_'),
2280 tuievents::KeyModifiers::NONE,
2281 )
2282 .into(),
2283 seek_forward: tuievents::Key::Char('f').into(),
2284 seek_backward: tuievents::Key::Char('b').into(),
2285 speed_up: tuievents::KeyEvent::new(
2286 tuievents::Key::Char('f'),
2287 tuievents::KeyModifiers::CONTROL,
2288 )
2289 .into(),
2290 speed_down: tuievents::KeyEvent::new(
2291 tuievents::Key::Char('b'),
2292 tuievents::KeyModifiers::CONTROL,
2293 )
2294 .into(),
2295 toggle_prefetch: tuievents::KeyEvent::new(
2296 tuievents::Key::Char('g'),
2297 tuievents::KeyModifiers::CONTROL,
2298 )
2299 .into(),
2300 save_playlist: tuievents::KeyEvent::new(
2301 tuievents::Key::Char('s'),
2302 tuievents::KeyModifiers::CONTROL,
2303 )
2304 .into(),
2305 };
2306 assert_eq!(converted.player_keys, expected_player_keys);
2307 }
2308 }
2309}
2310
2311#[cfg(test)]
2312mod test {
2313 use super::*;
2314
2315 mod split_at_plus {
2316 use super::*;
2317 use pretty_assertions::assert_eq;
2318
2319 #[test]
2320 fn should_do_nothing_at_empty() {
2321 assert_eq!(
2322 Vec::<&str>::new(),
2323 SplitAtPlus::new("").collect::<Vec<&str>>()
2324 );
2325 }
2326
2327 #[test]
2328 fn should_treat_one_as_key() {
2329 assert_eq!(vec!["+"], SplitAtPlus::new("+").collect::<Vec<&str>>());
2330 }
2331
2332 #[test]
2333 fn should_parse_with_non_delim_last() {
2334 assert_eq!(
2335 vec!["+", "control"],
2336 SplitAtPlus::new("++control").collect::<Vec<&str>>()
2337 );
2338 }
2339
2340 #[test]
2341 fn should_parse_with_non_delim_first() {
2342 assert_eq!(
2343 vec!["control", "+"],
2344 SplitAtPlus::new("control++").collect::<Vec<&str>>()
2345 );
2346 }
2347
2348 #[test]
2349 fn should_parse_with_multiple_with_delim() {
2350 assert_eq!(
2351 vec!["+", "+"],
2352 SplitAtPlus::new("+++").collect::<Vec<&str>>()
2353 );
2354 }
2355
2356 #[test]
2357 fn should_parse_with_only_delim() {
2358 assert_eq!(
2359 vec!["q", "control"],
2360 SplitAtPlus::new("q+control").collect::<Vec<&str>>()
2361 );
2362 }
2363
2364 #[test]
2365 fn should_treat_without_delim() {
2366 assert_eq!(
2367 vec!["control"],
2368 SplitAtPlus::new("control").collect::<Vec<&str>>()
2369 );
2370 }
2371
2372 #[test]
2373 fn should_return_trailing_empty_string_on_delim_last() {
2374 assert_eq!(vec!["+", ""], SplitAtPlus::new("++").collect::<Vec<&str>>());
2375 assert_eq!(
2376 vec!["control", ""],
2377 SplitAtPlus::new("control+").collect::<Vec<&str>>()
2378 );
2379 }
2380
2381 #[test]
2382 fn should_parse_non_delim_delim_non_delim() {
2383 assert_eq!(
2384 vec!["control", "+", "shift"],
2385 SplitAtPlus::new("control+++shift").collect::<Vec<&str>>()
2386 );
2387 }
2388
2389 #[test]
2390 fn should_treat_delim_followed_by_key_as_trailing() {
2391 assert_eq!(vec!["", "q"], SplitAtPlus::new("+q").collect::<Vec<&str>>());
2392 }
2393 }
2394
2395 mod key_wrap {
2396 use super::*;
2397 use pretty_assertions::assert_eq;
2398
2399 #[test]
2400 fn should_parse_function_keys() {
2401 assert_eq!(
2402 KeyWrap(tuievents::Key::Function(10)),
2403 KeyWrap::try_from("f10").unwrap()
2404 );
2405 assert_eq!(
2406 KeyWrap(tuievents::Key::Function(0)),
2407 KeyWrap::try_from("f0").unwrap()
2408 );
2409 assert_eq!(
2410 KeyWrap(tuievents::Key::Function(255)),
2411 KeyWrap::try_from("f255").unwrap()
2412 );
2413 }
2414
2415 #[test]
2416 fn should_parse_char() {
2417 assert_eq!(
2418 KeyWrap(tuievents::Key::Char('q')),
2419 KeyWrap::try_from("q").unwrap()
2420 );
2421 assert_eq!(
2422 KeyWrap(tuievents::Key::Char('w')),
2423 KeyWrap::try_from("w").unwrap()
2424 );
2425 assert_eq!(
2426 KeyWrap(tuievents::Key::Char('.')),
2427 KeyWrap::try_from(".").unwrap()
2428 );
2429 assert_eq!(
2430 KeyWrap(tuievents::Key::Char('@')),
2431 KeyWrap::try_from("@").unwrap()
2432 );
2433
2434 assert_eq!(
2436 KeyWrap(tuievents::Key::Char(' ')),
2437 KeyWrap::try_from("space").unwrap()
2438 );
2439 }
2440
2441 #[test]
2442 fn should_serialize_function_keys() {
2443 assert_eq!(&"f10", &KeyWrap(tuievents::Key::Function(10)).to_string());
2444 assert_eq!(&"f0", &KeyWrap(tuievents::Key::Function(0)).to_string());
2445 assert_eq!(&"f255", &KeyWrap(tuievents::Key::Function(255)).to_string());
2446 }
2447
2448 #[test]
2449 fn should_serialize_char() {
2450 assert_eq!(&"q", &KeyWrap(tuievents::Key::Char('q')).to_string());
2451 assert_eq!(&"w", &KeyWrap(tuievents::Key::Char('w')).to_string());
2452 assert_eq!(&".", &KeyWrap(tuievents::Key::Char('.')).to_string());
2453 assert_eq!(&"@", &KeyWrap(tuievents::Key::Char('@')).to_string());
2454
2455 assert_eq!(&"space", &KeyWrap(tuievents::Key::Char(' ')).to_string());
2457 }
2458 }
2459
2460 mod key_binding {
2461 use super::*;
2462 use pretty_assertions::assert_eq;
2463
2464 #[test]
2465 fn should_parse_keys_simple() {
2466 assert_eq!(
2468 KeyBinding::from(tuievents::KeyEvent::new(
2469 tuievents::Key::Char('Q'),
2470 tuievents::KeyModifiers::all()
2471 )),
2472 KeyBinding::try_from("CONTROL+ALT+SHIFT+Q").unwrap()
2473 );
2474
2475 assert_eq!(
2477 KeyBinding::from(tuievents::KeyEvent::new(
2478 tuievents::Key::Char('q'),
2479 tuievents::KeyModifiers::empty()
2480 )),
2481 KeyBinding::try_from("Q").unwrap()
2482 );
2483
2484 assert_eq!(
2486 KeyBinding::from(tuievents::KeyEvent::new(
2487 tuievents::Key::Char('q'),
2488 tuievents::KeyModifiers::CONTROL
2489 )),
2490 KeyBinding::try_from("CONTROL+CONTROL+CONTROL+Q").unwrap()
2491 );
2492 }
2493
2494 #[test]
2495 fn should_error_on_multiple_keys() {
2496 assert_eq!(
2497 Err(KeyParseError::MultipleKeys {
2498 input: "q+s".to_string(),
2499 old_key: "q".to_string(),
2500 new_key: "s".to_string()
2501 }),
2502 KeyBinding::try_from("Q+S")
2503 );
2504 }
2505
2506 #[test]
2507 fn should_serialize() {
2508 assert_eq!(
2510 "control+alt+shift+q",
2511 KeyBinding::from(tuievents::KeyEvent::new(
2512 tuievents::Key::Char('q'),
2513 tuievents::KeyModifiers::all()
2514 ))
2515 .to_string()
2516 );
2517
2518 assert_eq!(
2520 "control+q",
2521 KeyBinding::from(tuievents::KeyEvent::new(
2522 tuievents::Key::Char('q'),
2523 tuievents::KeyModifiers::CONTROL
2524 ))
2525 .to_string()
2526 );
2527
2528 assert_eq!(
2530 "alt+q",
2531 KeyBinding::from(tuievents::KeyEvent::new(
2532 tuievents::Key::Char('q'),
2533 tuievents::KeyModifiers::ALT
2534 ))
2535 .to_string()
2536 );
2537
2538 assert_eq!(
2540 "shift+q",
2541 KeyBinding::from(tuievents::KeyEvent::new(
2542 tuievents::Key::Char('q'),
2543 tuievents::KeyModifiers::SHIFT
2544 ))
2545 .to_string()
2546 );
2547
2548 assert_eq!(
2550 "q",
2551 KeyBinding::from(tuievents::KeyEvent::new(
2552 tuievents::Key::Char('q'),
2553 tuievents::KeyModifiers::empty()
2554 ))
2555 .to_string()
2556 );
2557 }
2558
2559 #[test]
2560 fn should_allow_special_keys() {
2561 assert_eq!(
2563 KeyBinding::from(tuievents::KeyEvent::new(
2564 tuievents::Key::Char('+'),
2565 tuievents::KeyModifiers::empty()
2566 )),
2567 KeyBinding::try_from("+").unwrap()
2568 );
2569
2570 assert_eq!(
2572 KeyBinding::from(tuievents::KeyEvent::new(
2573 tuievents::Key::Char('-'),
2574 tuievents::KeyModifiers::empty()
2575 )),
2576 KeyBinding::try_from("-").unwrap()
2577 );
2578
2579 assert_eq!(
2580 KeyBinding::from(tuievents::KeyEvent::new(
2581 tuievents::Key::Char(' '),
2582 tuievents::KeyModifiers::empty()
2583 )),
2584 KeyBinding::try_from(" ").unwrap()
2585 );
2586 }
2587
2588 #[test]
2589 fn should_not_allow_invalid_formats() {
2590 assert_eq!(
2592 Err(KeyParseError::NoKeyFound(String::new())),
2593 KeyBinding::try_from("")
2594 );
2595
2596 assert_eq!(
2598 Err(KeyParseError::UnknownKey(" ".to_owned())),
2599 KeyBinding::try_from(" ")
2600 );
2601
2602 assert_eq!(
2604 Err(KeyParseError::TrailingDelimiter("++".to_owned())),
2605 KeyBinding::try_from("++")
2606 );
2607
2608 assert_eq!(
2610 Err(KeyParseError::TrailingDelimiter("control+".to_owned())),
2611 KeyBinding::try_from("control+")
2612 );
2613
2614 assert_eq!(
2616 Err(KeyParseError::TrailingDelimiter("+control".to_owned())),
2617 KeyBinding::try_from("+control")
2618 );
2619 }
2620 }
2621
2622 mod keys {
2623 use figment::{
2624 Figment,
2625 providers::{Format, Toml},
2626 };
2627 use pretty_assertions::assert_eq;
2628
2629 use super::*;
2630
2631 #[test]
2632 fn should_parse_default_keys() {
2633 let serialized = toml::to_string(&Keys::default()).unwrap();
2634
2635 let parsed: Keys = Figment::new()
2636 .merge(Toml::string(&serialized))
2637 .extract()
2638 .unwrap();
2639
2640 assert_eq!(Keys::default(), parsed);
2641 }
2642
2643 #[test]
2644 fn should_not_conflict_on_default() {
2645 assert_eq!(Ok(()), Keys::default().check_keys());
2646 }
2647
2648 #[test]
2649 fn should_not_conflict_on_different_view() {
2650 let mut keys = Keys::default();
2652 keys.library_keys.delete = tuievents::Key::Delete.into();
2653 keys.podcast_keys.delete_feed = tuievents::Key::Delete.into();
2654
2655 assert_eq!(Ok(()), keys.check_keys());
2656 }
2657
2658 #[test]
2659 fn should_err_on_global_key_conflict() {
2660 let mut keys = Keys::default();
2662 keys.select_view_keys.view_podcasts = tuievents::Key::Delete.into();
2663 keys.podcast_keys.delete_feed = tuievents::Key::Delete.into();
2664
2665 assert_eq!(
2666 Err(KeysCheckError {
2667 errored_keys: vec![KeyConflictError {
2668 key_path_first: "keys.view.view_podcasts".into(),
2669 key_path_second: "keys.podcast.delete_feed".into(),
2670 key: tuievents::Key::Delete.into()
2671 }]
2672 }),
2673 keys.check_keys()
2674 );
2675 }
2676 }
2677}