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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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.to_string(),
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("Failed to parse Key because multiple non-modifier keys were found, keys: [{old_key}, {new_key}], input: {input:#?}")]
1254 MultipleKeys {
1255 input: String,
1256 old_key: String,
1257 new_key: String,
1258 },
1259 #[error("Failed to parse Key because of unknown key in mapping: {0:#?}")]
1266 UnknownKey(String),
1267}
1268
1269#[derive(Debug)]
1273struct SplitAtPlus<'a> {
1274 text: &'a str,
1275 chars: Peekable<CharIndices<'a>>,
1276 last_char_was_returned_delim: bool,
1278 last_char_was_delim: bool,
1286}
1287
1288impl<'a> SplitAtPlus<'a> {
1289 const DELIM: char = '+';
1291
1292 fn new(text: &'a str) -> Self {
1293 Self {
1294 text,
1295 chars: text.char_indices().peekable(),
1296 last_char_was_returned_delim: false,
1297 last_char_was_delim: false,
1298 }
1299 }
1300}
1301
1302impl<'a> Iterator for SplitAtPlus<'a> {
1303 type Item = &'a str;
1304
1305 fn next(&mut self) -> Option<Self::Item> {
1306 let (start, mut prior_char) = loop {
1308 break match self.chars.next() {
1309 None => {
1311 if self.last_char_was_delim {
1312 self.last_char_was_delim = false;
1313 return Some("");
1314 }
1315
1316 return None;
1317 }
1318 Some((i, c)) if c == Self::DELIM => {
1319 if self.last_char_was_returned_delim {
1324 self.last_char_was_returned_delim = false;
1325 self.last_char_was_delim = true;
1326 continue;
1327 } else if i == 0 && self.chars.peek().is_some_and(|v| v.1 != Self::DELIM) {
1328 self.last_char_was_returned_delim = false;
1331 self.last_char_was_delim = true;
1332 return Some("");
1333 }
1334
1335 self.last_char_was_returned_delim = true;
1336 self.last_char_was_delim = false;
1337 return Some("+");
1338 }
1339 Some(v) => v,
1343 };
1344 };
1345
1346 self.last_char_was_delim = false;
1350
1351 loop {
1352 prior_char = match self.chars.next() {
1353 None => return Some(&self.text[start..]),
1356 Some((end, c)) if c == Self::DELIM && prior_char != Self::DELIM => {
1360 self.last_char_was_delim = true;
1361 return Some(&self.text[start..end]);
1362 }
1363 Some((_, c)) => c,
1365 }
1366 }
1367 }
1368}
1369
1370#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
1372#[serde(try_from = "String")]
1373#[serde(into = "String")]
1374pub struct KeyBinding {
1375 pub key_event: tuievents::KeyEvent,
1376}
1377
1378impl KeyBinding {
1379 pub fn try_from_str(input: &str) -> Result<Self, KeyParseError> {
1383 let input = input.to_lowercase();
1384 let mut modifiers = tuievents::KeyModifiers::empty();
1385 let mut key_opt: Option<tuievents::Key> = None;
1386
1387 for val in SplitAtPlus::new(&input) {
1388 if val.is_empty() {
1390 return Err(KeyParseError::TrailingDelimiter(input));
1391 }
1392
1393 if let Ok(new_key) = KeyWrap::try_from(val) {
1394 let opt: &mut Option<tuievents::Key> = &mut key_opt;
1395 if let Some(existing_key) = opt {
1396 return Err(KeyParseError::MultipleKeys {
1397 input,
1398 old_key: KeyWrap::from(*existing_key).to_string(),
1399 new_key: new_key.to_string(),
1400 });
1401 }
1402
1403 *opt = Some(new_key.0);
1404
1405 continue;
1406 }
1407
1408 if let Ok(new_modifier) = SupportedModifiers::try_from(val) {
1409 modifiers |= new_modifier.into();
1410
1411 continue;
1412 }
1413
1414 return Err(KeyParseError::UnknownKey(val.into()));
1415 }
1416
1417 let Some(mut code) = key_opt else {
1418 return Err(KeyParseError::NoKeyFound(input));
1419 };
1420
1421 if modifiers.intersects(tuievents::KeyModifiers::SHIFT) {
1423 if let tuievents::Key::Char(v) = code {
1424 code = tuievents::Key::Char(v.to_ascii_uppercase());
1425 }
1426 }
1427
1428 Ok(Self {
1429 key_event: tuievents::KeyEvent::new(code, modifiers),
1430 })
1431 }
1432
1433 #[inline]
1435 #[must_use]
1436 pub fn get(&self) -> tuievents::KeyEvent {
1437 self.key_event
1438 }
1439
1440 #[inline]
1442 #[must_use]
1443 pub fn mod_key(&self) -> (tuievents::KeyModifiers, String) {
1444 (
1445 self.key_event.modifiers,
1446 KeyWrap::from(self.key_event.code).to_string(),
1447 )
1448 }
1449}
1450
1451impl Display for KeyBinding {
1452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1454 let key = KeyWrap::from(self.key_event.code);
1455 for res in SupportedModifiers::from_keymodifiers(self.key_event.modifiers)
1456 .into_iter()
1457 .map(Into::<&str>::into)
1458 .map(|v| write!(f, "{v}+"))
1459 {
1460 res?;
1461 }
1462
1463 write!(f, "{key}")
1464 }
1465}
1466
1467impl TryFrom<&str> for KeyBinding {
1468 type Error = KeyParseError;
1469
1470 fn try_from(value: &str) -> Result<Self, Self::Error> {
1471 Self::try_from_str(value)
1472 }
1473}
1474
1475impl TryFrom<String> for KeyBinding {
1476 type Error = KeyParseError;
1477
1478 fn try_from(value: String) -> Result<Self, Self::Error> {
1479 Self::try_from_str(&value)
1480 }
1481}
1482
1483impl From<KeyBinding> for String {
1484 fn from(value: KeyBinding) -> Self {
1485 value.to_string()
1486 }
1487}
1488
1489impl From<KeyWrap> for KeyBinding {
1491 fn from(value: KeyWrap) -> Self {
1492 Self {
1493 key_event: tuievents::KeyEvent::new(value.0, tuievents::KeyModifiers::empty()),
1494 }
1495 }
1496}
1497
1498impl From<tuievents::Key> for KeyBinding {
1500 fn from(value: tuievents::Key) -> Self {
1501 Self::from(KeyWrap(value))
1502 }
1503}
1504
1505impl From<tuievents::KeyEvent> for KeyBinding {
1507 fn from(value: tuievents::KeyEvent) -> Self {
1508 Self { key_event: value }
1509 }
1510}
1511
1512#[derive(Debug, Clone, PartialEq)]
1514pub enum KeyWrapParseError {
1515 Empty,
1516 UnknownKey(String),
1517}
1518
1519#[derive(Debug, PartialEq)]
1521struct KeyWrap(tuievents::Key);
1522
1523mod const_keys {
1525 #[macro_export]
1542 macro_rules! const_str {
1543 (
1544 $(#[$outer:meta])*
1545 $name:ident, $content:expr
1546 ) => {
1547 $(#[$outer])*
1548 pub const $name: &str = $content;
1549 };
1550 (
1551 $(
1552 $(#[$outer:meta])*
1553 $name:ident $content:expr
1554 ),+ $(,)?
1555 ) => {
1556 $(const_str!{ $(#[$outer])* $name, $content })+
1557 }
1558 }
1559
1560 const_str! {
1561 BACKSPACE "backspace",
1562 ENTER "enter",
1563 TAB "tab",
1564 BACKTAB "backtab",
1565 DELETE "delete",
1566 INSERT "insert",
1567 HOME "home",
1568 END "end",
1569 ESCAPE "escape",
1570
1571 PAGEUP "pageup",
1572 PAGEDOWN "pagedown",
1573
1574 ARROWUP "arrowup",
1575 ARROWDOWN "arrowdown",
1576 ARROWLEFT "arrowleft",
1577 ARROWRIGHT "arrowright",
1578
1579 CAPSLOCK "capslock",
1581 SCROLLLOCK "scrolllock",
1582 NUMLOCK "numlock",
1583 PRINTSCREEN "printscreen",
1584 PAUSE "pause",
1586
1587 NULL "null",
1590 MENU "menu",
1592
1593 SPACE "space"
1595 }
1596
1597 const_str! {
1598 CONTROL "control",
1599 ALT "alt",
1600 SHIFT "shift",
1601 }
1602}
1603
1604impl TryFrom<&str> for KeyWrap {
1606 type Error = KeyWrapParseError;
1607
1608 fn try_from(value: &str) -> Result<Self, Self::Error> {
1609 use tuievents::Key as TKey;
1611 if value.is_empty() {
1612 return Err(KeyWrapParseError::Empty);
1613 }
1614
1615 if value.len() == 1 {
1616 return Ok(Self(tuievents::Key::Char(value.chars().next().unwrap())));
1618 }
1619
1620 if value.len() <= 4 {
1622 if let Some(val) = value.strip_prefix('f') {
1623 if let Ok(parsed) = val.parse::<u8>() {
1624 return Ok(Self(tuievents::Key::Function(parsed)));
1626 }
1627 }
1629 }
1630
1631 let ret = match value {
1632 const_keys::BACKSPACE => Self(TKey::Backspace),
1633 const_keys::ENTER => Self(TKey::Enter),
1634 const_keys::TAB => Self(TKey::Tab),
1635 const_keys::BACKTAB => Self(TKey::BackTab),
1636 const_keys::DELETE => Self(TKey::Delete),
1637 const_keys::INSERT => Self(TKey::Insert),
1638 const_keys::HOME => Self(TKey::Home),
1639 const_keys::END => Self(TKey::End),
1640 const_keys::ESCAPE => Self(TKey::Esc),
1641
1642 const_keys::PAGEUP => Self(TKey::PageUp),
1643 const_keys::PAGEDOWN => Self(TKey::PageDown),
1644
1645 const_keys::ARROWUP => Self(TKey::Up),
1646 const_keys::ARROWDOWN => Self(TKey::Down),
1647 const_keys::ARROWLEFT => Self(TKey::Left),
1648 const_keys::ARROWRIGHT => Self(TKey::Right),
1649
1650 const_keys::CAPSLOCK => Self(TKey::CapsLock),
1651 const_keys::SCROLLLOCK => Self(TKey::ScrollLock),
1652 const_keys::NUMLOCK => Self(TKey::NumLock),
1653 const_keys::PRINTSCREEN => Self(TKey::PrintScreen),
1654 const_keys::PAUSE => Self(TKey::Pause),
1655
1656 const_keys::NULL => Self(TKey::Null),
1657 const_keys::MENU => Self(TKey::Menu),
1658
1659 const_keys::SPACE => Self(TKey::Char(' ')),
1661
1662 v => return Err(KeyWrapParseError::UnknownKey(v.to_owned())),
1663 };
1664
1665 Ok(ret)
1666 }
1667}
1668
1669impl Display for KeyWrap {
1670 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1671 match self.0 {
1672 tuievents::Key::Backspace => const_keys::BACKSPACE.fmt(f),
1673 tuievents::Key::Enter => const_keys::ENTER.fmt(f),
1674 tuievents::Key::Tab => const_keys::TAB.fmt(f),
1675 tuievents::Key::BackTab => const_keys::BACKTAB.fmt(f),
1676 tuievents::Key::Delete => const_keys::DELETE.fmt(f),
1677 tuievents::Key::Insert => const_keys::INSERT.fmt(f),
1678 tuievents::Key::Home => const_keys::HOME.fmt(f),
1679 tuievents::Key::End => const_keys::END.fmt(f),
1680 tuievents::Key::Esc => const_keys::ESCAPE.fmt(f),
1681
1682 tuievents::Key::PageUp => const_keys::PAGEUP.fmt(f),
1683 tuievents::Key::PageDown => const_keys::PAGEDOWN.fmt(f),
1684
1685 tuievents::Key::Up => const_keys::ARROWUP.fmt(f),
1686 tuievents::Key::Down => const_keys::ARROWDOWN.fmt(f),
1687 tuievents::Key::Left => const_keys::ARROWLEFT.fmt(f),
1688 tuievents::Key::Right => const_keys::ARROWRIGHT.fmt(f),
1689
1690 tuievents::Key::CapsLock => const_keys::CAPSLOCK.fmt(f),
1691 tuievents::Key::ScrollLock => const_keys::SCROLLLOCK.fmt(f),
1692 tuievents::Key::NumLock => const_keys::NUMLOCK.fmt(f),
1693 tuievents::Key::PrintScreen => const_keys::PRINTSCREEN.fmt(f),
1694 tuievents::Key::Pause => const_keys::PAUSE.fmt(f),
1695
1696 tuievents::Key::Null => const_keys::NULL.fmt(f),
1697 tuievents::Key::Menu => const_keys::MENU.fmt(f),
1698
1699 tuievents::Key::Function(v) => write!(f, "f{v}"),
1700 tuievents::Key::Char(v) => {
1701 if v == ' ' {
1702 write!(f, "{}", const_keys::SPACE)
1703 } else {
1704 v.fmt(f)
1705 }
1706 }
1707
1708 tuievents::Key::Media(_) => unimplemented!(),
1710
1711 tuievents::Key::KeypadBegin => unimplemented!(),
1713
1714 tuievents::Key::ShiftLeft
1716 | tuievents::Key::AltLeft
1717 | tuievents::Key::CtrlLeft
1718 | tuievents::Key::ShiftRight
1719 | tuievents::Key::AltRight
1720 | tuievents::Key::CtrlRight
1721 | tuievents::Key::ShiftUp
1722 | tuievents::Key::AltUp
1723 | tuievents::Key::CtrlUp
1724 | tuievents::Key::ShiftDown
1725 | tuievents::Key::AltDown
1726 | tuievents::Key::CtrlDown
1727 | tuievents::Key::CtrlHome
1728 | tuievents::Key::CtrlEnd => unimplemented!(),
1729 }
1730 }
1731}
1732
1733impl From<tuievents::Key> for KeyWrap {
1735 fn from(value: tuievents::Key) -> Self {
1736 Self(value)
1737 }
1738}
1739
1740#[derive(Debug, Clone, Copy )]
1744enum SupportedModifiers {
1745 Control,
1746 Shift,
1747 Alt,
1748}
1749
1750impl From<SupportedModifiers> for &'static str {
1751 fn from(value: SupportedModifiers) -> Self {
1752 match value {
1753 SupportedModifiers::Control => const_keys::CONTROL,
1754 SupportedModifiers::Shift => const_keys::SHIFT,
1755 SupportedModifiers::Alt => const_keys::ALT,
1756 }
1757 }
1758}
1759
1760impl TryFrom<&str> for SupportedModifiers {
1762 type Error = KeyWrapParseError;
1763
1764 fn try_from(value: &str) -> Result<Self, Self::Error> {
1765 if value.is_empty() {
1766 return Err(KeyWrapParseError::Empty);
1767 }
1768
1769 let val = match value {
1770 const_keys::CONTROL => Self::Control,
1771 const_keys::ALT => Self::Alt,
1772 const_keys::SHIFT => Self::Shift,
1773 v => return Err(KeyWrapParseError::UnknownKey(v.to_owned())),
1774 };
1775
1776 Ok(val)
1777 }
1778}
1779
1780impl SupportedModifiers {
1781 fn from_keymodifiers(modifiers: tuievents::KeyModifiers) -> Vec<Self> {
1783 let mut ret = Vec::with_capacity(3);
1784
1785 if modifiers.contains(tuievents::KeyModifiers::CONTROL) {
1786 ret.push(Self::Control);
1787 }
1788 if modifiers.contains(tuievents::KeyModifiers::ALT) {
1789 ret.push(Self::Alt);
1790 }
1791 if modifiers.contains(tuievents::KeyModifiers::SHIFT) {
1792 ret.push(Self::Shift);
1793 }
1794
1795 ret
1796 }
1797}
1798
1799impl From<SupportedModifiers> for tuievents::KeyModifiers {
1800 fn from(value: SupportedModifiers) -> Self {
1801 match value {
1802 SupportedModifiers::Control => Self::CONTROL,
1803 SupportedModifiers::Shift => Self::SHIFT,
1804 SupportedModifiers::Alt => Self::ALT,
1805 }
1806 }
1807}
1808
1809mod v1_interop {
1810 use super::{
1811 tuievents, KeyBinding, Keys, KeysConfigEditor, KeysDatabase, KeysLibrary, KeysLyric,
1812 KeysMoveCoverArt, KeysNavigation, KeysPlayer, KeysPlaylist, KeysPodcast, KeysSelectView,
1813 };
1814 use crate::config::v1;
1815
1816 impl From<v1::BindingForEvent> for KeyBinding {
1817 fn from(value: v1::BindingForEvent) -> Self {
1818 let code = if let tuievents::Key::Char(char) = value.code {
1819 if value.modifier.intersects(tuievents::KeyModifiers::SHIFT) {
1821 tuievents::Key::Char(char.to_ascii_uppercase())
1822 } else {
1823 tuievents::Key::Char(char.to_ascii_lowercase())
1824 }
1825 } else {
1826 value.code
1827 };
1828 Self::from(tuievents::KeyEvent {
1829 code,
1830 modifiers: value.modifier,
1831 })
1832 }
1833 }
1834
1835 impl From<v1::Keys> for Keys {
1836 #[allow(clippy::too_many_lines)]
1837 fn from(value: v1::Keys) -> Self {
1838 let podcast_delete_feed_key =
1840 if value.podcast_episode_download == value.podcast_delete_feed {
1841 KeysPodcast::default().delete_feed
1842 } else {
1843 value.podcast_delete_feed.into()
1844 };
1845 let podcast_delete_delete_all_eq = match (
1846 value.podcast_delete_all_feeds.code,
1847 value.podcast_delete_feed.code,
1848 ) {
1849 (tuievents::Key::Char(left), tuievents::Key::Char(right)) => {
1851 left.eq_ignore_ascii_case(&right)
1852 }
1853 (left, right) => left == right,
1854 };
1855 let podcast_delete_all_feeds_key = if value.podcast_episode_download
1856 == value.podcast_delete_feed
1857 && podcast_delete_delete_all_eq
1858 {
1859 KeysPodcast::default().delete_all_feeds
1860 } else {
1861 value.podcast_delete_all_feeds.into()
1862 };
1863 let podcast_delete_episode_key = if podcast_delete_feed_key
1865 == value.podcast_episode_delete_file.key_event().into()
1866 {
1867 KeysPodcast::default().delete_local_episode
1868 } else {
1869 value.podcast_episode_delete_file.into()
1870 };
1871
1872 let player_volume_down_key = {
1875 let old = value.global_player_volume_minus_2;
1876 if old.code == tuievents::Key::Char('_')
1877 && old.modifier.intersects(tuievents::KeyModifiers::SHIFT)
1878 {
1879 KeyBinding::from(tuievents::KeyEvent::new(
1880 tuievents::Key::Char('_'),
1881 tuievents::KeyModifiers::NONE,
1882 ))
1883 } else {
1884 old.into()
1885 }
1886 };
1887
1888 Self {
1889 escape: value.global_esc.into(),
1890 quit: value.global_quit.into(),
1891 select_view_keys: KeysSelectView {
1892 view_library: value.global_layout_treeview.into(),
1893 view_database: value.global_layout_database.into(),
1894 view_podcasts: value.global_layout_podcast.into(),
1895 open_config: value.global_config_open.into(),
1896 open_help: value.global_help.into(),
1897 },
1898 navigation_keys: KeysNavigation {
1899 up: value.global_up.into(),
1900 down: value.global_down.into(),
1901 left: value.global_left.into(),
1902 right: value.global_right.into(),
1903 goto_top: value.global_goto_top.into(),
1904 goto_bottom: value.global_goto_bottom.into(),
1905 },
1906 player_keys: KeysPlayer {
1907 toggle_pause: value.global_player_toggle_pause.into(),
1908 next_track: value.global_player_next.into(),
1909 previous_track: value.global_player_previous.into(),
1910 volume_up: value.global_player_volume_plus_2.into(),
1911 volume_down: player_volume_down_key,
1912 seek_forward: value.global_player_seek_forward.into(),
1913 seek_backward: value.global_player_seek_backward.into(),
1914 speed_up: value.global_player_speed_up.into(),
1915 speed_down: value.global_player_speed_down.into(),
1916 toggle_prefetch: value.global_player_toggle_gapless.into(),
1917 save_playlist: value.global_save_playlist.into(),
1918 },
1919 lyric_keys: KeysLyric {
1920 adjust_offset_forwards: value.global_lyric_adjust_forward.into(),
1921 adjust_offset_backwards: value.global_lyric_adjust_backward.into(),
1922 cycle_frames: value.global_lyric_cycle.into(),
1923 },
1924 library_keys: KeysLibrary {
1925 load_track: value.global_right.into(),
1927 load_dir: value.library_load_dir.into(),
1928 delete: value.library_delete.into(),
1929 yank: value.library_yank.into(),
1930 paste: value.library_paste.into(),
1931 cycle_root: value.library_switch_root.into(),
1932 add_root: value.library_add_root.into(),
1933 remove_root: value.library_remove_root.into(),
1934 search: value.library_search.into(),
1935 youtube_search: value.library_search_youtube.into(),
1936 open_tag_editor: value.library_tag_editor_open.into(),
1937 },
1938 playlist_keys: KeysPlaylist {
1939 delete: value.playlist_delete.into(),
1940 delete_all: value.playlist_delete_all.into(),
1941 shuffle: value.playlist_shuffle.into(),
1942 cycle_loop_mode: value.playlist_mode_cycle.into(),
1943 play_selected: value.playlist_play_selected.into(),
1944 search: value.playlist_search.into(),
1945 swap_up: value.playlist_swap_up.into(),
1946 swap_down: value.playlist_swap_down.into(),
1947 add_random_songs: value.playlist_add_random_tracks.into(),
1948 add_random_album: value.playlist_add_random_album.into(),
1949 },
1950 database_keys: KeysDatabase {
1951 add_selected: value.global_right.into(),
1953 add_all: value.database_add_all.into(),
1954 },
1955 podcast_keys: KeysPodcast {
1956 search: value.podcast_search_add_feed.into(),
1957 mark_played: value.podcast_mark_played.into(),
1958 mark_all_played: value.podcast_mark_all_played.into(),
1959 refresh_feed: value.podcast_refresh_feed.into(),
1960 refresh_all_feeds: value.podcast_refresh_all_feeds.into(),
1961 download_episode: value.podcast_episode_download.into(),
1962 delete_local_episode: podcast_delete_episode_key,
1963 delete_feed: podcast_delete_feed_key,
1964 delete_all_feeds: podcast_delete_all_feeds_key,
1965 },
1966 move_cover_art_keys: KeysMoveCoverArt {
1967 move_left: value.global_xywh_move_left.into(),
1968 move_right: value.global_xywh_move_right.into(),
1969 move_up: value.global_xywh_move_up.into(),
1970 move_down: value.global_xywh_move_down.into(),
1971 increase_size: value.global_xywh_zoom_in.into(),
1972 decrease_size: value.global_xywh_zoom_out.into(),
1973 toggle_hide: value.global_xywh_hide.into(),
1974 },
1975 config_keys: KeysConfigEditor {
1976 save: value.config_save.into(),
1977 },
1978 }
1979 }
1980 }
1981
1982 #[cfg(test)]
1983 mod test {
1984 use super::*;
1985 use pretty_assertions::assert_eq;
1986 use v1::BindingForEvent;
1987
1988 #[allow(clippy::too_many_lines)] #[test]
1990 fn should_convert_default_without_error() {
1991 let converted: Keys = v1::Keys::default().into();
1992
1993 let expected_select_view_keys = KeysSelectView {
1995 view_library: tuievents::Key::Char('1').into(),
1996 view_database: tuievents::Key::Char('2').into(),
1997 view_podcasts: tuievents::Key::Char('3').into(),
1998 open_config: tuievents::KeyEvent::new(
1999 tuievents::Key::Char('C'),
2000 tuievents::KeyModifiers::SHIFT,
2001 )
2002 .into(),
2003 open_help: tuievents::KeyEvent::new(
2004 tuievents::Key::Char('h'),
2005 tuievents::KeyModifiers::CONTROL,
2006 )
2007 .into(),
2008 };
2009 assert_eq!(converted.select_view_keys, expected_select_view_keys);
2010
2011 let expected_navigation_keys = KeysNavigation {
2012 up: tuievents::Key::Char('k').into(),
2013 down: tuievents::Key::Char('j').into(),
2014 left: tuievents::Key::Char('h').into(),
2015 right: tuievents::Key::Char('l').into(),
2016 goto_top: tuievents::Key::Char('g').into(),
2017 goto_bottom: tuievents::KeyEvent::new(
2018 tuievents::Key::Char('G'),
2019 tuievents::KeyModifiers::SHIFT,
2020 )
2021 .into(),
2022 };
2023 assert_eq!(converted.navigation_keys, expected_navigation_keys);
2024
2025 let expected_player_keys = KeysPlayer {
2026 toggle_pause: tuievents::Key::Char(' ').into(),
2027 next_track: tuievents::Key::Char('n').into(),
2028 previous_track: tuievents::KeyEvent::new(
2029 tuievents::Key::Char('N'),
2030 tuievents::KeyModifiers::SHIFT,
2031 )
2032 .into(),
2033 volume_up: tuievents::KeyEvent::new(
2035 tuievents::Key::Char('='),
2036 tuievents::KeyModifiers::NONE,
2037 )
2038 .into(),
2039 volume_down: tuievents::KeyEvent::new(
2040 tuievents::Key::Char('_'),
2041 tuievents::KeyModifiers::NONE,
2042 )
2043 .into(),
2044 seek_forward: tuievents::Key::Char('f').into(),
2045 seek_backward: tuievents::Key::Char('b').into(),
2046 speed_up: tuievents::KeyEvent::new(
2047 tuievents::Key::Char('f'),
2048 tuievents::KeyModifiers::CONTROL,
2049 )
2050 .into(),
2051 speed_down: tuievents::KeyEvent::new(
2052 tuievents::Key::Char('b'),
2053 tuievents::KeyModifiers::CONTROL,
2054 )
2055 .into(),
2056 toggle_prefetch: tuievents::KeyEvent::new(
2057 tuievents::Key::Char('g'),
2058 tuievents::KeyModifiers::CONTROL,
2059 )
2060 .into(),
2061 save_playlist: tuievents::KeyEvent::new(
2062 tuievents::Key::Char('s'),
2063 tuievents::KeyModifiers::CONTROL,
2064 )
2065 .into(),
2066 };
2067 assert_eq!(converted.player_keys, expected_player_keys);
2068
2069 let expected_lyric_keys = KeysLyric {
2070 adjust_offset_forwards: tuievents::KeyEvent::new(
2071 tuievents::Key::Char('F'),
2072 tuievents::KeyModifiers::SHIFT,
2073 )
2074 .into(),
2075 adjust_offset_backwards: tuievents::KeyEvent::new(
2076 tuievents::Key::Char('B'),
2077 tuievents::KeyModifiers::SHIFT,
2078 )
2079 .into(),
2080 cycle_frames: tuievents::KeyEvent::new(
2081 tuievents::Key::Char('T'),
2082 tuievents::KeyModifiers::SHIFT,
2083 )
2084 .into(),
2085 };
2086 assert_eq!(converted.lyric_keys, expected_lyric_keys);
2087
2088 let expected_library_keys = KeysLibrary {
2089 load_track: tuievents::Key::Char('l').into(),
2090 load_dir: tuievents::KeyEvent::new(
2091 tuievents::Key::Char('L'),
2092 tuievents::KeyModifiers::SHIFT,
2093 )
2094 .into(),
2095 delete: tuievents::Key::Char('d').into(),
2096 yank: tuievents::Key::Char('y').into(),
2097 paste: tuievents::Key::Char('p').into(),
2098 cycle_root: tuievents::Key::Char('o').into(),
2099 add_root: tuievents::Key::Char('a').into(),
2100 remove_root: tuievents::KeyEvent::new(
2101 tuievents::Key::Char('A'),
2102 tuievents::KeyModifiers::SHIFT,
2103 )
2104 .into(),
2105 search: tuievents::Key::Char('/').into(),
2106 youtube_search: tuievents::Key::Char('s').into(),
2107 open_tag_editor: tuievents::Key::Char('t').into(),
2108 };
2109 assert_eq!(converted.library_keys, expected_library_keys);
2110
2111 let expected_playlist_keys = KeysPlaylist {
2112 delete: tuievents::Key::Char('d').into(),
2113 delete_all: tuievents::KeyEvent::new(
2114 tuievents::Key::Char('D'),
2115 tuievents::KeyModifiers::SHIFT,
2116 )
2117 .into(),
2118 shuffle: tuievents::Key::Char('r').into(),
2119 cycle_loop_mode: tuievents::Key::Char('m').into(),
2120 play_selected: tuievents::Key::Char('l').into(),
2121 search: tuievents::Key::Char('/').into(),
2122 swap_up: tuievents::KeyEvent::new(
2123 tuievents::Key::Char('K'),
2124 tuievents::KeyModifiers::SHIFT,
2125 )
2126 .into(),
2127 swap_down: tuievents::KeyEvent::new(
2128 tuievents::Key::Char('J'),
2129 tuievents::KeyModifiers::SHIFT,
2130 )
2131 .into(),
2132 add_random_songs: tuievents::Key::Char('s').into(),
2133 add_random_album: tuievents::KeyEvent::new(
2134 tuievents::Key::Char('S'),
2135 tuievents::KeyModifiers::SHIFT,
2136 )
2137 .into(),
2138 };
2139 assert_eq!(converted.playlist_keys, expected_playlist_keys);
2140
2141 let expected_database_keys = KeysDatabase {
2142 add_selected: tuievents::Key::Char('l').into(),
2143 add_all: tuievents::KeyEvent::new(
2144 tuievents::Key::Char('L'),
2145 tuievents::KeyModifiers::SHIFT,
2146 )
2147 .into(),
2148 };
2149 assert_eq!(converted.database_keys, expected_database_keys);
2150
2151 let expected_podcast_keys = KeysPodcast {
2152 search: tuievents::Key::Char('s').into(),
2153 mark_played: tuievents::Key::Char('m').into(),
2154 mark_all_played: tuievents::KeyEvent::new(
2155 tuievents::Key::Char('M'),
2156 tuievents::KeyModifiers::SHIFT,
2157 )
2158 .into(),
2159 refresh_feed: tuievents::Key::Char('r').into(),
2160 refresh_all_feeds: tuievents::KeyEvent::new(
2161 tuievents::Key::Char('R'),
2162 tuievents::KeyModifiers::SHIFT,
2163 )
2164 .into(),
2165 download_episode: tuievents::Key::Char('d').into(),
2166 delete_local_episode: tuievents::KeyEvent::new(
2167 tuievents::Key::Char('D'),
2168 tuievents::KeyModifiers::SHIFT,
2169 )
2170 .into(),
2171 delete_feed: tuievents::Key::Char('x').into(),
2172 delete_all_feeds: tuievents::KeyEvent::new(
2173 tuievents::Key::Char('X'),
2174 tuievents::KeyModifiers::SHIFT,
2175 )
2176 .into(),
2177 };
2178 assert_eq!(converted.podcast_keys, expected_podcast_keys);
2179
2180 let expected_move_cover_art_keys = KeysMoveCoverArt {
2181 move_left: tuievents::KeyEvent::new(
2182 tuievents::Key::Left,
2183 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2184 )
2185 .into(),
2186 move_right: tuievents::KeyEvent::new(
2187 tuievents::Key::Right,
2188 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2189 )
2190 .into(),
2191 move_up: tuievents::KeyEvent::new(
2192 tuievents::Key::Up,
2193 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2194 )
2195 .into(),
2196 move_down: tuievents::KeyEvent::new(
2197 tuievents::Key::Down,
2198 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2199 )
2200 .into(),
2201 increase_size: tuievents::KeyEvent::new(
2202 tuievents::Key::PageUp,
2203 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2204 )
2205 .into(),
2206 decrease_size: tuievents::KeyEvent::new(
2207 tuievents::Key::PageDown,
2208 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2209 )
2210 .into(),
2211 toggle_hide: tuievents::KeyEvent::new(
2212 tuievents::Key::End,
2213 tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2214 )
2215 .into(),
2216 };
2217 assert_eq!(converted.move_cover_art_keys, expected_move_cover_art_keys);
2218
2219 let expected_config_editor_keys = KeysConfigEditor {
2220 save: tuievents::KeyEvent::new(
2221 tuievents::Key::Char('s'),
2222 tuievents::KeyModifiers::CONTROL,
2223 )
2224 .into(),
2225 };
2226 assert_eq!(converted.config_keys, expected_config_editor_keys);
2227
2228 let expected_keys = Keys {
2229 escape: tuievents::Key::Esc.into(),
2230 quit: tuievents::Key::Char('q').into(),
2231 select_view_keys: expected_select_view_keys,
2232 navigation_keys: expected_navigation_keys,
2233 player_keys: expected_player_keys,
2234 lyric_keys: expected_lyric_keys,
2235 library_keys: expected_library_keys,
2236 playlist_keys: expected_playlist_keys,
2237 database_keys: expected_database_keys,
2238 podcast_keys: expected_podcast_keys,
2239 move_cover_art_keys: expected_move_cover_art_keys,
2240 config_keys: expected_config_editor_keys,
2241 };
2242
2243 assert_eq!(converted, expected_keys);
2244
2245 assert_eq!(Ok(()), expected_keys.check_keys());
2246 }
2247
2248 #[test]
2249 fn should_fixup_old_volume_default() {
2250 let converted: Keys = {
2251 let v1 = v1::Keys {
2252 global_player_volume_minus_2: BindingForEvent {
2253 code: tuievents::Key::Char('_'),
2254 modifier: tuievents::KeyModifiers::SHIFT,
2255 },
2256 ..v1::Keys::default()
2257 };
2258
2259 v1.into()
2260 };
2261
2262 let expected_player_keys = KeysPlayer {
2263 toggle_pause: tuievents::Key::Char(' ').into(),
2264 next_track: tuievents::Key::Char('n').into(),
2265 previous_track: tuievents::KeyEvent::new(
2266 tuievents::Key::Char('N'),
2267 tuievents::KeyModifiers::SHIFT,
2268 )
2269 .into(),
2270 volume_up: tuievents::KeyEvent::new(
2272 tuievents::Key::Char('='),
2273 tuievents::KeyModifiers::NONE,
2274 )
2275 .into(),
2276 volume_down: tuievents::KeyEvent::new(
2277 tuievents::Key::Char('_'),
2278 tuievents::KeyModifiers::NONE,
2279 )
2280 .into(),
2281 seek_forward: tuievents::Key::Char('f').into(),
2282 seek_backward: tuievents::Key::Char('b').into(),
2283 speed_up: tuievents::KeyEvent::new(
2284 tuievents::Key::Char('f'),
2285 tuievents::KeyModifiers::CONTROL,
2286 )
2287 .into(),
2288 speed_down: tuievents::KeyEvent::new(
2289 tuievents::Key::Char('b'),
2290 tuievents::KeyModifiers::CONTROL,
2291 )
2292 .into(),
2293 toggle_prefetch: tuievents::KeyEvent::new(
2294 tuievents::Key::Char('g'),
2295 tuievents::KeyModifiers::CONTROL,
2296 )
2297 .into(),
2298 save_playlist: tuievents::KeyEvent::new(
2299 tuievents::Key::Char('s'),
2300 tuievents::KeyModifiers::CONTROL,
2301 )
2302 .into(),
2303 };
2304 assert_eq!(converted.player_keys, expected_player_keys);
2305 }
2306 }
2307}
2308
2309#[cfg(test)]
2310mod test {
2311 use super::*;
2312
2313 mod split_at_plus {
2314 use super::*;
2315 use pretty_assertions::assert_eq;
2316
2317 #[test]
2318 fn should_do_nothing_at_empty() {
2319 assert_eq!(
2320 Vec::<&str>::new(),
2321 SplitAtPlus::new("").collect::<Vec<&str>>()
2322 );
2323 }
2324
2325 #[test]
2326 fn should_treat_one_as_key() {
2327 assert_eq!(vec!["+"], SplitAtPlus::new("+").collect::<Vec<&str>>());
2328 }
2329
2330 #[test]
2331 fn should_parse_with_non_delim_last() {
2332 assert_eq!(
2333 vec!["+", "control"],
2334 SplitAtPlus::new("++control").collect::<Vec<&str>>()
2335 );
2336 }
2337
2338 #[test]
2339 fn should_parse_with_non_delim_first() {
2340 assert_eq!(
2341 vec!["control", "+"],
2342 SplitAtPlus::new("control++").collect::<Vec<&str>>()
2343 );
2344 }
2345
2346 #[test]
2347 fn should_parse_with_multiple_with_delim() {
2348 assert_eq!(
2349 vec!["+", "+"],
2350 SplitAtPlus::new("+++").collect::<Vec<&str>>()
2351 );
2352 }
2353
2354 #[test]
2355 fn should_parse_with_only_delim() {
2356 assert_eq!(
2357 vec!["q", "control"],
2358 SplitAtPlus::new("q+control").collect::<Vec<&str>>()
2359 );
2360 }
2361
2362 #[test]
2363 fn should_treat_without_delim() {
2364 assert_eq!(
2365 vec!["control"],
2366 SplitAtPlus::new("control").collect::<Vec<&str>>()
2367 );
2368 }
2369
2370 #[test]
2371 fn should_return_trailing_empty_string_on_delim_last() {
2372 assert_eq!(vec!["+", ""], SplitAtPlus::new("++").collect::<Vec<&str>>());
2373 assert_eq!(
2374 vec!["control", ""],
2375 SplitAtPlus::new("control+").collect::<Vec<&str>>()
2376 );
2377 }
2378
2379 #[test]
2380 fn should_parse_non_delim_delim_non_delim() {
2381 assert_eq!(
2382 vec!["control", "+", "shift"],
2383 SplitAtPlus::new("control+++shift").collect::<Vec<&str>>()
2384 );
2385 }
2386
2387 #[test]
2388 fn should_treat_delim_followed_by_key_as_trailing() {
2389 assert_eq!(vec!["", "q"], SplitAtPlus::new("+q").collect::<Vec<&str>>());
2390 }
2391 }
2392
2393 mod key_wrap {
2394 use super::*;
2395 use pretty_assertions::assert_eq;
2396
2397 #[test]
2398 fn should_parse_function_keys() {
2399 assert_eq!(
2400 KeyWrap(tuievents::Key::Function(10)),
2401 KeyWrap::try_from("f10").unwrap()
2402 );
2403 assert_eq!(
2404 KeyWrap(tuievents::Key::Function(0)),
2405 KeyWrap::try_from("f0").unwrap()
2406 );
2407 assert_eq!(
2408 KeyWrap(tuievents::Key::Function(255)),
2409 KeyWrap::try_from("f255").unwrap()
2410 );
2411 }
2412
2413 #[test]
2414 fn should_parse_char() {
2415 assert_eq!(
2416 KeyWrap(tuievents::Key::Char('q')),
2417 KeyWrap::try_from("q").unwrap()
2418 );
2419 assert_eq!(
2420 KeyWrap(tuievents::Key::Char('w')),
2421 KeyWrap::try_from("w").unwrap()
2422 );
2423 assert_eq!(
2424 KeyWrap(tuievents::Key::Char('.')),
2425 KeyWrap::try_from(".").unwrap()
2426 );
2427 assert_eq!(
2428 KeyWrap(tuievents::Key::Char('@')),
2429 KeyWrap::try_from("@").unwrap()
2430 );
2431
2432 assert_eq!(
2434 KeyWrap(tuievents::Key::Char(' ')),
2435 KeyWrap::try_from("space").unwrap()
2436 );
2437 }
2438
2439 #[test]
2440 fn should_serialize_function_keys() {
2441 assert_eq!(&"f10", &KeyWrap(tuievents::Key::Function(10)).to_string());
2442 assert_eq!(&"f0", &KeyWrap(tuievents::Key::Function(0)).to_string());
2443 assert_eq!(&"f255", &KeyWrap(tuievents::Key::Function(255)).to_string());
2444 }
2445
2446 #[test]
2447 fn should_serialize_char() {
2448 assert_eq!(&"q", &KeyWrap(tuievents::Key::Char('q')).to_string());
2449 assert_eq!(&"w", &KeyWrap(tuievents::Key::Char('w')).to_string());
2450 assert_eq!(&".", &KeyWrap(tuievents::Key::Char('.')).to_string());
2451 assert_eq!(&"@", &KeyWrap(tuievents::Key::Char('@')).to_string());
2452
2453 assert_eq!(&"space", &KeyWrap(tuievents::Key::Char(' ')).to_string());
2455 }
2456 }
2457
2458 mod key_binding {
2459 use super::*;
2460 use pretty_assertions::assert_eq;
2461
2462 #[test]
2463 fn should_parse_keys_simple() {
2464 assert_eq!(
2466 KeyBinding::from(tuievents::KeyEvent::new(
2467 tuievents::Key::Char('Q'),
2468 tuievents::KeyModifiers::all()
2469 )),
2470 KeyBinding::try_from("CONTROL+ALT+SHIFT+Q").unwrap()
2471 );
2472
2473 assert_eq!(
2475 KeyBinding::from(tuievents::KeyEvent::new(
2476 tuievents::Key::Char('q'),
2477 tuievents::KeyModifiers::empty()
2478 )),
2479 KeyBinding::try_from("Q").unwrap()
2480 );
2481
2482 assert_eq!(
2484 KeyBinding::from(tuievents::KeyEvent::new(
2485 tuievents::Key::Char('q'),
2486 tuievents::KeyModifiers::CONTROL
2487 )),
2488 KeyBinding::try_from("CONTROL+CONTROL+CONTROL+Q").unwrap()
2489 );
2490 }
2491
2492 #[test]
2493 fn should_error_on_multiple_keys() {
2494 assert_eq!(
2495 Err(KeyParseError::MultipleKeys {
2496 input: "q+s".to_string(),
2497 old_key: "q".to_string(),
2498 new_key: "s".to_string()
2499 }),
2500 KeyBinding::try_from("Q+S")
2501 );
2502 }
2503
2504 #[test]
2505 fn should_serialize() {
2506 assert_eq!(
2508 "control+alt+shift+q",
2509 KeyBinding::from(tuievents::KeyEvent::new(
2510 tuievents::Key::Char('q'),
2511 tuievents::KeyModifiers::all()
2512 ))
2513 .to_string()
2514 );
2515
2516 assert_eq!(
2518 "control+q",
2519 KeyBinding::from(tuievents::KeyEvent::new(
2520 tuievents::Key::Char('q'),
2521 tuievents::KeyModifiers::CONTROL
2522 ))
2523 .to_string()
2524 );
2525
2526 assert_eq!(
2528 "alt+q",
2529 KeyBinding::from(tuievents::KeyEvent::new(
2530 tuievents::Key::Char('q'),
2531 tuievents::KeyModifiers::ALT
2532 ))
2533 .to_string()
2534 );
2535
2536 assert_eq!(
2538 "shift+q",
2539 KeyBinding::from(tuievents::KeyEvent::new(
2540 tuievents::Key::Char('q'),
2541 tuievents::KeyModifiers::SHIFT
2542 ))
2543 .to_string()
2544 );
2545
2546 assert_eq!(
2548 "q",
2549 KeyBinding::from(tuievents::KeyEvent::new(
2550 tuievents::Key::Char('q'),
2551 tuievents::KeyModifiers::empty()
2552 ))
2553 .to_string()
2554 );
2555 }
2556
2557 #[test]
2558 fn should_allow_special_keys() {
2559 assert_eq!(
2561 KeyBinding::from(tuievents::KeyEvent::new(
2562 tuievents::Key::Char('+'),
2563 tuievents::KeyModifiers::empty()
2564 )),
2565 KeyBinding::try_from("+").unwrap()
2566 );
2567
2568 assert_eq!(
2570 KeyBinding::from(tuievents::KeyEvent::new(
2571 tuievents::Key::Char('-'),
2572 tuievents::KeyModifiers::empty()
2573 )),
2574 KeyBinding::try_from("-").unwrap()
2575 );
2576
2577 assert_eq!(
2578 KeyBinding::from(tuievents::KeyEvent::new(
2579 tuievents::Key::Char(' '),
2580 tuievents::KeyModifiers::empty()
2581 )),
2582 KeyBinding::try_from(" ").unwrap()
2583 );
2584 }
2585
2586 #[test]
2587 fn should_not_allow_invalid_formats() {
2588 assert_eq!(
2590 Err(KeyParseError::NoKeyFound(String::new())),
2591 KeyBinding::try_from("")
2592 );
2593
2594 assert_eq!(
2596 Err(KeyParseError::UnknownKey(" ".to_owned())),
2597 KeyBinding::try_from(" ")
2598 );
2599
2600 assert_eq!(
2602 Err(KeyParseError::TrailingDelimiter("++".to_owned())),
2603 KeyBinding::try_from("++")
2604 );
2605
2606 assert_eq!(
2608 Err(KeyParseError::TrailingDelimiter("control+".to_owned())),
2609 KeyBinding::try_from("control+")
2610 );
2611
2612 assert_eq!(
2614 Err(KeyParseError::TrailingDelimiter("+control".to_owned())),
2615 KeyBinding::try_from("+control")
2616 );
2617 }
2618 }
2619
2620 mod keys {
2621 use figment::{
2622 providers::{Format, Toml},
2623 Figment,
2624 };
2625 use pretty_assertions::assert_eq;
2626
2627 use super::*;
2628
2629 #[test]
2630 fn should_parse_default_keys() {
2631 let serialized = toml::to_string(&Keys::default()).unwrap();
2632
2633 let parsed: Keys = Figment::new()
2634 .merge(Toml::string(&serialized))
2635 .extract()
2636 .unwrap();
2637
2638 assert_eq!(Keys::default(), parsed);
2639 }
2640
2641 #[test]
2642 fn should_not_conflict_on_default() {
2643 assert_eq!(Ok(()), Keys::default().check_keys());
2644 }
2645
2646 #[test]
2647 fn should_not_conflict_on_different_view() {
2648 let mut keys = Keys::default();
2650 keys.library_keys.delete = tuievents::Key::Delete.into();
2651 keys.podcast_keys.delete_feed = tuievents::Key::Delete.into();
2652
2653 assert_eq!(Ok(()), keys.check_keys());
2654 }
2655
2656 #[test]
2657 fn should_err_on_global_key_conflict() {
2658 let mut keys = Keys::default();
2660 keys.select_view_keys.view_podcasts = tuievents::Key::Delete.into();
2661 keys.podcast_keys.delete_feed = tuievents::Key::Delete.into();
2662
2663 assert_eq!(
2664 Err(KeysCheckError {
2665 errored_keys: vec![KeyConflictError {
2666 key_path_first: "keys.view.view_podcasts".into(),
2667 key_path_second: "keys.podcast.delete_feed".into(),
2668 key: tuievents::Key::Delete.into()
2669 }]
2670 }),
2671 keys.check_keys()
2672 );
2673 }
2674 }
2675}