termusiclib/config/v2/tui/keys/
mod.rs

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)] // allow missing fields and fill them with the `..Self::default()` in this struct
51pub struct Keys {
52    // -- Escape controls --
53    /// Key to escape / close a layer (like closing a popup); never quits
54    ///
55    /// Global (applies everywhere, except text-input for Char's)
56    pub escape: KeyBinding,
57    /// Key to quit the application, also acts as "escape" if there are layers to quit
58    ///
59    /// Global (applies everywhere, except text-input for Char's)
60    pub quit: KeyBinding,
61
62    // -- Specifically grouped --
63    #[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    /// Check all the keys if they conflict with each-other
87    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        // add & check direct keys, that are global everywhere
132        for (key, path) in self.iter() {
133            // check global first
134            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        // -------------
157        // lets do all the views first that dont rely on global player keys
158        let init_len = global_keys.len(); // sanity check
159        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        // key_path.push("tag_editor");
165        // if let Err(new) = self.tag_editor.check_conflict(key_path, global_keys) {
166        //     conflicts.extend(new);
167        // }
168        // key_path.pop();
169
170        assert_eq!(global_keys.len(), init_len); // sanity check, the above should not have added global_keys
171
172        // -------------
173        // now lets do all the ones that add global player keys
174        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        // -------------
199        // now lets do all the ones that do not add any global player keys, but need to be checked against those
200        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        // -------------
222        if !conflicts.is_empty() {
223            return Err(conflicts);
224        }
225
226        Ok(())
227    }
228}
229
230/// Global keys to open global views
231#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
232#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
233pub struct KeysSelectView {
234    /// Key to switch to the Music-Library view
235    pub view_library: KeyBinding,
236    /// Key to switch to the Database view
237    pub view_database: KeyBinding,
238    /// Key to switch to the Podcast view
239    pub view_podcasts: KeyBinding,
240
241    /// Key to open the Config view
242    pub open_config: KeyBinding,
243    /// Key to open the Help-Popup
244    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            // check global first
289            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/// Global Player controls
320#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
321#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
322pub struct KeysPlayer {
323    /// Key to toggle Play/Pause
324    ///
325    /// Will only apply in specific widgets (like the Playlist, but not in Config)
326    pub toggle_pause: KeyBinding,
327    /// Key to change to the next track
328    ///
329    /// Will only apply in specific widgets (like the Playlist, but not in Config)
330    pub next_track: KeyBinding,
331    /// Key to change to the previous track
332    ///
333    /// Will only apply in specific widgets (like the Playlist, but not in Config)
334    pub previous_track: KeyBinding,
335    /// Key to increase volume (by a set amount)
336    ///
337    /// Will only apply in specific widgets (like the Playlist, but not in Config)
338    pub volume_up: KeyBinding,
339    /// Key to decrease volume (by a set amount)
340    ///
341    /// Will only apply in specific widgets (like the Playlist, but not in Config)
342    pub volume_down: KeyBinding,
343    /// Key to seek forwards (by a set amount)
344    ///
345    /// Will only apply in specific widgets (like the Playlist, but not in Config)
346    pub seek_forward: KeyBinding,
347    /// Key to seek backwards (by a set amount)
348    ///
349    /// Will only apply in specific widgets (like the Playlist, but not in Config)
350    pub seek_backward: KeyBinding,
351    /// Key to increase speed (by a set amount)
352    ///
353    /// Will only apply in specific widgets (like the Playlist, but not in Config)
354    pub speed_up: KeyBinding,
355    /// Key to decrease speed (by a set amount)
356    ///
357    /// Will only apply in specific widgets (like the Playlist, but not in Config)
358    pub speed_down: KeyBinding,
359    /// Key to toggle if track-prefetching should be enabled
360    ///
361    /// Will only apply in specific widgets (like the Playlist, but not in Config)
362    // TODO: always enable "gapless" in rusty backend and rename option to "prefetch"
363    pub toggle_prefetch: KeyBinding,
364
365    /// Key to save the current playlist as a "m3u" playlist
366    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            // check global first
435            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/// Global Lyric adjustment keys
466#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
467#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
468pub struct KeysLyric {
469    /// Key to adjust lyric offset forwards (by a set amount)
470    ///
471    /// Will only apply in specific widgets (like the Playlist, but not in Config)
472    pub adjust_offset_forwards: KeyBinding,
473    /// Key to adjust lyric offset backwards (by a set amount)
474    ///
475    /// Will only apply in specific widgets (like the Playlist, but not in Config)
476    pub adjust_offset_backwards: KeyBinding,
477    /// Key to cycle through multiple lyric frames
478    ///
479    /// Will only apply in specific widgets (like the Playlist, but not in Config)
480    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            // check global first
524            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/// Extra navigation keys (like vim keylayout)
555#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
556#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
557pub struct KeysNavigation {
558    // Note: Arrow-keys will always correspond to this
559    /// Key to navigate upwards (like in a list)
560    pub up: KeyBinding,
561    /// Key to navigate downwards (like in a list)
562    pub down: KeyBinding,
563    /// Key to navigate left (like closing a node in the music library)
564    pub left: KeyBinding,
565    /// Key to navigate right (like opening a node in the music library)
566    pub right: KeyBinding,
567    /// Key to navigate to the top (like in a list)
568    pub goto_top: KeyBinding,
569    /// Key to navigate to the bottom (like in a list)
570    pub goto_bottom: KeyBinding,
571}
572
573impl Default for KeysNavigation {
574    fn default() -> Self {
575        // using vim-like navigation
576        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            // check global first
613            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)] // allow missing fields and fill them with the `..Self::default()` in this struct
644pub struct KeysLibrary {
645    /// Key to load the currently selected track (only if on a file node)
646    pub load_track: KeyBinding,
647    /// Key to load the whole directory (only if on a directory node)
648    pub load_dir: KeyBinding,
649    /// Key to delete the currently selected node (which can be both a track or a directory)
650    pub delete: KeyBinding,
651    /// Key to start moving a node to another (requires "paste" to finish move)
652    pub yank: KeyBinding,
653    /// Key to finish moving a node (requires "yank" to start a move)
654    pub paste: KeyBinding,
655    /// Key to cycle through the Music-Directories
656    pub cycle_root: KeyBinding,
657    /// Key to add the currently entered node as a music root
658    pub add_root: KeyBinding,
659    /// Key to remove the currently entered node as music root
660    pub remove_root: KeyBinding,
661
662    /// Key to open local search (root being the selected `music_dir` root)
663    pub search: KeyBinding,
664    /// Key to open youtube search
665    pub youtube_search: KeyBinding,
666    /// Key to open the tag editor on that node (only works for files)
667    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            // check global first
724            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)] // allow missing fields and fill them with the `..Self::default()` in this struct
755pub struct KeysPlaylist {
756    /// Key to delete the currently selected node from the playlist
757    pub delete: KeyBinding,
758    /// Key to clear the playlist of all tracks
759    pub delete_all: KeyBinding,
760    /// Key to shuffle the playlist with all currently added tracks
761    pub shuffle: KeyBinding,
762    /// Key to cycle through the Loop-modes, see [`LoopMode`](super::super::server::LoopMode)
763    pub cycle_loop_mode: KeyBinding,
764    /// Key to play the currently selected node
765    pub play_selected: KeyBinding,
766    /// Key to open playlist search (searches through the songs currently in the playlist)
767    pub search: KeyBinding,
768    /// Key to swap currently selected track with the node above it
769    pub swap_up: KeyBinding,
770    /// Key to swap currently selected track with the node below it
771    pub swap_down: KeyBinding,
772
773    /// Key to add random songs to the playlist (a set amount)
774    ///
775    /// previously known as `cmus_tqueue`
776    pub add_random_songs: KeyBinding,
777    /// Key to add a random Album to the playlist
778    ///
779    /// previously known as `cmus_lqueue`
780    // NOTE: currently this can be somewhat broken sometimes, cause unknown
781    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            // check global first
844            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)] // allow missing fields and fill them with the `..Self::default()` in this struct
875pub struct KeysPodcast {
876    /// Key to open the search for new feeds
877    pub search: KeyBinding,
878    /// Key to mark the currently selected podcast episode as "played"
879    pub mark_played: KeyBinding,
880    /// Key to mark all episodes in the current podcast as "played"
881    pub mark_all_played: KeyBinding,
882    /// Key to refresh the currently selected feed
883    pub refresh_feed: KeyBinding,
884    /// Key to refresh all added feeds
885    pub refresh_all_feeds: KeyBinding,
886    /// Key to download the currently selected episode
887    pub download_episode: KeyBinding,
888    /// Key to delete the downloaded local file of the currently selected episode
889    pub delete_local_episode: KeyBinding,
890    /// Key to delete the currently selected feed
891    pub delete_feed: KeyBinding,
892    /// Key to delete all the added feeds
893    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            // check global first
953            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/// Keys to manipulate the Cover-Art position
983#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
984#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
985pub struct KeysMoveCoverArt {
986    /// Key to move the album cover to the left (by a set amount)
987    pub move_left: KeyBinding,
988    /// Key to move the album cover to the right (by a set amount)
989    pub move_right: KeyBinding,
990    /// Key to move the album cover up (by a set amount)
991    pub move_up: KeyBinding,
992    /// Key to move the album cover down (by a set amount)
993    pub move_down: KeyBinding,
994
995    /// Key to increase the cover-art size (by a set amount)
996    pub increase_size: KeyBinding,
997    /// Key to decrease the cover-art size (by a set amount)
998    pub decrease_size: KeyBinding,
999
1000    /// Key to toggle whether the Cover-Art is or not
1001    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            // check global first
1071            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/// Keys for the config editor
1102#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1103#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
1104pub struct KeysConfigEditor {
1105    /// Save the config to disk
1106    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            // check global first
1138            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/// Keys for the database view
1168#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1169#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
1170pub struct KeysDatabase {
1171    /// Add the currently selected track to the playlist
1172    pub add_selected: KeyBinding,
1173    /// Add all tracks in the Database view "Tracks" section
1174    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            // check global first
1207            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// TODO: upgrade errors with what config-key has errored
1237/// Error for when [`KeyBinding`] parsing fails
1238#[derive(Debug, Clone, PartialEq, thiserror::Error)]
1239pub enum KeyParseError {
1240    /// Error when either the string is empty, or only has modifiers.
1241    ///
1242    /// Listing (`key_bind`)
1243    #[error("Failed to parse Key because no key was found in the mapping, input: {0:#?}")]
1244    NoKeyFound(String),
1245    /// The Key shortcut was formatted incorrectly (like "++" or "+control")
1246    ///
1247    /// Listing (`key_bind`)
1248    #[error("Failed to parse Key because of a trailing delimiter in input: {0:#?}")]
1249    TrailingDelimiter(String),
1250    /// Error when multiple keys are found (like "Q+E")
1251    ///
1252    /// Listing (`key_bind`, (`old_key`, `new_key`))
1253    #[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 when a unknown value is found (a value that could not be parsed as a key or modifier)
1262    ///
1263    /// Example being a value that is not 1 length, starts with "f" and has numbers following or is a match against [`const_keys`].
1264    /// like `"    "`
1265    ///
1266    /// Listing (`key_bind`)
1267    #[error("Failed to parse Key because of unknown key in mapping: {0:#?}")]
1268    UnknownKey(String),
1269}
1270
1271// Note: this could likely be optimized / improved when the std patters becomes available (to match "".split('')), see https://github.com/rust-lang/rust/issues/27721
1272/// A [`str::split`] replacement that works similar to `str::split(_, '+')`, but can also return the delimiter if directly followed
1273/// like `"control++"` separates it into `["control", "+"]`.
1274#[derive(Debug)]
1275struct SplitAtPlus<'a> {
1276    text: &'a str,
1277    chars: Peekable<CharIndices<'a>>,
1278    /// Track if the previous character was [`Self::DELIM`] but returned as a character across "self.next" calls
1279    last_char_was_returned_delim: bool,
1280    /// Tracker that indicates that the last char was a [`Self::DELIM`] and is used to return a trailing empty-string.
1281    ///
1282    /// For example this is wanted so that we can return a `InvalidFormat` in the actual use-case of this split type.
1283    ///
1284    /// Examples:
1285    /// - `"++"` -> `["+", ""]`
1286    /// - `"q+"` -> `["q", ""]`
1287    last_char_was_delim: bool,
1288}
1289
1290impl<'a> SplitAtPlus<'a> {
1291    /// The Delimiter used in this custom split
1292    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        // loop until a start position can be found (should loop at most 2 times)
1309        let (start, mut prior_char) = loop {
1310            break match self.chars.next() {
1311                // pass-on if there is nothing to return anymore
1312                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                    // return a "+" if not yet encountered, like:
1322                    // in "++control" count the first plus as a key and the second as a delimiter
1323                    // in "+++" count the first plus as a key, the second as a delimiter and the third as a key again
1324                    // in "control++" where we are at the iteration after "control+" and at the last "+"
1325                    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                        // special case where the delimiter is the first, but not followed by another delimiter, like "+q"
1331                        // this is so we return a InvalidFormat later on (treat the first "+" as a delimiter instead of a key)
1332                        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                // not a delimiter, so just pass it as the start
1342                // this case is for example "q+control" where "q" is the first character
1343                // or "control++"
1344                Some(v) => v,
1345            };
1346        };
1347
1348        // the following should never need to be set, as "last_char_was_returned_delim" will only get set in the case above
1349        // and down below consumed by the "chars.next" call
1350        // self.last_char_was_returned_delim = false;
1351        self.last_char_was_delim = false;
1352
1353        loop {
1354            prior_char = match self.chars.next() {
1355                // if there is no next char, return the string from the start point as there is also no delimiter
1356                // example "q+control" where this iteration is past the "q+" and at "control"
1357                None => return Some(&self.text[start..]),
1358                // we have run into a delimiter, so return all the text since then
1359                // like the first plus in "q+control"
1360                // also note that "chars.next()" consumes the delimiter character and so will not be returned in the next "self.next" call
1361                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                // use this new char as the last_char and repeat the loop as we have not hit the end or a delimiter yet
1366                Some((_, c)) => c,
1367            }
1368        }
1369    }
1370}
1371
1372/// Wrapper around the stored Key-Event to use custom de- and serialization
1373#[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    /// Parse a Key with modifiers from a given string.
1382    ///
1383    /// Multiple same-modifiers are counted as one, and multiple keys are a error
1384    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            // make a trailing "+" as a error, like "q+"
1391            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        // transform the key to be upper-case if "Shift" is enabled, as that is what tuirealm will provide (and we cannot modify that)
1424        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    /// Get the inner key
1436    #[inline]
1437    #[must_use]
1438    pub fn get(&self) -> tuievents::KeyEvent {
1439        self.key_event
1440    }
1441
1442    /// Get the Current Modifier, and the string representation of the key
1443    #[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    /// Get a string from the current instance in the format of modifiers+key like "control+alt+shift+q", all lowercase
1455    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
1491/// Simple implementation to easily convert a key without modifiers to one
1492impl 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
1500// convenience convertion for easier construction
1501impl From<tuievents::Key> for KeyBinding {
1502    fn from(value: tuievents::Key) -> Self {
1503        Self::from(KeyWrap(value))
1504    }
1505}
1506
1507// convenience convertion for easier construction
1508impl From<tuievents::KeyEvent> for KeyBinding {
1509    fn from(value: tuievents::KeyEvent) -> Self {
1510        Self { key_event: value }
1511    }
1512}
1513
1514/// Error for when [`KeyWrap`] parsing fails
1515#[derive(Debug, Clone, PartialEq)]
1516enum KeyWrapParseError {
1517    Empty,
1518    UnknownKey(String),
1519}
1520
1521/// Wrapper to parse and serialize a key in a defined format
1522#[derive(Debug, PartialEq)]
1523struct KeyWrap(tuievents::Key);
1524
1525/// Module for defining key string in one place, instead of multiple times in multiple places
1526pub mod const_keys {
1527    /// Macro to not repeat yourself writing `const IDENT: &str = CONTENT`
1528    ///
1529    /// Allows usage of calling one at a time:
1530    ///
1531    /// ```
1532    /// const_str!(NAME, "STRING")
1533    /// ```
1534    ///
1535    /// or multiple at a time to even save repeated "`const_str`!" invokations:
1536    ///
1537    /// ```
1538    /// const_str! {
1539    ///     NAME1 "STRING",
1540    ///     NAME2 "STRING",
1541    /// }
1542    /// ```
1543    #[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        // special keys
1582        CAPSLOCK "capslock",
1583        SCROLLLOCK "scrolllock",
1584        NUMLOCK "numlock",
1585        PRINTSCREEN "printscreen",
1586        /// The "Pause/Break" key, commonly besides "PRINT" and "SCROLLLOCK"
1587        PAUSE "pause",
1588
1589        // weird keys
1590        /// <https://en.wikipedia.org/wiki/Null_character>
1591        NULL "null",
1592        /// <https://en.wikipedia.org/wiki/Menu_key>
1593        MENU "menu",
1594
1595        // aliases
1596        SPACE "space"
1597    }
1598
1599    const_str! {
1600        CONTROL "control",
1601        ALT "alt",
1602        SHIFT "shift",
1603    }
1604}
1605
1606/// This conversion expects the input to already be lowercased
1607impl TryFrom<&str> for KeyWrap {
1608    type Error = KeyWrapParseError;
1609
1610    fn try_from(value: &str) -> Result<Self, Self::Error> {
1611        // simple alias for less code
1612        use tuievents::Key as TKey;
1613        if value.is_empty() {
1614            return Err(KeyWrapParseError::Empty);
1615        }
1616
1617        if value.len() == 1 {
1618            // safe unwrap because we checked the length
1619            return Ok(Self(tuievents::Key::Char(value.chars().next().unwrap())));
1620        }
1621
1622        // yes, this also matches F255
1623        if value.len() <= 4 {
1624            if let Some(val) = value.strip_prefix('f') {
1625                if let Ok(parsed) = val.parse::<u8>() {
1626                    // no number validation as tuirealm seems to not care
1627                    return Ok(Self(tuievents::Key::Function(parsed)));
1628                }
1629                // if parsing fails, just try the other keys, or report "UnknownKey"
1630            }
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            // aliases
1662            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            // not supporting media keys as those are handled by the mpris implementation
1711            tuievents::Key::Media(_) => unimplemented!(),
1712
1713            // i literally have no clue what key this is supposed to be
1714            tuievents::Key::KeypadBegin => unimplemented!(),
1715
1716            // the following are new events with tuirealm 2.0, but only available in backend "termion", which we dont use
1717            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
1735// convenience function to convert
1736impl From<tuievents::Key> for KeyWrap {
1737    fn from(value: tuievents::Key) -> Self {
1738        Self(value)
1739    }
1740}
1741
1742/// All Key-Modifiers we support
1743///
1744/// It is defined here as we want a consistent config and be in control of it instead of some upstream package
1745#[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
1762/// This conversion expects the input to already be lowercased
1763impl 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    /// Get a array of [`SupportedModifiers`] from the provided modifiers
1784    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                // transform the key to be upper-case if "Shift" is enabled, as that is what tuirealm will provide (and we cannot modify that)
1822                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            // extra case because the v1 defaults have conflicting keys
1841            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                // the old impl had lowercase and uppercase characters, need to compare them equally
1852                (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            // need to change it here too because the v1 default is "x", which had been changed to "d+shift" to be a upgrade over "download"
1866            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            // this only really applies to volume_down_1 had "_+SHIFT", but now volume_down_2 is actually used instead
1875            // fixup the old broken way where volume down 1 was by default set to "shift+_", which actually never fires and only fires "_"
1876            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                    // this is weird, but the previous implementation used "global_right" as the loading key to not conflict
1928                    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                    // this is weird, but the previous implementation used "global_right" as the loading key to not conflict
1954                    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)] // this test just requires a lot of fields
1991        #[test]
1992        fn should_convert_default_without_error() {
1993            let converted: Keys = v1::Keys::default().into();
1994
1995            // this is all checked by themself (and then fully) so that if there is a error, you actually get a better error than a bunch of long text
1996            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 and volume_down have different default key-bindings in v2
2036                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 and volume_down have different default key-bindings in v2
2273                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            // space alias
2435            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            // space
2456            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            // all modifiers
2467            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            // no modifiers
2476            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            // multiple of the same modifier
2485            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            // all modifiers
2509            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            // only control
2519            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            // only alt
2529            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            // only shift
2539            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            // no modifiers
2549            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            // we currently split with a delimiter of "+", but it should still be available
2562            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            // just some extra tests
2571            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            // empty string
2591            assert_eq!(
2592                Err(KeyParseError::NoKeyFound(String::new())),
2593                KeyBinding::try_from("")
2594            );
2595
2596            // multiple spaces
2597            assert_eq!(
2598                Err(KeyParseError::UnknownKey("   ".to_owned())),
2599                KeyBinding::try_from("   ")
2600            );
2601
2602            // this could either mean key "+" plus invalid, or invalid plus "+" key
2603            assert_eq!(
2604                Err(KeyParseError::TrailingDelimiter("++".to_owned())),
2605                KeyBinding::try_from("++")
2606            );
2607
2608            // trailing delimiter
2609            assert_eq!(
2610                Err(KeyParseError::TrailingDelimiter("control+".to_owned())),
2611                KeyBinding::try_from("control+")
2612            );
2613
2614            // first trailing delimiter
2615            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            // check that views that would not conflict do not conflict
2651            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            // check that views that would not conflict do not conflict
2661            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}