microswitch/
config.rs

1use crate::error::ConfigError;
2use gilrs::Button;
3use iced::keyboard::KeyCode;
4use serde::{Deserialize};
5use std::collections::HashMap;
6use std::fmt::Debug;
7use std::path::{Path, PathBuf};
8use std::{fs, array};
9
10const KEYBOARD_BUTTON_MAPPING: [(&str, KeyCode); 136] = [
11    ("1", KeyCode::Key1),
12    ("2", KeyCode::Key2),
13    ("3", KeyCode::Key3),
14    ("4", KeyCode::Key4),
15    ("5", KeyCode::Key5),
16    ("6", KeyCode::Key6),
17    ("7", KeyCode::Key7),
18    ("8", KeyCode::Key8),
19    ("9", KeyCode::Key9),
20    ("0", KeyCode::Key0),
21    ("A", KeyCode::A),
22    ("B", KeyCode::B),
23    ("C", KeyCode::C),
24    ("D", KeyCode::D),
25    ("E", KeyCode::E),
26    ("F", KeyCode::F),
27    ("G", KeyCode::G),
28    ("H", KeyCode::H),
29    ("I", KeyCode::I),
30    ("J", KeyCode::J),
31    ("K", KeyCode::K),
32    ("L", KeyCode::L),
33    ("M", KeyCode::M),
34    ("N", KeyCode::N),
35    ("O", KeyCode::O),
36    ("P", KeyCode::P),
37    ("Q", KeyCode::Q),
38    ("R", KeyCode::R),
39    ("S", KeyCode::S),
40    ("T", KeyCode::T),
41    ("U", KeyCode::U),
42    ("V", KeyCode::V),
43    ("W", KeyCode::W),
44    ("X", KeyCode::X),
45    ("Y", KeyCode::Y),
46    ("Z", KeyCode::Z),
47    ("Escape", KeyCode::Escape),
48    ("F1", KeyCode::F1),
49    ("F2", KeyCode::F2),
50    ("F3", KeyCode::F3),
51    ("F4", KeyCode::F4),
52    ("F5", KeyCode::F5),
53    ("F6", KeyCode::F6),
54    ("F7", KeyCode::F7),
55    ("F8", KeyCode::F8),
56    ("F9", KeyCode::F9),
57    ("F10", KeyCode::F10),
58    ("F11", KeyCode::F11),
59    ("F12", KeyCode::F12),
60    ("F13", KeyCode::F13),
61    ("F14", KeyCode::F14),
62    ("F15", KeyCode::F15),
63    ("F16", KeyCode::F16),
64    ("F17", KeyCode::F17),
65    ("F18", KeyCode::F18),
66    ("F19", KeyCode::F19),
67    ("F20", KeyCode::F20),
68    ("F21", KeyCode::F21),
69    ("F22", KeyCode::F22),
70    ("F23", KeyCode::F23),
71    ("F24", KeyCode::F24),
72    ("Scroll", KeyCode::Scroll),
73    ("Pause", KeyCode::Pause),
74    ("Insert", KeyCode::Insert),
75    ("Home", KeyCode::Home),
76    ("Delete", KeyCode::Delete),
77    ("End", KeyCode::End),
78    ("PageDown", KeyCode::PageDown),
79    ("PageUp", KeyCode::PageUp),
80    ("Left", KeyCode::Left),
81    ("Up", KeyCode::Up),
82    ("Right", KeyCode::Right),
83    ("Down", KeyCode::Down),
84    ("Backspace", KeyCode::Backspace),
85    ("Enter", KeyCode::Enter),
86    ("Space", KeyCode::Space),
87    ("Compose", KeyCode::Compose),
88    ("Caret", KeyCode::Caret),
89    ("Numlock", KeyCode::Numlock),
90    ("Numpad0", KeyCode::Numpad0),
91    ("Numpad1", KeyCode::Numpad1),
92    ("Numpad2", KeyCode::Numpad2),
93    ("Numpad3", KeyCode::Numpad3),
94    ("Numpad4", KeyCode::Numpad4),
95    ("Numpad5", KeyCode::Numpad5),
96    ("Numpad6", KeyCode::Numpad6),
97    ("Numpad7", KeyCode::Numpad7),
98    ("Numpad8", KeyCode::Numpad8),
99    ("Numpad9", KeyCode::Numpad9),
100    ("NumpadAdd", KeyCode::NumpadAdd),
101    ("NumpadDivide", KeyCode::NumpadDivide),
102    ("NumpadDecimal", KeyCode::NumpadDecimal),
103    ("NumpadComma", KeyCode::NumpadComma),
104    ("NumpadEnter", KeyCode::NumpadEnter),
105    ("NumpadEquals", KeyCode::NumpadEquals),
106    ("NumpadMultiply", KeyCode::NumpadMultiply),
107    ("NumpadSubtract", KeyCode::NumpadSubtract),
108    ("Apostrophe", KeyCode::Apostrophe),
109    ("Asterisk", KeyCode::Asterisk),
110    ("Backslash", KeyCode::Backslash),
111    ("Capital", KeyCode::Capital),
112    ("Colon", KeyCode::Colon),
113    ("Comma", KeyCode::Comma),
114    ("Convert", KeyCode::Convert),
115    ("Equals", KeyCode::Equals),
116    ("Grave", KeyCode::Grave),
117    ("LAlt", KeyCode::LAlt),
118    ("LBracket", KeyCode::LBracket),
119    ("LControl", KeyCode::LControl),
120    ("LShift", KeyCode::LShift),
121    ("LWin", KeyCode::LWin),
122    ("MediaSelect", KeyCode::MediaSelect),
123    ("MediaStop", KeyCode::MediaStop),
124    ("Minus", KeyCode::Minus),
125    ("Mute", KeyCode::Mute),
126    ("NavigateForward", KeyCode::NavigateForward),
127    ("NavigateBackward", KeyCode::NavigateBackward),
128    ("NextTrack", KeyCode::NextTrack),
129    ("Period", KeyCode::Period),
130    ("PlayPause", KeyCode::PlayPause),
131    ("Plus", KeyCode::Plus),
132    ("PrevTrack", KeyCode::PrevTrack),
133    ("RAlt", KeyCode::RAlt),
134    ("RBracket", KeyCode::RBracket),
135    ("RControl", KeyCode::RControl),
136    ("RShift", KeyCode::RShift),
137    ("RWin", KeyCode::RWin),
138    ("Semicolon", KeyCode::Semicolon),
139    ("Slash", KeyCode::Slash),
140    ("Sleep", KeyCode::Sleep),
141    ("Stop", KeyCode::Stop),
142    ("Sysrq", KeyCode::Sysrq),
143    ("Tab", KeyCode::Tab),
144    ("Underline", KeyCode::Underline),
145    ("VolumeDown", KeyCode::VolumeDown),
146    ("VolumeUp", KeyCode::VolumeUp),
147];
148
149const GAMEPAD_BUTTON_MAPPING: [(&str, Button); 19] = [
150    ("South", Button::South),
151    ("East", Button::East),
152    ("North", Button::North),
153    ("West", Button::West),
154    ("C", Button::C),
155    ("Z", Button::Z),
156    ("LeftTrigger", Button::LeftTrigger),
157    ("LeftTrigger2", Button::LeftTrigger2),
158    ("RightTrigger", Button::RightTrigger),
159    ("RightTrigger2", Button::RightTrigger2),
160    ("Select", Button::Select),
161    ("Start", Button::Start),
162    ("Mode", Button::Mode),
163    ("LeftThumb", Button::LeftThumb),
164    ("RightThumb", Button::RightThumb),
165    ("DPadUp", Button::DPadUp),
166    ("DPadDown", Button::DPadDown),
167    ("DPadLeft", Button::DPadLeft),
168    ("DPadRight", Button::DPadRight),
169];
170
171fn make_keyboard_button_map() -> HashMap<&'static str, KeyCode> {
172    KEYBOARD_BUTTON_MAPPING.iter().cloned().collect()
173}
174
175fn make_valid_keyboard_button_vec() -> Vec<&'static str>{
176    array::IntoIter::new(KEYBOARD_BUTTON_MAPPING)
177        .map(|x| x.0)
178        .collect()
179}
180
181fn make_gamepad_button_map() -> HashMap<&'static str, Button> {
182    GAMEPAD_BUTTON_MAPPING.iter().cloned().collect()
183}
184
185fn make_valid_gamepad_button_vec() -> Vec<&'static str>{
186    array::IntoIter::new(GAMEPAD_BUTTON_MAPPING)
187        .map(|x| x.0)
188        .collect()
189}
190
191// SampleId and BankId are used during config parsing, they are a human readable reference to
192// specific banks/samples. They are translated to BankRef and SampleRef, which enable config structs
193// to quickly look up other config structs by index.
194type SampleId = String;
195type BankId = String;
196
197#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
198pub struct BankRef {
199    pub bank_index: usize,
200}
201
202#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
203pub struct SampleRef {
204    pub sample_index: usize,
205}
206
207#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
208pub struct BankSampleRef {
209    pub bank: BankRef,
210    pub sample: SampleRef,
211}
212
213#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
214pub struct SwitchRef {
215    pub switch_index: usize,
216}
217
218#[derive(Deserialize, Debug, Clone, PartialEq)]
219#[serde(deny_unknown_fields)]
220pub struct SampleConfig {
221    pub id: SampleId,
222    pub file: PathBuf,
223
224    // Cached //
225
226    #[serde(skip)]
227    pub bank_sample_ref: BankSampleRef,
228
229    /// None if the config was embedded, Some if the config is from disk
230    #[serde(skip)]
231    pub file_resolved: Option<PathBuf>,
232}
233
234#[derive(Deserialize, Debug, Clone, PartialEq)]
235#[serde(deny_unknown_fields)]
236pub struct BankConfig {
237    pub id: BankId,
238    /// If true, do not mute other samples when playing a sample in this bank
239    #[serde(default)]
240    pub poly: bool,
241    #[serde(default)]
242    pub samples: Vec<SampleConfig>,
243
244    // Cached //
245
246    #[serde(skip)]
247    pub bank_ref: BankRef,
248}
249
250impl BankConfig {
251    pub fn sample(&self, sample_ref: SampleRef) -> &SampleConfig {
252        &self.samples[sample_ref.sample_index]
253    }
254}
255
256#[derive(Deserialize, Debug, Clone, PartialEq)]
257#[serde(deny_unknown_fields)]
258#[serde(rename_all = "camelCase")]
259pub struct Gamepad {
260    pub device_id: Option<usize>,
261
262    /// Must match an identifier present in GAMEPAD_BUTTON_MAPPING
263    pub button: String,
264
265    // Cached //
266
267    #[serde(skip)]
268    pub gilrs_button: Button,
269}
270
271#[derive(Deserialize, Debug, Clone, PartialEq)]
272#[serde(deny_unknown_fields)]
273#[serde(rename_all = "camelCase")]
274pub struct SwitchPlay {
275    pub bank: BankId,
276    pub sample: SampleId,
277
278    // Cached //
279
280    #[serde(skip)]
281    pub bank_sample_ref: BankSampleRef,
282}
283
284#[derive(Deserialize, Debug, Clone, PartialEq)]
285#[serde(deny_unknown_fields)]
286#[serde(rename_all = "camelCase")]
287pub struct SwitchPlayRandom {
288    pub bank: BankId,
289
290    // Cached //
291
292    #[serde(skip)]
293    pub bank_ref: BankRef,
294}
295
296#[derive(Deserialize, Debug, Clone, PartialEq)]
297#[serde(deny_unknown_fields)]
298#[serde(rename_all = "camelCase")]
299pub struct SwitchPlayStep {
300    pub bank: BankId,
301    /// How many steps to skip to skip in the bank
302    /// -1 = play the previous one
303    /// 0 = repeat the last played sample
304    /// 1 = play the next one
305    /// 2 = play the next one, skipping a sample
306    /// etc
307
308    pub steps: i32,
309
310    // Cached //
311
312    #[serde(skip)]
313    pub bank_ref: BankRef,
314}
315
316#[derive(Deserialize, Debug, Clone, PartialEq)]
317#[serde(deny_unknown_fields)]
318#[serde(rename_all = "camelCase")]
319pub struct SwitchConfig {
320    // Switch trigger conditions //
321
322    /// the title in the gui
323    pub title: String,
324    /// trigger based on a keyboard key
325    pub key: Option<String>,
326    /// trigger based on a gamepad button
327    pub gamepad: Option<Gamepad>,
328
329    // Actions //
330
331    /// if true, stop all sounds
332    #[serde(default)]
333    pub stop_sounds: bool,
334
335    /// Play a specific sample in a specific bank
336    pub play: Option<SwitchPlay>,
337
338    // Play a random sample in a bank
339    pub play_random: Option<SwitchPlayRandom>,
340
341    // Play a sample, relative in position to the sample previously played in a bank
342    pub play_step: Option<SwitchPlayStep>,
343
344    // Cached //
345
346    #[serde(skip)]
347    pub switch_ref: SwitchRef,
348
349    /// same as `key` but translated to a KeyCode
350    #[serde(skip)]
351    pub key_code: Option<KeyCode>,
352}
353
354struct ConfigIdLookup {
355    /// bank.id => BankRef
356    bank_id_lookup: HashMap<String, BankRef>,
357
358    /// sample.id => BankRef
359    sample_id_lookup: HashMap<String, HashMap<String, BankSampleRef>>,
360}
361
362impl ConfigIdLookup {
363    fn new(banks: &Vec<BankConfig>) -> Self {
364        let bank_id_lookup = banks
365            .iter()
366            .map(|bank| (bank.id.clone(), bank.bank_ref))
367            .collect();
368
369        let sample_id_lookup = banks
370            .iter()
371            .map(|bank| {
372                let samples = bank.samples
373                    .iter()
374                    .map(|sample| (sample.id.clone(), sample.bank_sample_ref))
375                    .collect();
376
377                (bank.id.clone(), samples)
378            })
379            .collect();
380
381        ConfigIdLookup { bank_id_lookup, sample_id_lookup }
382    }
383
384    fn bank_id_to_ref(&self, bank_id: &str) -> Result<BankRef, ConfigError> {
385        match self.bank_id_lookup.get(bank_id) {
386            Some(bank_ref) => Ok(*bank_ref),
387            None => Err(ConfigError::UnknownBankId {
388                bank: bank_id.to_string(),
389            }),
390        }
391    }
392
393    fn sample_id_to_ref(&self, bank_id: &str, sample_id: &str) -> Result<BankSampleRef, ConfigError> {
394        let result = self.sample_id_lookup
395            .get(bank_id)
396            .map(|map| map.get(sample_id));
397
398        match result {
399            None => Err(ConfigError::UnknownBankId {
400                bank: bank_id.to_string(),
401            }),
402            Some(None) => Err(ConfigError::UnknownSampleId {
403                bank: bank_id.to_string(),
404                sample: sample_id.to_string(),
405            }),
406            Some(Some(bank_sample_ref)) => Ok(*bank_sample_ref),
407        }
408    }
409}
410
411#[derive(Deserialize, Debug, Clone, PartialEq)]
412#[serde(deny_unknown_fields)]
413pub struct Config {
414    pub banks: Vec<BankConfig>,
415    pub switches: Vec<SwitchConfig>,
416
417    // Cached //
418
419    /// The path that all other paths are relative to
420    /// None if the config was embedded, Some if the config is from disk
421    #[serde(skip)]
422    pub resolve_path: Option<PathBuf>,
423
424    #[serde(skip)]
425    // keyboard key code => switch config reference
426    keyboard_key_to_switch_lookup_table: HashMap<KeyCode, SwitchRef>,
427
428    #[serde(skip)]
429    // device id => gamepad button => switch config reference
430    gamepad_button_to_switch_lookup_table: HashMap<Option<usize>, HashMap<Button, SwitchRef>>,
431
432    #[serde(skip)]
433    // for each switch that has a SwitchConfig.play (SwitchPlay) configuration, map the sample that it specifies to the switch
434    sample_to_switch_play: HashMap<BankSampleRef, Vec<SwitchRef>>,
435
436    #[serde(skip)]
437    empty_switch_ref_vec: Vec<SwitchRef>, // serde will init this to Default::default() which will be an empty vec
438}
439
440impl Config {
441    pub fn from_string(yaml_string: &str, resolve_path: Option<PathBuf>) -> Result<Config, ConfigError> {
442        let mut config: Config = serde_yaml::from_str(yaml_string)?;
443        config.resolve_path = resolve_path;
444
445        config.resolve_refs()?;
446        config.resolve_bank_paths();
447        config.resolve_gamepad_button_mappings()?;
448        config.resolve_keyboard_key_to_switch_lookup_table()?;
449        config.resolve_gamepad_button_to_switch_lookup_table();
450        config.resolve_sample_to_switch_play_lookup_table();
451
452        Ok(config)
453    }
454
455    pub fn from_file(path: &Path) -> Result<Config, ConfigError> {
456        let content = fs::read_to_string(&path)?;
457
458        // all paths defined in the config file, are relative to the directory the config file is in
459        let mut resolve_path = path.to_path_buf();
460        resolve_path.pop();
461        Ok(Config::from_string(&content, Some(resolve_path))?)
462    }
463
464    fn resolve_refs(&mut self) -> Result<(), ConfigError> {
465        for (bank_index, bank_config) in &mut self.banks.iter_mut().enumerate() {
466            bank_config.bank_ref.bank_index = bank_index;
467
468            for (sample_index, sample_config) in &mut bank_config.samples.iter_mut().enumerate() {
469                sample_config.bank_sample_ref.bank.bank_index = bank_index;
470                sample_config.bank_sample_ref.sample.sample_index = sample_index;
471            }
472        }
473
474        for (switch_index, switch_config) in &mut self.switches.iter_mut().enumerate() {
475            switch_config.switch_ref.switch_index = switch_index;
476        }
477
478        let lookup = ConfigIdLookup::new(&self.banks);
479
480        for switch_config in &mut self.switches.iter_mut() {
481            if let Some(play) = &mut switch_config.play.as_mut() {
482                play.bank_sample_ref = lookup.sample_id_to_ref(&play.bank, &play.sample)?;
483            }
484
485            if let Some(play) = &mut switch_config.play_random.as_mut() {
486                play.bank_ref = lookup.bank_id_to_ref(&play.bank)?;
487            }
488
489            if let Some(play) = &mut switch_config.play_step.as_mut() {
490                play.bank_ref = lookup.bank_id_to_ref(&play.bank)?;
491            }
492        }
493
494        Ok(())
495    }
496
497    fn resolve_bank_paths(&mut self) {
498        let resolve_path = &self.resolve_path;
499        if resolve_path.is_none() {
500            return;
501        }
502        let resolve_path = resolve_path.as_ref().unwrap();
503
504        for bank_config in &mut self.banks {
505            for sample in &mut bank_config.samples {
506                let mut resolved_file = PathBuf::from(resolve_path);
507                resolved_file.push(&sample.file);
508                sample.file_resolved = Some(resolved_file);
509            }
510        }
511    }
512
513    fn resolve_gamepad_button_mappings(&mut self) -> Result<(), ConfigError> {
514        let mapping = make_gamepad_button_map();
515        for switch_config in &mut self.switches {
516            if let Some(gamepad) = &mut switch_config.gamepad {
517                let gilrs_button = match mapping.get(&gamepad.button.as_str()) {
518                    None => {
519                        return Err(ConfigError::UnknownGamepadButton {
520                            button: String::from(&gamepad.button),
521                            allowed_values: make_valid_gamepad_button_vec().join(", "),
522                        });
523                    },
524                    Some(v) => v,
525                };
526                gamepad.gilrs_button = *gilrs_button;
527            }
528        }
529
530        Ok(())
531    }
532
533    fn resolve_keyboard_key_to_switch_lookup_table(&mut self) -> Result<(), ConfigError> {
534        let mapping = make_keyboard_button_map();
535        let mut lookup_table = HashMap::new();
536
537        for switch_config in (&mut self.switches).into_iter() {
538            if let Some(key) = &switch_config.key {
539                match mapping.get(key.as_str()) {
540                    Some(key_code) => {
541                        switch_config.key_code = Some(*key_code);
542                    },
543                    None => {
544                        return Err(ConfigError::UnknownKeyboardButton {
545                            button: key.to_string(),
546                            allowed_values: make_valid_keyboard_button_vec().join(", "),
547                        });
548                    },
549                }
550            }
551        }
552
553        for switch_config in (&self.switches).into_iter() {
554            if let Some(key_code) = &switch_config.key_code {
555                lookup_table.insert(*key_code, switch_config.switch_ref);
556            }
557        }
558
559        self.keyboard_key_to_switch_lookup_table = lookup_table;
560        Ok(())
561    }
562
563    fn resolve_gamepad_button_to_switch_lookup_table(&mut self) {
564        let gamepad_configs = (&self.switches)
565            .into_iter()
566            .filter_map(|switch_config| {
567                match &switch_config.gamepad {
568                    Some(gamepad) => Some((gamepad, switch_config.switch_ref)),
569                    None => None,
570                }
571            });
572
573        let mut lookup_table = HashMap::new();
574
575        for (gamepad_config, switch_ref) in gamepad_configs {
576            if !lookup_table.contains_key(&gamepad_config.device_id) {
577                lookup_table.insert(gamepad_config.device_id, HashMap::new());
578            }
579
580            let map = lookup_table.get_mut(&gamepad_config.device_id).unwrap();
581            map.insert(gamepad_config.gilrs_button, switch_ref);
582        }
583
584        self.gamepad_button_to_switch_lookup_table = lookup_table;
585    }
586
587    fn resolve_sample_to_switch_play_lookup_table(&mut self) {
588        let mut lookup_table = HashMap::new();
589
590        for switch in &self.switches {
591            if let Some(play) = &switch.play {
592                let list = lookup_table.entry(play.bank_sample_ref).or_insert_with(|| Vec::new());
593                list.push(switch.switch_ref);
594            }
595        }
596
597        self.sample_to_switch_play = lookup_table;
598    }
599
600    pub fn find_switch_for_keyboard_key(&self, key: KeyCode) -> Option<&SwitchConfig> {
601        match self.keyboard_key_to_switch_lookup_table.get(&key) {
602            Some(switch_ref) => Some(&self.switches[switch_ref.switch_index]),
603            None => None,
604        }
605    }
606
607    pub fn find_switch_for_gamepad_button(&self, device_id: usize, button: Button) -> Option<&SwitchConfig> {
608        // first try to find a switch configured for a specific gamepad device
609        let switch_ref = match self.gamepad_button_to_switch_lookup_table.get(&Some(device_id)) {
610            Some(map) => map.get(&button),
611            None => None,
612        };
613
614        // Then try to find a switch configured for all gamepad devices
615        let switch_ref = match switch_ref {
616            Some(v) => Some(v),
617            None => {
618                match self.gamepad_button_to_switch_lookup_table.get(&None) {
619                    Some(map) => map.get(&button),
620                    None => None,
621                }
622            }
623        };
624
625        match switch_ref {
626            Some(switch_ref) => Some(&self.switches[switch_ref.switch_index]),
627            None => None,
628        }
629    }
630
631    pub fn find_switch_play_for_sample(&self, bank_sample_ref: BankSampleRef) -> &Vec<SwitchRef> {
632        if let Some(list) = self.sample_to_switch_play.get(&bank_sample_ref) {
633            list
634        }
635        else {
636            &self.empty_switch_ref_vec
637        }
638    }
639
640    pub fn switch(&self, switch_ref: SwitchRef) -> &SwitchConfig {
641        // this will crash if switch_ref.switch_index is out of bounds, however the expectation is
642        // that a SwitchRef instance is always valid.
643        &self.switches[switch_ref.switch_index]
644    }
645
646    pub fn bank(&self, bank_ref: BankRef) -> &BankConfig {
647        &self.banks[bank_ref.bank_index]
648    }
649
650    pub fn sample(&self, bank_sample_ref: BankSampleRef) -> (&BankConfig, &SampleConfig) {
651        let bank = self.bank(bank_sample_ref.bank);
652        let sample = bank.sample(bank_sample_ref.sample);
653        (bank, sample)
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use crate::config::{Config, BankConfig, BankRef, SampleConfig, SampleRef, BankSampleRef, SwitchConfig, SwitchRef, SwitchPlay, SwitchPlayRandom, SwitchPlayStep, Gamepad};
660    use std::path::{PathBuf};
661    use gilrs::Button;
662    use iced::keyboard::KeyCode;
663    use pretty_assertions::{assert_eq};
664    use crate::error::ConfigError;
665
666    fn test_path(extra_parts: &[&str]) -> PathBuf {
667        let path_sep = std::path::MAIN_SEPARATOR.to_string();
668        [path_sep.as_str(), "test", "path"].iter().chain(extra_parts.iter()).collect()
669    }
670
671    #[test]
672    fn successful_config_parsing() {
673        let config_source = r###"
674banks:
675  - id: bankA
676    # poly option is false by default
677    poly: true
678    samples:
679      # Multiple samples
680      - id: foo1
681        file: foo1.mp3
682      - id: foo2
683        file: foo2.wav
684      - id: foo3
685        file: foo3.ogg
686      - id: foo4
687        file: foo4.flac
688
689  - id: bankB
690    # poly option is not set here
691    samples:
692      # sample with id "foo1" also exists in "bankA". They should not interfere
693      - id: foo1
694        file: foo1-bankB.mp3
695
696switches:
697  - title: play option
698    play:
699      bank: bankB
700      sample: foo1
701
702  - title: playRandom option
703    playRandom:
704      bank: bankB
705
706  - title: playStep option
707    playStep:
708      bank: bankA
709      steps: 1
710
711  - title: stopSounds option
712    stopSounds: true
713
714  - title: Only the required fields
715
716  - title: keyboard key
717    key: X
718
719  - title: gamepad button on any device
720    gamepad:
721      button: North
722
723  - title: gamepad button, specific device
724    gamepad:
725      deviceId: 123
726      button: North
727
728  - title: another play option, same actions as the first
729    play:
730      bank: bankB
731      sample: foo1
732"###;
733        let config = Config::from_string(config_source, Some(test_path(&[]))).unwrap();
734        assert_eq!(config, Config {
735            banks: vec![
736                BankConfig {
737                    id: "bankA".to_string(),
738                    poly: true,
739                    samples: vec![
740                        SampleConfig {
741                            id: "foo1".to_string(),
742                            file: PathBuf::from("foo1.mp3"),
743                            bank_sample_ref: BankSampleRef {
744                                bank: BankRef { bank_index: 0 },
745                                sample: SampleRef { sample_index: 0 },
746                            },
747                            file_resolved: Some(test_path(&["foo1.mp3"])),
748                        },
749                        SampleConfig {
750                            id: "foo2".to_string(),
751                            file: PathBuf::from("foo2.wav"),
752                            bank_sample_ref: BankSampleRef {
753                                bank: BankRef { bank_index: 0 },
754                                sample: SampleRef { sample_index: 1 },
755                            },
756                            file_resolved: Some(test_path(&["foo2.wav"])),
757                        },
758                        SampleConfig {
759                            id: "foo3".to_string(),
760                            file: PathBuf::from("foo3.ogg"),
761                            bank_sample_ref: BankSampleRef {
762                                bank: BankRef { bank_index: 0 },
763                                sample: SampleRef { sample_index: 2 },
764                            },
765                            file_resolved: Some(test_path(&["foo3.ogg"])),
766                        },
767                        SampleConfig {
768                            id: "foo4".to_string(),
769                            file: PathBuf::from("foo4.flac"),
770                            bank_sample_ref: BankSampleRef {
771                                bank: BankRef { bank_index: 0 },
772                                sample: SampleRef { sample_index: 3 },
773                            },
774                            file_resolved: Some(test_path(&["foo4.flac"])),
775                        },
776                    ],
777                    bank_ref: BankRef { bank_index: 0 },
778                },
779                BankConfig {
780                    id: "bankB".to_string(),
781                    poly: false,
782                    samples: vec![
783                        SampleConfig {
784                            id: "foo1".to_string(),
785                            file: PathBuf::from("foo1-bankB.mp3"),
786                            bank_sample_ref: BankSampleRef {
787                                bank: BankRef { bank_index: 1 },
788                                sample: SampleRef { sample_index: 0 },
789                            },
790                            file_resolved: Some(test_path(&["foo1-bankB.mp3"])),
791                        },
792                    ],
793                    bank_ref: BankRef { bank_index: 1 },
794                },
795            ],
796            switches: vec![
797                SwitchConfig {
798                    title: "play option".to_string(),
799                    key: None,
800                    gamepad: None,
801                    stop_sounds: false,
802                    play: Some(SwitchPlay {
803                        bank: "bankB".to_string(),
804                        sample: "foo1".to_string(),
805                        bank_sample_ref: BankSampleRef {
806                            bank: BankRef { bank_index: 1 },
807                            sample: SampleRef { sample_index: 0 },
808                        }
809                    }),
810                    play_random: None,
811                    play_step: None,
812                    switch_ref: SwitchRef { switch_index: 0 },
813                    key_code: None
814                },
815                SwitchConfig {
816                    title: "playRandom option".to_string(),
817                    key: None,
818                    gamepad: None,
819                    stop_sounds: false,
820                    play: None,
821                    play_random: Some(
822                        SwitchPlayRandom {
823                            bank: "bankB".to_string(),
824                            bank_ref: BankRef { bank_index: 1 },
825                        },
826                    ),
827                    play_step: None,
828                    switch_ref: SwitchRef { switch_index: 1 },
829                    key_code: None,
830                },
831                SwitchConfig {
832                    title: "playStep option".to_string(),
833                    key: None,
834                    gamepad: None,
835                    stop_sounds: false,
836                    play: None,
837                    play_random: None,
838                    play_step: Some(
839                        SwitchPlayStep {
840                            bank: "bankA".to_string(),
841                            steps: 1,
842                            bank_ref: BankRef { bank_index: 0 },
843                        },
844                    ),
845                    switch_ref: SwitchRef { switch_index: 2 },
846                    key_code: None,
847                },
848                SwitchConfig {
849                    title: "stopSounds option".to_string(),
850                    key: None,
851                    gamepad: None,
852                    stop_sounds: true,
853                    play: None,
854                    play_random: None,
855                    play_step: None,
856                    switch_ref: SwitchRef { switch_index: 3 },
857                    key_code: None,
858                },
859                SwitchConfig {
860                    title: "Only the required fields".to_string(),
861                    key: None,
862                    gamepad: None,
863                    stop_sounds: false,
864                    play: None,
865                    play_random: None,
866                    play_step: None,
867                    switch_ref: SwitchRef { switch_index: 4 },
868                    key_code: None,
869                },
870                SwitchConfig {
871                    title: "keyboard key".to_string(),
872                    key: Some("X".to_string()),
873                    gamepad: None,
874                    stop_sounds: false,
875                    play: None,
876                    play_random: None,
877                    play_step: None,
878                    switch_ref: SwitchRef { switch_index: 5 },
879                    key_code: Some(KeyCode::X),
880                },
881                SwitchConfig {
882                    title: "gamepad button on any device".to_string(),
883                    key: None,
884                    gamepad: Some(
885                        Gamepad {
886                            device_id: None,
887                            button: "North".to_string(),
888                            gilrs_button: Button::North,
889                        },
890                    ),
891                    stop_sounds: false,
892                    play: None,
893                    play_random: None,
894                    play_step: None,
895                    switch_ref: SwitchRef { switch_index: 6 },
896                    key_code: None,
897                },
898                SwitchConfig {
899                    title: "gamepad button, specific device".to_string(),
900                    key: None,
901                    gamepad: Some(
902                        Gamepad {
903                            device_id: Some(123),
904                            button: "North".to_string(),
905                            gilrs_button: Button::North,
906                        },
907                    ),
908                    stop_sounds: false,
909                    play: None,
910                    play_random: None,
911                    play_step: None,
912                    switch_ref: SwitchRef { switch_index: 7 },
913                    key_code: None,
914                },
915                SwitchConfig {
916                    title: "another play option, same actions as the first".to_string(),
917                    key: None,
918                    gamepad: None,
919                    stop_sounds: false,
920                    play: Some(SwitchPlay {
921                        bank: "bankB".to_string(),
922                        sample: "foo1".to_string(),
923                        bank_sample_ref: BankSampleRef {
924                            bank: BankRef { bank_index: 1 },
925                            sample: SampleRef { sample_index: 0 },
926                        }
927                    }),
928                    play_random: None,
929                    play_step: None,
930                    switch_ref: SwitchRef { switch_index: 8 },
931                    key_code: None
932                },
933            ],
934            resolve_path: Some(test_path(&[])),
935
936            keyboard_key_to_switch_lookup_table: vec![
937                (KeyCode::X, SwitchRef { switch_index: 5 }),
938            ].into_iter().collect(),
939
940            gamepad_button_to_switch_lookup_table: vec![
941                (
942                    None,
943                    vec![
944                        (Button::North, SwitchRef { switch_index: 6 }),
945                    ].into_iter().collect(),
946                ),
947                (
948                    Some(123),
949                    vec![
950                        (Button::North, SwitchRef { switch_index: 7 }),
951                    ].into_iter().collect(),
952                ),
953            ].into_iter().collect(),
954
955            sample_to_switch_play: vec![
956                (
957                    BankSampleRef {
958                        bank: BankRef { bank_index: 1 },
959                        sample: SampleRef { sample_index: 0 },
960                    },
961                    vec![
962                        SwitchRef { switch_index: 0 },
963                        SwitchRef { switch_index: 8 },
964                    ],
965                )
966            ].into_iter().collect(),
967            empty_switch_ref_vec: vec![],
968        });
969    }
970
971    #[test]
972    fn find_switch_for_keyboard_key() {
973        let config_source = r###"
974banks: []
975switches:
976  - title: Key a
977    key: A
978
979  # should override the first switch
980  - title: Key a (again)
981    key: A
982
983  - title: Key b
984    key: B
985
986  - title: No key
987"###;
988
989        let config = Config::from_string(config_source, Some(test_path(&[]))).unwrap();
990        assert!(config.find_switch_for_keyboard_key(KeyCode::Z).is_none());
991        assert_eq!(config.find_switch_for_keyboard_key(KeyCode::A).unwrap().title, "Key a (again)");
992        assert_eq!(config.find_switch_for_keyboard_key(KeyCode::B).unwrap().title, "Key b");
993    }
994
995    #[test]
996    fn find_switch_for_gamepad_button() {
997        let config_source = r###"
998banks: []
999switches:
1000  - title: South, no device filter
1001    gamepad:
1002      button: South
1003
1004  # should override the first switch
1005  - title: South, no device filter (again)
1006    gamepad:
1007      button: South
1008
1009  - title: North, device 123
1010    gamepad:
1011      deviceId: 123
1012      button: North
1013
1014  # Should have less priority because it specifies no device
1015  - title: North, no device filter
1016    gamepad:
1017      button: North
1018
1019  # The only entry that specifies West
1020  - title: West, device 123
1021    gamepad:
1022      deviceId: 123
1023      button: West
1024"###;
1025        let config = Config::from_string(config_source, Some(test_path(&[]))).unwrap();
1026
1027        assert!(config.find_switch_for_gamepad_button(10, Button::West).is_none());
1028        assert_eq!(config.find_switch_for_gamepad_button(123, Button::West).unwrap().title, "West, device 123");
1029
1030        assert_eq!(config.find_switch_for_gamepad_button(123, Button::South).unwrap().title, "South, no device filter (again)");
1031
1032        assert_eq!(config.find_switch_for_gamepad_button(123, Button::North).unwrap().title, "North, device 123");
1033        assert_eq!(config.find_switch_for_gamepad_button(456, Button::North).unwrap().title, "North, no device filter");
1034
1035        // Start button is not specified at all
1036        assert!(config.find_switch_for_gamepad_button(10, Button::Start).is_none());
1037    }
1038
1039    #[test]
1040    fn config_with_yaml_syntax_error() {
1041        let config_source = "bla'[ ";
1042        Config::from_string(config_source, Some(test_path(&[]))).unwrap_err();
1043    }
1044
1045    #[test]
1046    fn config_with_mismatched_type() {
1047        let config_source = r###"
1048switches: this should be an array
1049"###;
1050        let error = Config::from_string(config_source, Some(test_path(&[]))).unwrap_err();
1051        let error_message = format!("{}", error);
1052        assert!(error_message.contains("invalid type: string"));
1053    }
1054
1055    #[test]
1056    fn config_with_invalid_bank_id() {
1057        let config_source = r###"
1058banks:
1059  - id: mybank
1060switches:
1061  - title: invalid bank id
1062    playRandom:
1063      bank: invalid
1064"###;
1065        let error = Config::from_string(config_source, Some(test_path(&[]))).unwrap_err();
1066        match error {
1067            ConfigError::UnknownBankId { bank } => {
1068                assert_eq!(bank.as_str(), "invalid");
1069            }
1070            _ => {
1071                panic!("Expected error to be ConfigError::UnknownBankId");
1072            }
1073        }
1074    }
1075
1076    #[test]
1077    fn config_with_invalid_sample_id() {
1078        let config_source = r###"
1079banks:
1080  - id: mybank
1081    samples:
1082      - id: mysample
1083        file: foo.mp3
1084
1085switches:
1086  - title: invalid bank id
1087    play:
1088      bank: mybank
1089      sample: invalid
1090"###;
1091        let error = Config::from_string(config_source, Some(test_path(&[]))).unwrap_err();
1092        match error {
1093            ConfigError::UnknownSampleId { bank, sample } => {
1094                assert_eq!(bank.as_str(), "mybank");
1095                assert_eq!(sample.as_str(), "invalid");
1096            }
1097            _ => {
1098                panic!("Expected error to be ConfigError::UnknownSampleId");
1099            }
1100        }
1101    }
1102}