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.to_string(),
137                    key_path_second: key_path.join_with_field(path),
138                    key: key.clone(),
139                });
140                continue;
141            }
142
143            if let Some(existing_path) = current_keys.get(key) {
144                conflicts.push(KeyConflictError {
145                    key_path_first: key_path.join_with_field(existing_path),
146                    key_path_second: key_path.join_with_field(path),
147                    key: key.clone(),
148                });
149                continue;
150            }
151
152            global_keys.insert(key.clone(), key_path.join_with_field(path));
153            current_keys.insert(key, path);
154        }
155
156        // -------------
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.to_string(),
292                    key_path_second: key_path.join_with_field(path),
293                    key: key.clone(),
294                });
295                continue;
296            }
297
298            if let Some(existing_path) = current_keys.get(key) {
299                conflicts.push(KeyConflictError {
300                    key_path_first: key_path.join_with_field(existing_path),
301                    key_path_second: key_path.join_with_field(path),
302                    key: key.clone(),
303                });
304                continue;
305            }
306
307            global_keys.insert(key.clone(), key_path.join_with_field(path));
308            current_keys.insert(key, path);
309        }
310
311        if !conflicts.is_empty() {
312            return Err(conflicts);
313        }
314
315        Ok(())
316    }
317}
318
319/// 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.to_string(),
438                    key_path_second: key_path.join_with_field(path),
439                    key: key.clone(),
440                });
441                continue;
442            }
443
444            if let Some(existing_path) = current_keys.get(key) {
445                conflicts.push(KeyConflictError {
446                    key_path_first: key_path.join_with_field(existing_path),
447                    key_path_second: key_path.join_with_field(path),
448                    key: key.clone(),
449                });
450                continue;
451            }
452
453            global_keys.insert(key.clone(), key_path.join_with_field(path));
454            current_keys.insert(key, path);
455        }
456
457        if !conflicts.is_empty() {
458            return Err(conflicts);
459        }
460
461        Ok(())
462    }
463}
464
465/// 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.to_string(),
527                    key_path_second: key_path.join_with_field(path),
528                    key: key.clone(),
529                });
530                continue;
531            }
532
533            if let Some(existing_path) = current_keys.get(key) {
534                conflicts.push(KeyConflictError {
535                    key_path_first: key_path.join_with_field(existing_path),
536                    key_path_second: key_path.join_with_field(path),
537                    key: key.clone(),
538                });
539                continue;
540            }
541
542            global_keys.insert(key.clone(), key_path.join_with_field(path));
543            current_keys.insert(key, path);
544        }
545
546        if !conflicts.is_empty() {
547            return Err(conflicts);
548        }
549
550        Ok(())
551    }
552}
553
554/// 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.to_string(),
616                    key_path_second: key_path.join_with_field(path),
617                    key: key.clone(),
618                });
619                continue;
620            }
621
622            if let Some(existing_path) = current_keys.get(key) {
623                conflicts.push(KeyConflictError {
624                    key_path_first: key_path.join_with_field(existing_path),
625                    key_path_second: key_path.join_with_field(path),
626                    key: key.clone(),
627                });
628                continue;
629            }
630
631            current_keys.insert(key, path);
632        }
633
634        if !conflicts.is_empty() {
635            return Err(conflicts);
636        }
637
638        Ok(())
639    }
640}
641
642#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
643#[serde(default)] // 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.to_string(),
727                    key_path_second: key_path.join_with_field(path),
728                    key: key.clone(),
729                });
730                continue;
731            }
732
733            if let Some(existing_path) = current_keys.get(key) {
734                conflicts.push(KeyConflictError {
735                    key_path_first: key_path.join_with_field(existing_path),
736                    key_path_second: key_path.join_with_field(path),
737                    key: key.clone(),
738                });
739                continue;
740            }
741
742            current_keys.insert(key, path);
743        }
744
745        if !conflicts.is_empty() {
746            return Err(conflicts);
747        }
748
749        Ok(())
750    }
751}
752
753#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
754#[serde(default)] // 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.to_string(),
847                    key_path_second: key_path.join_with_field(path),
848                    key: key.clone(),
849                });
850                continue;
851            }
852
853            if let Some(existing_path) = current_keys.get(key) {
854                conflicts.push(KeyConflictError {
855                    key_path_first: key_path.join_with_field(existing_path),
856                    key_path_second: key_path.join_with_field(path),
857                    key: key.clone(),
858                });
859                continue;
860            }
861
862            current_keys.insert(key, path);
863        }
864
865        if !conflicts.is_empty() {
866            return Err(conflicts);
867        }
868
869        Ok(())
870    }
871}
872
873#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
874#[serde(default)] // 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.to_string(),
956                    key_path_second: key_path.join_with_field(path),
957                    key: key.clone(),
958                });
959                continue;
960            }
961
962            if let Some(existing_path) = current_keys.get(key) {
963                conflicts.push(KeyConflictError {
964                    key_path_first: key_path.join_with_field(existing_path),
965                    key_path_second: key_path.join_with_field(path),
966                    key: key.clone(),
967                });
968                continue;
969            }
970
971            current_keys.insert(key, path);
972        }
973
974        if !conflicts.is_empty() {
975            return Err(conflicts);
976        }
977
978        Ok(())
979    }
980}
981
982/// 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.to_string(),
1074                    key_path_second: key_path.join_with_field(path),
1075                    key: key.clone(),
1076                });
1077                continue;
1078            }
1079
1080            if let Some(existing_path) = current_keys.get(key) {
1081                conflicts.push(KeyConflictError {
1082                    key_path_first: key_path.join_with_field(existing_path),
1083                    key_path_second: key_path.join_with_field(path),
1084                    key: key.clone(),
1085                });
1086                continue;
1087            }
1088
1089            global_keys.insert(key.clone(), key_path.join_with_field(path));
1090            current_keys.insert(key, path);
1091        }
1092
1093        if !conflicts.is_empty() {
1094            return Err(conflicts);
1095        }
1096
1097        Ok(())
1098    }
1099}
1100
1101/// 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.to_string(),
1141                    key_path_second: key_path.join_with_field(path),
1142                    key: key.clone(),
1143                });
1144                continue;
1145            }
1146
1147            if let Some(existing_path) = current_keys.get(key) {
1148                conflicts.push(KeyConflictError {
1149                    key_path_first: key_path.join_with_field(existing_path),
1150                    key_path_second: key_path.join_with_field(path),
1151                    key: key.clone(),
1152                });
1153                continue;
1154            }
1155
1156            current_keys.insert(key, path);
1157        }
1158
1159        if !conflicts.is_empty() {
1160            return Err(conflicts);
1161        }
1162
1163        Ok(())
1164    }
1165}
1166
1167/// 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.to_string(),
1210                    key_path_second: key_path.join_with_field(path),
1211                    key: key.clone(),
1212                });
1213                continue;
1214            }
1215
1216            if let Some(existing_path) = current_keys.get(key) {
1217                conflicts.push(KeyConflictError {
1218                    key_path_first: key_path.join_with_field(existing_path),
1219                    key_path_second: key_path.join_with_field(path),
1220                    key: key.clone(),
1221                });
1222                continue;
1223            }
1224
1225            current_keys.insert(key, path);
1226        }
1227
1228        if !conflicts.is_empty() {
1229            return Err(conflicts);
1230        }
1231
1232        Ok(())
1233    }
1234}
1235
1236// TODO: upgrade errors with what config-key has errored
1237/// Error for when [`Key`] 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("Failed to parse Key because multiple non-modifier keys were found, keys: [{old_key}, {new_key}], input: {input:#?}")]
1254    MultipleKeys {
1255        input: String,
1256        old_key: String,
1257        new_key: String,
1258    },
1259    /// Error when a unknown value is found (a value that could not be parsed as a key or modifier)
1260    ///
1261    /// Example being a value that is not 1 length, starts with "f" and has numbers following or is a match against [`const_keys`].
1262    /// like `"    "`
1263    ///
1264    /// Listing (`key_bind`)
1265    #[error("Failed to parse Key because of unknown key in mapping: {0:#?}")]
1266    UnknownKey(String),
1267}
1268
1269// 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
1270/// A [`str::split`] replacement that works similar to `str::split(_, '+')`, but can also return the delimiter if directly followed
1271/// like `"control++"` separates it into `["control", "+"]`.
1272#[derive(Debug)]
1273struct SplitAtPlus<'a> {
1274    text: &'a str,
1275    chars: Peekable<CharIndices<'a>>,
1276    /// Track if the previous character was [`Self::DELIM`] but returned as a character across "self.next" calls
1277    last_char_was_returned_delim: bool,
1278    /// Tracker that indicates that the last char was a [`Self::DELIM`] and is used to return a trailing empty-string.
1279    ///
1280    /// For example this is wanted so that we can return a `InvalidFormat` in the actual use-case of this split type.
1281    ///
1282    /// Examples:
1283    /// - `"++"` -> `["+", ""]`
1284    /// - `"q+"` -> `["q", ""]`
1285    last_char_was_delim: bool,
1286}
1287
1288impl<'a> SplitAtPlus<'a> {
1289    /// The Delimiter used in this custom split
1290    const DELIM: char = '+';
1291
1292    fn new(text: &'a str) -> Self {
1293        Self {
1294            text,
1295            chars: text.char_indices().peekable(),
1296            last_char_was_returned_delim: false,
1297            last_char_was_delim: false,
1298        }
1299    }
1300}
1301
1302impl<'a> Iterator for SplitAtPlus<'a> {
1303    type Item = &'a str;
1304
1305    fn next(&mut self) -> Option<Self::Item> {
1306        // loop until a start position can be found (should loop at most 2 times)
1307        let (start, mut prior_char) = loop {
1308            break match self.chars.next() {
1309                // pass-on if there is nothing to return anymore
1310                None => {
1311                    if self.last_char_was_delim {
1312                        self.last_char_was_delim = false;
1313                        return Some("");
1314                    }
1315
1316                    return None;
1317                }
1318                Some((i, c)) if c == Self::DELIM => {
1319                    // return a "+" if not yet encountered, like:
1320                    // in "++control" count the first plus as a key and the second as a delimiter
1321                    // in "+++" count the first plus as a key, the second as a delimiter and the third as a key again
1322                    // in "control++" where we are at the iteration after "control+" and at the last "+"
1323                    if self.last_char_was_returned_delim {
1324                        self.last_char_was_returned_delim = false;
1325                        self.last_char_was_delim = true;
1326                        continue;
1327                    } else if i == 0 && self.chars.peek().is_some_and(|v| v.1 != Self::DELIM) {
1328                        // special case where the delimiter is the first, but not followed by another delimiter, like "+q"
1329                        // this is so we return a InvalidFormat later on (treat the first "+" as a delimiter instead of a key)
1330                        self.last_char_was_returned_delim = false;
1331                        self.last_char_was_delim = true;
1332                        return Some("");
1333                    }
1334
1335                    self.last_char_was_returned_delim = true;
1336                    self.last_char_was_delim = false;
1337                    return Some("+");
1338                }
1339                // not a delimiter, so just pass it as the start
1340                // this case is for example "q+control" where "q" is the first character
1341                // or "control++"
1342                Some(v) => v,
1343            };
1344        };
1345
1346        // the following should never need to be set, as "last_char_was_returned_delim" will only get set in the case above
1347        // and down below consumed by the "chars.next" call
1348        // self.last_char_was_returned_delim = false;
1349        self.last_char_was_delim = false;
1350
1351        loop {
1352            prior_char = match self.chars.next() {
1353                // if there is no next char, return the string from the start point as there is also no delimiter
1354                // example "q+control" where this iteration is past the "q+" and at "control"
1355                None => return Some(&self.text[start..]),
1356                // we have run into a delimiter, so return all the text since then
1357                // like the first plus in "q+control"
1358                // also note that "chars.next()" consumes the delimiter character and so will not be returned in the next "self.next" call
1359                Some((end, c)) if c == Self::DELIM && prior_char != Self::DELIM => {
1360                    self.last_char_was_delim = true;
1361                    return Some(&self.text[start..end]);
1362                }
1363                // use this new char as the last_char and repeat the loop as we have not hit the end or a delimiter yet
1364                Some((_, c)) => c,
1365            }
1366        }
1367    }
1368}
1369
1370/// Wrapper around the stored Key-Event to use custom de- and serialization
1371#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
1372#[serde(try_from = "String")]
1373#[serde(into = "String")]
1374pub struct KeyBinding {
1375    pub key_event: tuievents::KeyEvent,
1376}
1377
1378impl KeyBinding {
1379    /// Parse a Key with modifiers from a given string.
1380    ///
1381    /// Multiple same-modifiers are counted as one, and multiple keys are a error
1382    pub fn try_from_str(input: &str) -> Result<Self, KeyParseError> {
1383        let input = input.to_lowercase();
1384        let mut modifiers = tuievents::KeyModifiers::empty();
1385        let mut key_opt: Option<tuievents::Key> = None;
1386
1387        for val in SplitAtPlus::new(&input) {
1388            // make a trailing "+" as a error, like "q+"
1389            if val.is_empty() {
1390                return Err(KeyParseError::TrailingDelimiter(input));
1391            }
1392
1393            if let Ok(new_key) = KeyWrap::try_from(val) {
1394                let opt: &mut Option<tuievents::Key> = &mut key_opt;
1395                if let Some(existing_key) = opt {
1396                    return Err(KeyParseError::MultipleKeys {
1397                        input,
1398                        old_key: KeyWrap::from(*existing_key).to_string(),
1399                        new_key: new_key.to_string(),
1400                    });
1401                }
1402
1403                *opt = Some(new_key.0);
1404
1405                continue;
1406            }
1407
1408            if let Ok(new_modifier) = SupportedModifiers::try_from(val) {
1409                modifiers |= new_modifier.into();
1410
1411                continue;
1412            }
1413
1414            return Err(KeyParseError::UnknownKey(val.into()));
1415        }
1416
1417        let Some(mut code) = key_opt else {
1418            return Err(KeyParseError::NoKeyFound(input));
1419        };
1420
1421        // transform the key to be upper-case if "Shift" is enabled, as that is what tuirealm will provide (and we cannot modify that)
1422        if modifiers.intersects(tuievents::KeyModifiers::SHIFT) {
1423            if let tuievents::Key::Char(v) = code {
1424                code = tuievents::Key::Char(v.to_ascii_uppercase());
1425            }
1426        }
1427
1428        Ok(Self {
1429            key_event: tuievents::KeyEvent::new(code, modifiers),
1430        })
1431    }
1432
1433    /// Get the inner key
1434    #[inline]
1435    #[must_use]
1436    pub fn get(&self) -> tuievents::KeyEvent {
1437        self.key_event
1438    }
1439
1440    /// Get the Current Modifier, and the string representation of the key
1441    #[inline]
1442    #[must_use]
1443    pub fn mod_key(&self) -> (tuievents::KeyModifiers, String) {
1444        (
1445            self.key_event.modifiers,
1446            KeyWrap::from(self.key_event.code).to_string(),
1447        )
1448    }
1449}
1450
1451impl Display for KeyBinding {
1452    /// Get a string from the current instance in the format of modifiers+key like "control+alt+shift+q", all lowercase
1453    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1454        let key = KeyWrap::from(self.key_event.code);
1455        for res in SupportedModifiers::from_keymodifiers(self.key_event.modifiers)
1456            .into_iter()
1457            .map(Into::<&str>::into)
1458            .map(|v| write!(f, "{v}+"))
1459        {
1460            res?;
1461        }
1462
1463        write!(f, "{key}")
1464    }
1465}
1466
1467impl TryFrom<&str> for KeyBinding {
1468    type Error = KeyParseError;
1469
1470    fn try_from(value: &str) -> Result<Self, Self::Error> {
1471        Self::try_from_str(value)
1472    }
1473}
1474
1475impl TryFrom<String> for KeyBinding {
1476    type Error = KeyParseError;
1477
1478    fn try_from(value: String) -> Result<Self, Self::Error> {
1479        Self::try_from_str(&value)
1480    }
1481}
1482
1483impl From<KeyBinding> for String {
1484    fn from(value: KeyBinding) -> Self {
1485        value.to_string()
1486    }
1487}
1488
1489/// Simple implementation to easily convert a key without modifiers to one
1490impl From<KeyWrap> for KeyBinding {
1491    fn from(value: KeyWrap) -> Self {
1492        Self {
1493            key_event: tuievents::KeyEvent::new(value.0, tuievents::KeyModifiers::empty()),
1494        }
1495    }
1496}
1497
1498// convenience convertion for easier construction
1499impl From<tuievents::Key> for KeyBinding {
1500    fn from(value: tuievents::Key) -> Self {
1501        Self::from(KeyWrap(value))
1502    }
1503}
1504
1505// convenience convertion for easier construction
1506impl From<tuievents::KeyEvent> for KeyBinding {
1507    fn from(value: tuievents::KeyEvent) -> Self {
1508        Self { key_event: value }
1509    }
1510}
1511
1512/// Error for when [`SupportedKeys`] parsing fails
1513#[derive(Debug, Clone, PartialEq)]
1514pub enum KeyWrapParseError {
1515    Empty,
1516    UnknownKey(String),
1517}
1518
1519/// Wrapper to parse and serialize a key in a defined format
1520#[derive(Debug, PartialEq)]
1521struct KeyWrap(tuievents::Key);
1522
1523/// Module for defining key string in one place, instead of multiple times in multiple places
1524mod const_keys {
1525    /// Macro to not repeat yourself writing `const IDENT: &str = CONTENT`
1526    ///
1527    /// Allows usage of calling one at a time:
1528    ///
1529    /// ```
1530    /// const_str!(NAME, "STRING")
1531    /// ```
1532    ///
1533    /// or multiple at a time to even save repeated "`const_str`!" invokations:
1534    ///
1535    /// ```
1536    /// const_str! {
1537    ///     NAME1 "STRING",
1538    ///     NAME2 "STRING",
1539    /// }
1540    /// ```
1541    #[macro_export]
1542    macro_rules! const_str {
1543        (
1544            $(#[$outer:meta])*
1545            $name:ident, $content:expr
1546        ) => {
1547            $(#[$outer])*
1548            pub const $name: &str = $content;
1549        };
1550        (
1551            $(
1552                $(#[$outer:meta])*
1553                $name:ident $content:expr
1554            ),+ $(,)?
1555        ) => {
1556            $(const_str!{ $(#[$outer])* $name, $content })+
1557        }
1558    }
1559
1560    const_str! {
1561        BACKSPACE "backspace",
1562        ENTER "enter",
1563        TAB "tab",
1564        BACKTAB "backtab",
1565        DELETE "delete",
1566        INSERT "insert",
1567        HOME "home",
1568        END "end",
1569        ESCAPE "escape",
1570
1571        PAGEUP "pageup",
1572        PAGEDOWN "pagedown",
1573
1574        ARROWUP "arrowup",
1575        ARROWDOWN "arrowdown",
1576        ARROWLEFT "arrowleft",
1577        ARROWRIGHT "arrowright",
1578
1579        // special keys
1580        CAPSLOCK "capslock",
1581        SCROLLLOCK "scrolllock",
1582        NUMLOCK "numlock",
1583        PRINTSCREEN "printscreen",
1584        /// The "Pause/Break" key, commonly besides "PRINT" and "SCROLLLOCK"
1585        PAUSE "pause",
1586
1587        // weird keys
1588        /// https://en.wikipedia.org/wiki/Null_character
1589        NULL "null",
1590        /// https://en.wikipedia.org/wiki/Menu_key
1591        MENU "menu",
1592
1593        // aliases
1594        SPACE "space"
1595    }
1596
1597    const_str! {
1598        CONTROL "control",
1599        ALT "alt",
1600        SHIFT "shift",
1601    }
1602}
1603
1604/// This conversion expects the input to already be lowercased
1605impl TryFrom<&str> for KeyWrap {
1606    type Error = KeyWrapParseError;
1607
1608    fn try_from(value: &str) -> Result<Self, Self::Error> {
1609        // simple alias for less code
1610        use tuievents::Key as TKey;
1611        if value.is_empty() {
1612            return Err(KeyWrapParseError::Empty);
1613        }
1614
1615        if value.len() == 1 {
1616            // safe unwrap because we checked the length
1617            return Ok(Self(tuievents::Key::Char(value.chars().next().unwrap())));
1618        }
1619
1620        // yes, this also matches F255
1621        if value.len() <= 4 {
1622            if let Some(val) = value.strip_prefix('f') {
1623                if let Ok(parsed) = val.parse::<u8>() {
1624                    // no number validation as tuirealm seems to not care
1625                    return Ok(Self(tuievents::Key::Function(parsed)));
1626                }
1627                // if parsing fails, just try the other keys, or report "UnknownKey"
1628            }
1629        }
1630
1631        let ret = match value {
1632            const_keys::BACKSPACE => Self(TKey::Backspace),
1633            const_keys::ENTER => Self(TKey::Enter),
1634            const_keys::TAB => Self(TKey::Tab),
1635            const_keys::BACKTAB => Self(TKey::BackTab),
1636            const_keys::DELETE => Self(TKey::Delete),
1637            const_keys::INSERT => Self(TKey::Insert),
1638            const_keys::HOME => Self(TKey::Home),
1639            const_keys::END => Self(TKey::End),
1640            const_keys::ESCAPE => Self(TKey::Esc),
1641
1642            const_keys::PAGEUP => Self(TKey::PageUp),
1643            const_keys::PAGEDOWN => Self(TKey::PageDown),
1644
1645            const_keys::ARROWUP => Self(TKey::Up),
1646            const_keys::ARROWDOWN => Self(TKey::Down),
1647            const_keys::ARROWLEFT => Self(TKey::Left),
1648            const_keys::ARROWRIGHT => Self(TKey::Right),
1649
1650            const_keys::CAPSLOCK => Self(TKey::CapsLock),
1651            const_keys::SCROLLLOCK => Self(TKey::ScrollLock),
1652            const_keys::NUMLOCK => Self(TKey::NumLock),
1653            const_keys::PRINTSCREEN => Self(TKey::PrintScreen),
1654            const_keys::PAUSE => Self(TKey::Pause),
1655
1656            const_keys::NULL => Self(TKey::Null),
1657            const_keys::MENU => Self(TKey::Menu),
1658
1659            // aliases
1660            const_keys::SPACE => Self(TKey::Char(' ')),
1661
1662            v => return Err(KeyWrapParseError::UnknownKey(v.to_owned())),
1663        };
1664
1665        Ok(ret)
1666    }
1667}
1668
1669impl Display for KeyWrap {
1670    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1671        match self.0 {
1672            tuievents::Key::Backspace => const_keys::BACKSPACE.fmt(f),
1673            tuievents::Key::Enter => const_keys::ENTER.fmt(f),
1674            tuievents::Key::Tab => const_keys::TAB.fmt(f),
1675            tuievents::Key::BackTab => const_keys::BACKTAB.fmt(f),
1676            tuievents::Key::Delete => const_keys::DELETE.fmt(f),
1677            tuievents::Key::Insert => const_keys::INSERT.fmt(f),
1678            tuievents::Key::Home => const_keys::HOME.fmt(f),
1679            tuievents::Key::End => const_keys::END.fmt(f),
1680            tuievents::Key::Esc => const_keys::ESCAPE.fmt(f),
1681
1682            tuievents::Key::PageUp => const_keys::PAGEUP.fmt(f),
1683            tuievents::Key::PageDown => const_keys::PAGEDOWN.fmt(f),
1684
1685            tuievents::Key::Up => const_keys::ARROWUP.fmt(f),
1686            tuievents::Key::Down => const_keys::ARROWDOWN.fmt(f),
1687            tuievents::Key::Left => const_keys::ARROWLEFT.fmt(f),
1688            tuievents::Key::Right => const_keys::ARROWRIGHT.fmt(f),
1689
1690            tuievents::Key::CapsLock => const_keys::CAPSLOCK.fmt(f),
1691            tuievents::Key::ScrollLock => const_keys::SCROLLLOCK.fmt(f),
1692            tuievents::Key::NumLock => const_keys::NUMLOCK.fmt(f),
1693            tuievents::Key::PrintScreen => const_keys::PRINTSCREEN.fmt(f),
1694            tuievents::Key::Pause => const_keys::PAUSE.fmt(f),
1695
1696            tuievents::Key::Null => const_keys::NULL.fmt(f),
1697            tuievents::Key::Menu => const_keys::MENU.fmt(f),
1698
1699            tuievents::Key::Function(v) => write!(f, "f{v}"),
1700            tuievents::Key::Char(v) => {
1701                if v == ' ' {
1702                    write!(f, "{}", const_keys::SPACE)
1703                } else {
1704                    v.fmt(f)
1705                }
1706            }
1707
1708            // not supporting media keys as those are handled by the mpris implementation
1709            tuievents::Key::Media(_) => unimplemented!(),
1710
1711            // i literally have no clue what key this is supposed to be
1712            tuievents::Key::KeypadBegin => unimplemented!(),
1713
1714            // the following are new events with tuirealm 2.0, but only available in backend "termion", which we dont use
1715            tuievents::Key::ShiftLeft
1716            | tuievents::Key::AltLeft
1717            | tuievents::Key::CtrlLeft
1718            | tuievents::Key::ShiftRight
1719            | tuievents::Key::AltRight
1720            | tuievents::Key::CtrlRight
1721            | tuievents::Key::ShiftUp
1722            | tuievents::Key::AltUp
1723            | tuievents::Key::CtrlUp
1724            | tuievents::Key::ShiftDown
1725            | tuievents::Key::AltDown
1726            | tuievents::Key::CtrlDown
1727            | tuievents::Key::CtrlHome
1728            | tuievents::Key::CtrlEnd => unimplemented!(),
1729        }
1730    }
1731}
1732
1733// convenience function to convert
1734impl From<tuievents::Key> for KeyWrap {
1735    fn from(value: tuievents::Key) -> Self {
1736        Self(value)
1737    }
1738}
1739
1740/// All Key-Modifiers we support
1741///
1742/// It is defined here as we want a consistent config and be in control of it instead of some upstream package
1743#[derive(Debug, Clone, Copy /* , EnumString, IntoStaticStr */)]
1744enum SupportedModifiers {
1745    Control,
1746    Shift,
1747    Alt,
1748}
1749
1750impl From<SupportedModifiers> for &'static str {
1751    fn from(value: SupportedModifiers) -> Self {
1752        match value {
1753            SupportedModifiers::Control => const_keys::CONTROL,
1754            SupportedModifiers::Shift => const_keys::SHIFT,
1755            SupportedModifiers::Alt => const_keys::ALT,
1756        }
1757    }
1758}
1759
1760/// This conversion expects the input to already be lowercased
1761impl TryFrom<&str> for SupportedModifiers {
1762    type Error = KeyWrapParseError;
1763
1764    fn try_from(value: &str) -> Result<Self, Self::Error> {
1765        if value.is_empty() {
1766            return Err(KeyWrapParseError::Empty);
1767        }
1768
1769        let val = match value {
1770            const_keys::CONTROL => Self::Control,
1771            const_keys::ALT => Self::Alt,
1772            const_keys::SHIFT => Self::Shift,
1773            v => return Err(KeyWrapParseError::UnknownKey(v.to_owned())),
1774        };
1775
1776        Ok(val)
1777    }
1778}
1779
1780impl SupportedModifiers {
1781    /// Get a array of [`SupportedModifiers`] from the provided modifiers
1782    fn from_keymodifiers(modifiers: tuievents::KeyModifiers) -> Vec<Self> {
1783        let mut ret = Vec::with_capacity(3);
1784
1785        if modifiers.contains(tuievents::KeyModifiers::CONTROL) {
1786            ret.push(Self::Control);
1787        }
1788        if modifiers.contains(tuievents::KeyModifiers::ALT) {
1789            ret.push(Self::Alt);
1790        }
1791        if modifiers.contains(tuievents::KeyModifiers::SHIFT) {
1792            ret.push(Self::Shift);
1793        }
1794
1795        ret
1796    }
1797}
1798
1799impl From<SupportedModifiers> for tuievents::KeyModifiers {
1800    fn from(value: SupportedModifiers) -> Self {
1801        match value {
1802            SupportedModifiers::Control => Self::CONTROL,
1803            SupportedModifiers::Shift => Self::SHIFT,
1804            SupportedModifiers::Alt => Self::ALT,
1805        }
1806    }
1807}
1808
1809mod v1_interop {
1810    use super::{
1811        tuievents, KeyBinding, Keys, KeysConfigEditor, KeysDatabase, KeysLibrary, KeysLyric,
1812        KeysMoveCoverArt, KeysNavigation, KeysPlayer, KeysPlaylist, KeysPodcast, KeysSelectView,
1813    };
1814    use crate::config::v1;
1815
1816    impl From<v1::BindingForEvent> for KeyBinding {
1817        fn from(value: v1::BindingForEvent) -> Self {
1818            let code = if let tuievents::Key::Char(char) = value.code {
1819                // transform the key to be upper-case if "Shift" is enabled, as that is what tuirealm will provide (and we cannot modify that)
1820                if value.modifier.intersects(tuievents::KeyModifiers::SHIFT) {
1821                    tuievents::Key::Char(char.to_ascii_uppercase())
1822                } else {
1823                    tuievents::Key::Char(char.to_ascii_lowercase())
1824                }
1825            } else {
1826                value.code
1827            };
1828            Self::from(tuievents::KeyEvent {
1829                code,
1830                modifiers: value.modifier,
1831            })
1832        }
1833    }
1834
1835    impl From<v1::Keys> for Keys {
1836        #[allow(clippy::too_many_lines)]
1837        fn from(value: v1::Keys) -> Self {
1838            // extra case because the v1 defaults have conflicting keys
1839            let podcast_delete_feed_key =
1840                if value.podcast_episode_download == value.podcast_delete_feed {
1841                    KeysPodcast::default().delete_feed
1842                } else {
1843                    value.podcast_delete_feed.into()
1844                };
1845            let podcast_delete_delete_all_eq = match (
1846                value.podcast_delete_all_feeds.code,
1847                value.podcast_delete_feed.code,
1848            ) {
1849                // the old impl had lowercase and uppercase characters, need to compare them equally
1850                (tuievents::Key::Char(left), tuievents::Key::Char(right)) => {
1851                    left.eq_ignore_ascii_case(&right)
1852                }
1853                (left, right) => left == right,
1854            };
1855            let podcast_delete_all_feeds_key = if value.podcast_episode_download
1856                == value.podcast_delete_feed
1857                && podcast_delete_delete_all_eq
1858            {
1859                KeysPodcast::default().delete_all_feeds
1860            } else {
1861                value.podcast_delete_all_feeds.into()
1862            };
1863            // 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"
1864            let podcast_delete_episode_key = if podcast_delete_feed_key
1865                == value.podcast_episode_delete_file.key_event().into()
1866            {
1867                KeysPodcast::default().delete_local_episode
1868            } else {
1869                value.podcast_episode_delete_file.into()
1870            };
1871
1872            // this only really applies to volume_down_1 had "_+SHIFT", but now volume_down_2 is actually used instead
1873            // fixup the old broken way where volume down 1 was by default set to "shift+_", which actually never fires and only fires "_"
1874            let player_volume_down_key = {
1875                let old = value.global_player_volume_minus_2;
1876                if old.code == tuievents::Key::Char('_')
1877                    && old.modifier.intersects(tuievents::KeyModifiers::SHIFT)
1878                {
1879                    KeyBinding::from(tuievents::KeyEvent::new(
1880                        tuievents::Key::Char('_'),
1881                        tuievents::KeyModifiers::NONE,
1882                    ))
1883                } else {
1884                    old.into()
1885                }
1886            };
1887
1888            Self {
1889                escape: value.global_esc.into(),
1890                quit: value.global_quit.into(),
1891                select_view_keys: KeysSelectView {
1892                    view_library: value.global_layout_treeview.into(),
1893                    view_database: value.global_layout_database.into(),
1894                    view_podcasts: value.global_layout_podcast.into(),
1895                    open_config: value.global_config_open.into(),
1896                    open_help: value.global_help.into(),
1897                },
1898                navigation_keys: KeysNavigation {
1899                    up: value.global_up.into(),
1900                    down: value.global_down.into(),
1901                    left: value.global_left.into(),
1902                    right: value.global_right.into(),
1903                    goto_top: value.global_goto_top.into(),
1904                    goto_bottom: value.global_goto_bottom.into(),
1905                },
1906                player_keys: KeysPlayer {
1907                    toggle_pause: value.global_player_toggle_pause.into(),
1908                    next_track: value.global_player_next.into(),
1909                    previous_track: value.global_player_previous.into(),
1910                    volume_up: value.global_player_volume_plus_2.into(),
1911                    volume_down: player_volume_down_key,
1912                    seek_forward: value.global_player_seek_forward.into(),
1913                    seek_backward: value.global_player_seek_backward.into(),
1914                    speed_up: value.global_player_speed_up.into(),
1915                    speed_down: value.global_player_speed_down.into(),
1916                    toggle_prefetch: value.global_player_toggle_gapless.into(),
1917                    save_playlist: value.global_save_playlist.into(),
1918                },
1919                lyric_keys: KeysLyric {
1920                    adjust_offset_forwards: value.global_lyric_adjust_forward.into(),
1921                    adjust_offset_backwards: value.global_lyric_adjust_backward.into(),
1922                    cycle_frames: value.global_lyric_cycle.into(),
1923                },
1924                library_keys: KeysLibrary {
1925                    // this is weird, but the previous implementation used "global_right" as the loading key to not conflict
1926                    load_track: value.global_right.into(),
1927                    load_dir: value.library_load_dir.into(),
1928                    delete: value.library_delete.into(),
1929                    yank: value.library_yank.into(),
1930                    paste: value.library_paste.into(),
1931                    cycle_root: value.library_switch_root.into(),
1932                    add_root: value.library_add_root.into(),
1933                    remove_root: value.library_remove_root.into(),
1934                    search: value.library_search.into(),
1935                    youtube_search: value.library_search_youtube.into(),
1936                    open_tag_editor: value.library_tag_editor_open.into(),
1937                },
1938                playlist_keys: KeysPlaylist {
1939                    delete: value.playlist_delete.into(),
1940                    delete_all: value.playlist_delete_all.into(),
1941                    shuffle: value.playlist_shuffle.into(),
1942                    cycle_loop_mode: value.playlist_mode_cycle.into(),
1943                    play_selected: value.playlist_play_selected.into(),
1944                    search: value.playlist_search.into(),
1945                    swap_up: value.playlist_swap_up.into(),
1946                    swap_down: value.playlist_swap_down.into(),
1947                    add_random_songs: value.playlist_add_random_tracks.into(),
1948                    add_random_album: value.playlist_add_random_album.into(),
1949                },
1950                database_keys: KeysDatabase {
1951                    // this is weird, but the previous implementation used "global_right" as the loading key to not conflict
1952                    add_selected: value.global_right.into(),
1953                    add_all: value.database_add_all.into(),
1954                },
1955                podcast_keys: KeysPodcast {
1956                    search: value.podcast_search_add_feed.into(),
1957                    mark_played: value.podcast_mark_played.into(),
1958                    mark_all_played: value.podcast_mark_all_played.into(),
1959                    refresh_feed: value.podcast_refresh_feed.into(),
1960                    refresh_all_feeds: value.podcast_refresh_all_feeds.into(),
1961                    download_episode: value.podcast_episode_download.into(),
1962                    delete_local_episode: podcast_delete_episode_key,
1963                    delete_feed: podcast_delete_feed_key,
1964                    delete_all_feeds: podcast_delete_all_feeds_key,
1965                },
1966                move_cover_art_keys: KeysMoveCoverArt {
1967                    move_left: value.global_xywh_move_left.into(),
1968                    move_right: value.global_xywh_move_right.into(),
1969                    move_up: value.global_xywh_move_up.into(),
1970                    move_down: value.global_xywh_move_down.into(),
1971                    increase_size: value.global_xywh_zoom_in.into(),
1972                    decrease_size: value.global_xywh_zoom_out.into(),
1973                    toggle_hide: value.global_xywh_hide.into(),
1974                },
1975                config_keys: KeysConfigEditor {
1976                    save: value.config_save.into(),
1977                },
1978            }
1979        }
1980    }
1981
1982    #[cfg(test)]
1983    mod test {
1984        use super::*;
1985        use pretty_assertions::assert_eq;
1986        use v1::BindingForEvent;
1987
1988        #[allow(clippy::too_many_lines)] // this test just requires a lot of fields
1989        #[test]
1990        fn should_convert_default_without_error() {
1991            let converted: Keys = v1::Keys::default().into();
1992
1993            // 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
1994            let expected_select_view_keys = KeysSelectView {
1995                view_library: tuievents::Key::Char('1').into(),
1996                view_database: tuievents::Key::Char('2').into(),
1997                view_podcasts: tuievents::Key::Char('3').into(),
1998                open_config: tuievents::KeyEvent::new(
1999                    tuievents::Key::Char('C'),
2000                    tuievents::KeyModifiers::SHIFT,
2001                )
2002                .into(),
2003                open_help: tuievents::KeyEvent::new(
2004                    tuievents::Key::Char('h'),
2005                    tuievents::KeyModifiers::CONTROL,
2006                )
2007                .into(),
2008            };
2009            assert_eq!(converted.select_view_keys, expected_select_view_keys);
2010
2011            let expected_navigation_keys = KeysNavigation {
2012                up: tuievents::Key::Char('k').into(),
2013                down: tuievents::Key::Char('j').into(),
2014                left: tuievents::Key::Char('h').into(),
2015                right: tuievents::Key::Char('l').into(),
2016                goto_top: tuievents::Key::Char('g').into(),
2017                goto_bottom: tuievents::KeyEvent::new(
2018                    tuievents::Key::Char('G'),
2019                    tuievents::KeyModifiers::SHIFT,
2020                )
2021                .into(),
2022            };
2023            assert_eq!(converted.navigation_keys, expected_navigation_keys);
2024
2025            let expected_player_keys = KeysPlayer {
2026                toggle_pause: tuievents::Key::Char(' ').into(),
2027                next_track: tuievents::Key::Char('n').into(),
2028                previous_track: tuievents::KeyEvent::new(
2029                    tuievents::Key::Char('N'),
2030                    tuievents::KeyModifiers::SHIFT,
2031                )
2032                .into(),
2033                // volume_up and volume_down have different default key-bindings in v2
2034                volume_up: tuievents::KeyEvent::new(
2035                    tuievents::Key::Char('='),
2036                    tuievents::KeyModifiers::NONE,
2037                )
2038                .into(),
2039                volume_down: tuievents::KeyEvent::new(
2040                    tuievents::Key::Char('_'),
2041                    tuievents::KeyModifiers::NONE,
2042                )
2043                .into(),
2044                seek_forward: tuievents::Key::Char('f').into(),
2045                seek_backward: tuievents::Key::Char('b').into(),
2046                speed_up: tuievents::KeyEvent::new(
2047                    tuievents::Key::Char('f'),
2048                    tuievents::KeyModifiers::CONTROL,
2049                )
2050                .into(),
2051                speed_down: tuievents::KeyEvent::new(
2052                    tuievents::Key::Char('b'),
2053                    tuievents::KeyModifiers::CONTROL,
2054                )
2055                .into(),
2056                toggle_prefetch: tuievents::KeyEvent::new(
2057                    tuievents::Key::Char('g'),
2058                    tuievents::KeyModifiers::CONTROL,
2059                )
2060                .into(),
2061                save_playlist: tuievents::KeyEvent::new(
2062                    tuievents::Key::Char('s'),
2063                    tuievents::KeyModifiers::CONTROL,
2064                )
2065                .into(),
2066            };
2067            assert_eq!(converted.player_keys, expected_player_keys);
2068
2069            let expected_lyric_keys = KeysLyric {
2070                adjust_offset_forwards: tuievents::KeyEvent::new(
2071                    tuievents::Key::Char('F'),
2072                    tuievents::KeyModifiers::SHIFT,
2073                )
2074                .into(),
2075                adjust_offset_backwards: tuievents::KeyEvent::new(
2076                    tuievents::Key::Char('B'),
2077                    tuievents::KeyModifiers::SHIFT,
2078                )
2079                .into(),
2080                cycle_frames: tuievents::KeyEvent::new(
2081                    tuievents::Key::Char('T'),
2082                    tuievents::KeyModifiers::SHIFT,
2083                )
2084                .into(),
2085            };
2086            assert_eq!(converted.lyric_keys, expected_lyric_keys);
2087
2088            let expected_library_keys = KeysLibrary {
2089                load_track: tuievents::Key::Char('l').into(),
2090                load_dir: tuievents::KeyEvent::new(
2091                    tuievents::Key::Char('L'),
2092                    tuievents::KeyModifiers::SHIFT,
2093                )
2094                .into(),
2095                delete: tuievents::Key::Char('d').into(),
2096                yank: tuievents::Key::Char('y').into(),
2097                paste: tuievents::Key::Char('p').into(),
2098                cycle_root: tuievents::Key::Char('o').into(),
2099                add_root: tuievents::Key::Char('a').into(),
2100                remove_root: tuievents::KeyEvent::new(
2101                    tuievents::Key::Char('A'),
2102                    tuievents::KeyModifiers::SHIFT,
2103                )
2104                .into(),
2105                search: tuievents::Key::Char('/').into(),
2106                youtube_search: tuievents::Key::Char('s').into(),
2107                open_tag_editor: tuievents::Key::Char('t').into(),
2108            };
2109            assert_eq!(converted.library_keys, expected_library_keys);
2110
2111            let expected_playlist_keys = KeysPlaylist {
2112                delete: tuievents::Key::Char('d').into(),
2113                delete_all: tuievents::KeyEvent::new(
2114                    tuievents::Key::Char('D'),
2115                    tuievents::KeyModifiers::SHIFT,
2116                )
2117                .into(),
2118                shuffle: tuievents::Key::Char('r').into(),
2119                cycle_loop_mode: tuievents::Key::Char('m').into(),
2120                play_selected: tuievents::Key::Char('l').into(),
2121                search: tuievents::Key::Char('/').into(),
2122                swap_up: tuievents::KeyEvent::new(
2123                    tuievents::Key::Char('K'),
2124                    tuievents::KeyModifiers::SHIFT,
2125                )
2126                .into(),
2127                swap_down: tuievents::KeyEvent::new(
2128                    tuievents::Key::Char('J'),
2129                    tuievents::KeyModifiers::SHIFT,
2130                )
2131                .into(),
2132                add_random_songs: tuievents::Key::Char('s').into(),
2133                add_random_album: tuievents::KeyEvent::new(
2134                    tuievents::Key::Char('S'),
2135                    tuievents::KeyModifiers::SHIFT,
2136                )
2137                .into(),
2138            };
2139            assert_eq!(converted.playlist_keys, expected_playlist_keys);
2140
2141            let expected_database_keys = KeysDatabase {
2142                add_selected: tuievents::Key::Char('l').into(),
2143                add_all: tuievents::KeyEvent::new(
2144                    tuievents::Key::Char('L'),
2145                    tuievents::KeyModifiers::SHIFT,
2146                )
2147                .into(),
2148            };
2149            assert_eq!(converted.database_keys, expected_database_keys);
2150
2151            let expected_podcast_keys = KeysPodcast {
2152                search: tuievents::Key::Char('s').into(),
2153                mark_played: tuievents::Key::Char('m').into(),
2154                mark_all_played: tuievents::KeyEvent::new(
2155                    tuievents::Key::Char('M'),
2156                    tuievents::KeyModifiers::SHIFT,
2157                )
2158                .into(),
2159                refresh_feed: tuievents::Key::Char('r').into(),
2160                refresh_all_feeds: tuievents::KeyEvent::new(
2161                    tuievents::Key::Char('R'),
2162                    tuievents::KeyModifiers::SHIFT,
2163                )
2164                .into(),
2165                download_episode: tuievents::Key::Char('d').into(),
2166                delete_local_episode: tuievents::KeyEvent::new(
2167                    tuievents::Key::Char('D'),
2168                    tuievents::KeyModifiers::SHIFT,
2169                )
2170                .into(),
2171                delete_feed: tuievents::Key::Char('x').into(),
2172                delete_all_feeds: tuievents::KeyEvent::new(
2173                    tuievents::Key::Char('X'),
2174                    tuievents::KeyModifiers::SHIFT,
2175                )
2176                .into(),
2177            };
2178            assert_eq!(converted.podcast_keys, expected_podcast_keys);
2179
2180            let expected_move_cover_art_keys = KeysMoveCoverArt {
2181                move_left: tuievents::KeyEvent::new(
2182                    tuievents::Key::Left,
2183                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2184                )
2185                .into(),
2186                move_right: tuievents::KeyEvent::new(
2187                    tuievents::Key::Right,
2188                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2189                )
2190                .into(),
2191                move_up: tuievents::KeyEvent::new(
2192                    tuievents::Key::Up,
2193                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2194                )
2195                .into(),
2196                move_down: tuievents::KeyEvent::new(
2197                    tuievents::Key::Down,
2198                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2199                )
2200                .into(),
2201                increase_size: tuievents::KeyEvent::new(
2202                    tuievents::Key::PageUp,
2203                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2204                )
2205                .into(),
2206                decrease_size: tuievents::KeyEvent::new(
2207                    tuievents::Key::PageDown,
2208                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2209                )
2210                .into(),
2211                toggle_hide: tuievents::KeyEvent::new(
2212                    tuievents::Key::End,
2213                    tuievents::KeyModifiers::CONTROL | tuievents::KeyModifiers::SHIFT,
2214                )
2215                .into(),
2216            };
2217            assert_eq!(converted.move_cover_art_keys, expected_move_cover_art_keys);
2218
2219            let expected_config_editor_keys = KeysConfigEditor {
2220                save: tuievents::KeyEvent::new(
2221                    tuievents::Key::Char('s'),
2222                    tuievents::KeyModifiers::CONTROL,
2223                )
2224                .into(),
2225            };
2226            assert_eq!(converted.config_keys, expected_config_editor_keys);
2227
2228            let expected_keys = Keys {
2229                escape: tuievents::Key::Esc.into(),
2230                quit: tuievents::Key::Char('q').into(),
2231                select_view_keys: expected_select_view_keys,
2232                navigation_keys: expected_navigation_keys,
2233                player_keys: expected_player_keys,
2234                lyric_keys: expected_lyric_keys,
2235                library_keys: expected_library_keys,
2236                playlist_keys: expected_playlist_keys,
2237                database_keys: expected_database_keys,
2238                podcast_keys: expected_podcast_keys,
2239                move_cover_art_keys: expected_move_cover_art_keys,
2240                config_keys: expected_config_editor_keys,
2241            };
2242
2243            assert_eq!(converted, expected_keys);
2244
2245            assert_eq!(Ok(()), expected_keys.check_keys());
2246        }
2247
2248        #[test]
2249        fn should_fixup_old_volume_default() {
2250            let converted: Keys = {
2251                let v1 = v1::Keys {
2252                    global_player_volume_minus_2: BindingForEvent {
2253                        code: tuievents::Key::Char('_'),
2254                        modifier: tuievents::KeyModifiers::SHIFT,
2255                    },
2256                    ..v1::Keys::default()
2257                };
2258
2259                v1.into()
2260            };
2261
2262            let expected_player_keys = KeysPlayer {
2263                toggle_pause: tuievents::Key::Char(' ').into(),
2264                next_track: tuievents::Key::Char('n').into(),
2265                previous_track: tuievents::KeyEvent::new(
2266                    tuievents::Key::Char('N'),
2267                    tuievents::KeyModifiers::SHIFT,
2268                )
2269                .into(),
2270                // volume_up and volume_down have different default key-bindings in v2
2271                volume_up: tuievents::KeyEvent::new(
2272                    tuievents::Key::Char('='),
2273                    tuievents::KeyModifiers::NONE,
2274                )
2275                .into(),
2276                volume_down: tuievents::KeyEvent::new(
2277                    tuievents::Key::Char('_'),
2278                    tuievents::KeyModifiers::NONE,
2279                )
2280                .into(),
2281                seek_forward: tuievents::Key::Char('f').into(),
2282                seek_backward: tuievents::Key::Char('b').into(),
2283                speed_up: tuievents::KeyEvent::new(
2284                    tuievents::Key::Char('f'),
2285                    tuievents::KeyModifiers::CONTROL,
2286                )
2287                .into(),
2288                speed_down: tuievents::KeyEvent::new(
2289                    tuievents::Key::Char('b'),
2290                    tuievents::KeyModifiers::CONTROL,
2291                )
2292                .into(),
2293                toggle_prefetch: tuievents::KeyEvent::new(
2294                    tuievents::Key::Char('g'),
2295                    tuievents::KeyModifiers::CONTROL,
2296                )
2297                .into(),
2298                save_playlist: tuievents::KeyEvent::new(
2299                    tuievents::Key::Char('s'),
2300                    tuievents::KeyModifiers::CONTROL,
2301                )
2302                .into(),
2303            };
2304            assert_eq!(converted.player_keys, expected_player_keys);
2305        }
2306    }
2307}
2308
2309#[cfg(test)]
2310mod test {
2311    use super::*;
2312
2313    mod split_at_plus {
2314        use super::*;
2315        use pretty_assertions::assert_eq;
2316
2317        #[test]
2318        fn should_do_nothing_at_empty() {
2319            assert_eq!(
2320                Vec::<&str>::new(),
2321                SplitAtPlus::new("").collect::<Vec<&str>>()
2322            );
2323        }
2324
2325        #[test]
2326        fn should_treat_one_as_key() {
2327            assert_eq!(vec!["+"], SplitAtPlus::new("+").collect::<Vec<&str>>());
2328        }
2329
2330        #[test]
2331        fn should_parse_with_non_delim_last() {
2332            assert_eq!(
2333                vec!["+", "control"],
2334                SplitAtPlus::new("++control").collect::<Vec<&str>>()
2335            );
2336        }
2337
2338        #[test]
2339        fn should_parse_with_non_delim_first() {
2340            assert_eq!(
2341                vec!["control", "+"],
2342                SplitAtPlus::new("control++").collect::<Vec<&str>>()
2343            );
2344        }
2345
2346        #[test]
2347        fn should_parse_with_multiple_with_delim() {
2348            assert_eq!(
2349                vec!["+", "+"],
2350                SplitAtPlus::new("+++").collect::<Vec<&str>>()
2351            );
2352        }
2353
2354        #[test]
2355        fn should_parse_with_only_delim() {
2356            assert_eq!(
2357                vec!["q", "control"],
2358                SplitAtPlus::new("q+control").collect::<Vec<&str>>()
2359            );
2360        }
2361
2362        #[test]
2363        fn should_treat_without_delim() {
2364            assert_eq!(
2365                vec!["control"],
2366                SplitAtPlus::new("control").collect::<Vec<&str>>()
2367            );
2368        }
2369
2370        #[test]
2371        fn should_return_trailing_empty_string_on_delim_last() {
2372            assert_eq!(vec!["+", ""], SplitAtPlus::new("++").collect::<Vec<&str>>());
2373            assert_eq!(
2374                vec!["control", ""],
2375                SplitAtPlus::new("control+").collect::<Vec<&str>>()
2376            );
2377        }
2378
2379        #[test]
2380        fn should_parse_non_delim_delim_non_delim() {
2381            assert_eq!(
2382                vec!["control", "+", "shift"],
2383                SplitAtPlus::new("control+++shift").collect::<Vec<&str>>()
2384            );
2385        }
2386
2387        #[test]
2388        fn should_treat_delim_followed_by_key_as_trailing() {
2389            assert_eq!(vec!["", "q"], SplitAtPlus::new("+q").collect::<Vec<&str>>());
2390        }
2391    }
2392
2393    mod key_wrap {
2394        use super::*;
2395        use pretty_assertions::assert_eq;
2396
2397        #[test]
2398        fn should_parse_function_keys() {
2399            assert_eq!(
2400                KeyWrap(tuievents::Key::Function(10)),
2401                KeyWrap::try_from("f10").unwrap()
2402            );
2403            assert_eq!(
2404                KeyWrap(tuievents::Key::Function(0)),
2405                KeyWrap::try_from("f0").unwrap()
2406            );
2407            assert_eq!(
2408                KeyWrap(tuievents::Key::Function(255)),
2409                KeyWrap::try_from("f255").unwrap()
2410            );
2411        }
2412
2413        #[test]
2414        fn should_parse_char() {
2415            assert_eq!(
2416                KeyWrap(tuievents::Key::Char('q')),
2417                KeyWrap::try_from("q").unwrap()
2418            );
2419            assert_eq!(
2420                KeyWrap(tuievents::Key::Char('w')),
2421                KeyWrap::try_from("w").unwrap()
2422            );
2423            assert_eq!(
2424                KeyWrap(tuievents::Key::Char('.')),
2425                KeyWrap::try_from(".").unwrap()
2426            );
2427            assert_eq!(
2428                KeyWrap(tuievents::Key::Char('@')),
2429                KeyWrap::try_from("@").unwrap()
2430            );
2431
2432            // space alias
2433            assert_eq!(
2434                KeyWrap(tuievents::Key::Char(' ')),
2435                KeyWrap::try_from("space").unwrap()
2436            );
2437        }
2438
2439        #[test]
2440        fn should_serialize_function_keys() {
2441            assert_eq!(&"f10", &KeyWrap(tuievents::Key::Function(10)).to_string());
2442            assert_eq!(&"f0", &KeyWrap(tuievents::Key::Function(0)).to_string());
2443            assert_eq!(&"f255", &KeyWrap(tuievents::Key::Function(255)).to_string());
2444        }
2445
2446        #[test]
2447        fn should_serialize_char() {
2448            assert_eq!(&"q", &KeyWrap(tuievents::Key::Char('q')).to_string());
2449            assert_eq!(&"w", &KeyWrap(tuievents::Key::Char('w')).to_string());
2450            assert_eq!(&".", &KeyWrap(tuievents::Key::Char('.')).to_string());
2451            assert_eq!(&"@", &KeyWrap(tuievents::Key::Char('@')).to_string());
2452
2453            // space
2454            assert_eq!(&"space", &KeyWrap(tuievents::Key::Char(' ')).to_string());
2455        }
2456    }
2457
2458    mod key_binding {
2459        use super::*;
2460        use pretty_assertions::assert_eq;
2461
2462        #[test]
2463        fn should_parse_keys_simple() {
2464            // all modifiers
2465            assert_eq!(
2466                KeyBinding::from(tuievents::KeyEvent::new(
2467                    tuievents::Key::Char('Q'),
2468                    tuievents::KeyModifiers::all()
2469                )),
2470                KeyBinding::try_from("CONTROL+ALT+SHIFT+Q").unwrap()
2471            );
2472
2473            // no modifiers
2474            assert_eq!(
2475                KeyBinding::from(tuievents::KeyEvent::new(
2476                    tuievents::Key::Char('q'),
2477                    tuievents::KeyModifiers::empty()
2478                )),
2479                KeyBinding::try_from("Q").unwrap()
2480            );
2481
2482            // multiple of the same modifier
2483            assert_eq!(
2484                KeyBinding::from(tuievents::KeyEvent::new(
2485                    tuievents::Key::Char('q'),
2486                    tuievents::KeyModifiers::CONTROL
2487                )),
2488                KeyBinding::try_from("CONTROL+CONTROL+CONTROL+Q").unwrap()
2489            );
2490        }
2491
2492        #[test]
2493        fn should_error_on_multiple_keys() {
2494            assert_eq!(
2495                Err(KeyParseError::MultipleKeys {
2496                    input: "q+s".to_string(),
2497                    old_key: "q".to_string(),
2498                    new_key: "s".to_string()
2499                }),
2500                KeyBinding::try_from("Q+S")
2501            );
2502        }
2503
2504        #[test]
2505        fn should_serialize() {
2506            // all modifiers
2507            assert_eq!(
2508                "control+alt+shift+q",
2509                KeyBinding::from(tuievents::KeyEvent::new(
2510                    tuievents::Key::Char('q'),
2511                    tuievents::KeyModifiers::all()
2512                ))
2513                .to_string()
2514            );
2515
2516            // only control
2517            assert_eq!(
2518                "control+q",
2519                KeyBinding::from(tuievents::KeyEvent::new(
2520                    tuievents::Key::Char('q'),
2521                    tuievents::KeyModifiers::CONTROL
2522                ))
2523                .to_string()
2524            );
2525
2526            // only alt
2527            assert_eq!(
2528                "alt+q",
2529                KeyBinding::from(tuievents::KeyEvent::new(
2530                    tuievents::Key::Char('q'),
2531                    tuievents::KeyModifiers::ALT
2532                ))
2533                .to_string()
2534            );
2535
2536            // only shift
2537            assert_eq!(
2538                "shift+q",
2539                KeyBinding::from(tuievents::KeyEvent::new(
2540                    tuievents::Key::Char('q'),
2541                    tuievents::KeyModifiers::SHIFT
2542                ))
2543                .to_string()
2544            );
2545
2546            // no modifiers
2547            assert_eq!(
2548                "q",
2549                KeyBinding::from(tuievents::KeyEvent::new(
2550                    tuievents::Key::Char('q'),
2551                    tuievents::KeyModifiers::empty()
2552                ))
2553                .to_string()
2554            );
2555        }
2556
2557        #[test]
2558        fn should_allow_special_keys() {
2559            // we currently split with a delimiter of "+", but it should still be available
2560            assert_eq!(
2561                KeyBinding::from(tuievents::KeyEvent::new(
2562                    tuievents::Key::Char('+'),
2563                    tuievents::KeyModifiers::empty()
2564                )),
2565                KeyBinding::try_from("+").unwrap()
2566            );
2567
2568            // just some extra tests
2569            assert_eq!(
2570                KeyBinding::from(tuievents::KeyEvent::new(
2571                    tuievents::Key::Char('-'),
2572                    tuievents::KeyModifiers::empty()
2573                )),
2574                KeyBinding::try_from("-").unwrap()
2575            );
2576
2577            assert_eq!(
2578                KeyBinding::from(tuievents::KeyEvent::new(
2579                    tuievents::Key::Char(' '),
2580                    tuievents::KeyModifiers::empty()
2581                )),
2582                KeyBinding::try_from(" ").unwrap()
2583            );
2584        }
2585
2586        #[test]
2587        fn should_not_allow_invalid_formats() {
2588            // empty string
2589            assert_eq!(
2590                Err(KeyParseError::NoKeyFound(String::new())),
2591                KeyBinding::try_from("")
2592            );
2593
2594            // multiple spaces
2595            assert_eq!(
2596                Err(KeyParseError::UnknownKey("   ".to_owned())),
2597                KeyBinding::try_from("   ")
2598            );
2599
2600            // this could either mean key "+" plus invalid, or invalid plus "+" key
2601            assert_eq!(
2602                Err(KeyParseError::TrailingDelimiter("++".to_owned())),
2603                KeyBinding::try_from("++")
2604            );
2605
2606            // trailing delimiter
2607            assert_eq!(
2608                Err(KeyParseError::TrailingDelimiter("control+".to_owned())),
2609                KeyBinding::try_from("control+")
2610            );
2611
2612            // first trailing delimiter
2613            assert_eq!(
2614                Err(KeyParseError::TrailingDelimiter("+control".to_owned())),
2615                KeyBinding::try_from("+control")
2616            );
2617        }
2618    }
2619
2620    mod keys {
2621        use figment::{
2622            providers::{Format, Toml},
2623            Figment,
2624        };
2625        use pretty_assertions::assert_eq;
2626
2627        use super::*;
2628
2629        #[test]
2630        fn should_parse_default_keys() {
2631            let serialized = toml::to_string(&Keys::default()).unwrap();
2632
2633            let parsed: Keys = Figment::new()
2634                .merge(Toml::string(&serialized))
2635                .extract()
2636                .unwrap();
2637
2638            assert_eq!(Keys::default(), parsed);
2639        }
2640
2641        #[test]
2642        fn should_not_conflict_on_default() {
2643            assert_eq!(Ok(()), Keys::default().check_keys());
2644        }
2645
2646        #[test]
2647        fn should_not_conflict_on_different_view() {
2648            // check that views that would not conflict do not conflict
2649            let mut keys = Keys::default();
2650            keys.library_keys.delete = tuievents::Key::Delete.into();
2651            keys.podcast_keys.delete_feed = tuievents::Key::Delete.into();
2652
2653            assert_eq!(Ok(()), keys.check_keys());
2654        }
2655
2656        #[test]
2657        fn should_err_on_global_key_conflict() {
2658            // check that views that would not conflict do not conflict
2659            let mut keys = Keys::default();
2660            keys.select_view_keys.view_podcasts = tuievents::Key::Delete.into();
2661            keys.podcast_keys.delete_feed = tuievents::Key::Delete.into();
2662
2663            assert_eq!(
2664                Err(KeysCheckError {
2665                    errored_keys: vec![KeyConflictError {
2666                        key_path_first: "keys.view.view_podcasts".into(),
2667                        key_path_second: "keys.podcast.delete_feed".into(),
2668                        key: tuievents::Key::Delete.into()
2669                    }]
2670                }),
2671                keys.check_keys()
2672            );
2673        }
2674    }
2675}