1use crate::prelude::*;
13
14use alloc::collections::{BTreeMap, BTreeSet};
15use core::fmt;
16use core::marker::PhantomData;
17use core::str::FromStr;
18
19use serde::{Deserialize, Serialize};
20
21use crate::error::{ContentError, codes};
22use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
23use crate::ron::parse_ron;
24
25pub mod action_set;
26#[cfg(any(test, feature = "sample-actions"))]
27pub mod sample;
28#[cfg(test)]
29mod tests;
30
31pub use action_set::{ActionSet, UiAction};
32
33macro_rules! keys {
36 ($($variant:ident => $name:literal,)+) => {
37 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40 pub enum Key {
41 $(
42 #[doc = concat!("The `` ", $name, " `` key.")]
43 $variant,
44 )+
45 }
46
47 impl Key {
48 pub const ALL: &'static [Key] = &[$(Key::$variant),+];
50
51 pub fn name(self) -> &'static str {
56 match self {
57 $(Key::$variant => $name,)+
58 }
59 }
60 }
61 };
62}
63
64keys! {
65 A => "a",
66 B => "b",
67 C => "c",
68 D => "d",
69 E => "e",
70 F => "f",
71 G => "g",
72 H => "h",
73 I => "i",
74 J => "j",
75 K => "k",
76 L => "l",
77 M => "m",
78 N => "n",
79 O => "o",
80 P => "p",
81 Q => "q",
82 R => "r",
83 S => "s",
84 T => "t",
85 U => "u",
86 V => "v",
87 W => "w",
88 X => "x",
89 Y => "y",
90 Z => "z",
91 Digit0 => "0",
92 Digit1 => "1",
93 Digit2 => "2",
94 Digit3 => "3",
95 Digit4 => "4",
96 Digit5 => "5",
97 Digit6 => "6",
98 Digit7 => "7",
99 Digit8 => "8",
100 Digit9 => "9",
101 Semicolon => ";",
102 Up => "Up",
103 Down => "Down",
104 Left => "Left",
105 Right => "Right",
106 Enter => "Enter",
107 Escape => "Escape",
108 Space => "Space",
109 Tab => "Tab",
110 Backspace => "Backspace",
111 F1 => "F1",
112 F2 => "F2",
113 F3 => "F3",
114 F4 => "F4",
115 F5 => "F5",
116 F6 => "F6",
117 F7 => "F7",
118 F8 => "F8",
119 F9 => "F9",
120 F10 => "F10",
121 F11 => "F11",
122 F12 => "F12",
123 Comma => ",",
124 Period => ".",
125 Slash => "/",
126 Apostrophe => "'",
127 LeftBracket => "[",
128 RightBracket => "]",
129 Backslash => "\\",
130 Minus => "-",
131 Equal => "=",
132 Backquote => "`",
133 Insert => "Insert",
134 Delete => "Delete",
135 Home => "Home",
136 End => "End",
137 PageUp => "PageUp",
138 PageDown => "PageDown",
139 Kp0 => "Kp0",
140 Kp1 => "Kp1",
141 Kp2 => "Kp2",
142 Kp3 => "Kp3",
143 Kp4 => "Kp4",
144 Kp5 => "Kp5",
145 Kp6 => "Kp6",
146 Kp7 => "Kp7",
147 Kp8 => "Kp8",
148 Kp9 => "Kp9",
149 KpAdd => "Kp+",
150 KpSubtract => "Kp-",
151 KpMultiply => "Kp*",
152 KpDivide => "Kp/",
153 KpDecimal => "Kp.",
154}
155
156impl Key {
157 pub fn from_name(name: &str) -> Option<Self> {
159 Self::ALL.iter().copied().find(|k| k.name() == name)
160 }
161}
162
163pub const SLOTS: usize = 3;
167
168impl fmt::Display for Key {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 f.write_str(self.name())
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
177pub struct Chord {
178 pub key: Key,
180 pub shift: bool,
182}
183
184const SHIFT_PREFIX: &str = "Shift+";
186
187impl Chord {
188 pub const fn plain(key: Key) -> Self {
190 Self { key, shift: false }
191 }
192
193 pub const fn shifted(key: Key) -> Self {
195 Self { key, shift: true }
196 }
197
198 pub fn is_reserved(self) -> bool {
203 !self.shift && matches!(self.key, Key::Escape | Key::Delete)
204 }
205
206 pub fn parse(s: &str) -> Result<Self, String> {
208 let (shift, name) = match s.strip_prefix(SHIFT_PREFIX) {
209 Some(rest) => (true, rest),
210 None => (false, s),
211 };
212 match Key::from_name(name) {
213 Some(key) => Ok(Self { key, shift }),
214 None => Err(unknown_key_message(s, name)),
215 }
216 }
217}
218
219fn unknown_key_message(chord: &str, name: &str) -> String {
220 let hint = if name.len() == 1 && name.bytes().all(|b| b.is_ascii_uppercase()) {
221 format!(
222 " (letters are lowercase; write \"{SHIFT_PREFIX}{}\" for a shifted letter)",
223 name.to_ascii_lowercase()
224 )
225 } else {
226 String::new()
227 };
228 format!(
229 "unknown key chord \"{chord}\"{hint}; expected a key such as \"h\", \"0\", \";\", \"Left\", \
230 \"Enter\", \"Escape\", \"Space\", \"Tab\", \"Backspace\" or \"F1\", optionally \
231 prefixed by \"{SHIFT_PREFIX}\""
232 )
233}
234
235impl FromStr for Chord {
236 type Err = String;
237
238 fn from_str(s: &str) -> Result<Self, Self::Err> {
239 Self::parse(s)
240 }
241}
242
243impl fmt::Display for Chord {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 if self.shift {
246 f.write_str(SHIFT_PREFIX)?;
247 }
248 f.write_str(self.key.name())
249 }
250}
251
252macro_rules! buttons {
255 ($($variant:ident => $doc:literal,)+) => {
256 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
270 pub enum Button {
271 $(
272 #[doc = $doc]
273 $variant,
274 )+
275 }
276
277 impl Button {
278 pub const ALL: &'static [Button] = &[$(Button::$variant),+];
280
281 pub fn name(self) -> &'static str {
283 match self {
284 $(Button::$variant => stringify!($variant),)+
285 }
286 }
287 }
288 };
289}
290
291buttons! {
292 South => "The bottom face button (Xbox `A`).",
293 East => "The right face button (Xbox `B`).",
294 West => "The left face button (Xbox `X`).",
295 North => "The top face button (Xbox `Y`).",
296 LeftShoulder => "The left shoulder button (Xbox `LB`).",
297 RightShoulder => "The right shoulder button (Xbox `RB`).",
298 LeftTrigger => "The left trigger (Xbox `LT`), pulled far enough to count as pressed.",
299 RightTrigger => "The right trigger (Xbox `RT`), pulled far enough to count as pressed.",
300 Select => "The left centre button (Xbox `Back`).",
301 Start => "The right centre button (Xbox `Start`).",
302 LeftStickPress => "Pressing the left stick in.",
303 RightStickPress => "Pressing the right stick in.",
304 DpadUp => "D-pad up.",
305 DpadDown => "D-pad down.",
306 DpadLeft => "D-pad left.",
307 DpadRight => "D-pad right.",
308 LeftStickUp => "The left stick pushed up.",
309 LeftStickDown => "The left stick pushed down.",
310 LeftStickLeft => "The left stick pushed left.",
311 LeftStickRight => "The left stick pushed right.",
312 RightStickUp => "The right stick pushed up.",
313 RightStickDown => "The right stick pushed down.",
314 RightStickLeft => "The right stick pushed left.",
315 RightStickRight => "The right stick pushed right.",
316 CrankBack => "The crank wound one notch backwards (counterclockwise).",
317 CrankForward => "The crank wound one notch forwards (clockwise).",
318 SystemMenu0 => "The first item of the game's own in the system's menu.",
319 SystemMenu1 => "The second item of the game's own in the system's menu.",
320 SystemMenu2 => "The third item of the game's own in the system's menu.",
321 SystemMenu3 => "The fourth item of the game's own in the system's menu.",
322 SystemMenu4 => "The fifth item of the game's own in the system's menu.",
323 SystemMenu5 => "The sixth item of the game's own in the system's menu.",
324 SystemMenu6 => "The seventh item of the game's own in the system's menu.",
325 SystemMenu7 => "The eighth item of the game's own in the system's menu.",
326}
327
328pub const SYSTEM_MENU_ITEMS: u8 = 8;
332
333impl Button {
334 pub fn system_menu(slot: u8) -> Option<Self> {
337 let first = Self::ALL.iter().position(|&b| b == Self::SystemMenu0)?;
338 Self::ALL.get(first + usize::from(slot)).copied()
339 }
340
341 pub fn system_menu_slot(self) -> Option<u8> {
343 (0..SYSTEM_MENU_ITEMS).find(|&slot| Self::system_menu(slot) == Some(self))
344 }
345
346 pub fn from_name(name: &str) -> Option<Self> {
348 Self::ALL.iter().copied().find(|b| b.name() == name)
349 }
350
351 pub fn parse(s: &str) -> Result<Self, String> {
354 Self::from_name(s).ok_or_else(|| {
355 let known: Vec<&str> = Self::ALL.iter().map(|b| b.name()).collect();
356 format!(
357 "unknown button \"{s}\"; known buttons: {}",
358 known.join(", ")
359 )
360 })
361 }
362}
363
364impl FromStr for Button {
365 type Err = String;
366
367 fn from_str(s: &str) -> Result<Self, Self::Err> {
368 Self::parse(s)
369 }
370}
371
372impl Serialize for Button {
375 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
376 serializer.serialize_str(self.name())
377 }
378}
379
380impl<'de> Deserialize<'de> for Button {
381 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382 let name = String::deserialize(deserializer)?;
383 Self::parse(&name).map_err(serde::de::Error::custom)
384 }
385}
386
387impl fmt::Display for Button {
388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389 f.write_str(self.name())
390 }
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
400pub struct ButtonChord {
401 first: Button,
402 second: Option<Button>,
403}
404
405const CHORD_JOIN: char = '+';
407
408impl ButtonChord {
409 pub const fn single(button: Button) -> Self {
411 Self {
412 first: button,
413 second: None,
414 }
415 }
416
417 pub fn pair(a: Button, b: Button) -> Option<Self> {
420 let (first, second) = match a.cmp(&b) {
421 core::cmp::Ordering::Less => (a, b),
422 core::cmp::Ordering::Greater => (b, a),
423 core::cmp::Ordering::Equal => return None,
424 };
425 Some(Self {
426 first,
427 second: Some(second),
428 })
429 }
430
431 pub fn first(self) -> Button {
433 self.first
434 }
435
436 pub fn single_button(self) -> Option<Button> {
438 match self.second {
439 None => Some(self.first),
440 Some(_) => None,
441 }
442 }
443
444 pub fn buttons(self) -> Option<(Button, Button)> {
446 self.second.map(|second| (self.first, second))
447 }
448
449 pub fn partner(self, button: Button) -> Option<Button> {
451 let (a, b) = self.buttons()?;
452 if a == button {
453 Some(b)
454 } else if b == button {
455 Some(a)
456 } else {
457 None
458 }
459 }
460
461 pub fn parse(s: &str) -> Result<Self, String> {
463 let Some((a, b)) = s.split_once(CHORD_JOIN) else {
464 return Button::parse(s).map(Self::single);
465 };
466 let (a, b) = (Button::parse(a.trim())?, Button::parse(b.trim())?);
467 Self::pair(a, b).ok_or_else(|| format!("\"{s}\" names the same button twice"))
468 }
469}
470
471impl From<Button> for ButtonChord {
472 fn from(button: Button) -> Self {
473 Self::single(button)
474 }
475}
476
477impl PartialEq<Button> for ButtonChord {
479 fn eq(&self, button: &Button) -> bool {
480 self.single_button() == Some(*button)
481 }
482}
483
484impl FromStr for ButtonChord {
485 type Err = String;
486
487 fn from_str(s: &str) -> Result<Self, Self::Err> {
488 Self::parse(s)
489 }
490}
491
492impl fmt::Display for ButtonChord {
493 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494 f.write_str(self.first.name())?;
495 if let Some(second) = self.second {
496 write!(f, "{CHORD_JOIN}{}", second.name())?;
497 }
498 Ok(())
499 }
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
504#[serde(deny_unknown_fields)]
505pub struct RepeatDef {
506 pub delay_ms: u32,
508 pub interval_ms: u32,
510}
511
512impl Default for RepeatDef {
513 fn default() -> Self {
514 Self {
515 delay_ms: 300,
516 interval_ms: 55,
517 }
518 }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
526#[serde(deny_unknown_fields)]
527pub struct StickDef {
528 pub press_percent: u8,
530 pub release_percent: u8,
532}
533
534impl Default for StickDef {
535 fn default() -> Self {
536 Self {
537 press_percent: 50,
538 release_percent: 35,
539 }
540 }
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
547#[serde(deny_unknown_fields)]
548pub struct ChordDef {
549 pub window_ms: u32,
552}
553
554impl Default for ChordDef {
555 fn default() -> Self {
556 Self { window_ms: 80 }
557 }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
564#[serde(deny_unknown_fields)]
565pub struct CrankDef {
566 pub notch_degrees: u16,
568}
569
570impl Default for CrankDef {
571 fn default() -> Self {
572 Self { notch_degrees: 45 }
573 }
574}
575
576pub const LAYOUT_NAME_MAX: usize = 24;
578
579#[derive(Clone, Copy, PartialEq, Eq, Hash)]
585pub struct Layout {
586 len: usize,
587 bytes: [u8; LAYOUT_NAME_MAX],
588}
589
590impl Layout {
591 pub const fn named(name: &'static str) -> Self {
594 match Self::try_named(name) {
595 Some(layout) => layout,
596 None => panic!("a layout name is 1 to 24 ASCII letters, digits or _"),
597 }
598 }
599
600 pub fn from_name(name: &str) -> Option<Self> {
604 Self::try_named(name)
605 }
606
607 const fn try_named(name: &str) -> Option<Self> {
608 let given = name.as_bytes();
609 if given.is_empty() || given.len() > LAYOUT_NAME_MAX {
610 return None;
611 }
612 let mut bytes = [0; LAYOUT_NAME_MAX];
613 let mut i = 0;
614 while i < given.len() {
615 let b = given[i];
616 if !(b.is_ascii_alphanumeric() || b == b'_') {
617 return None;
618 }
619 bytes[i] = b;
620 i += 1;
621 }
622 Some(Self {
623 len: given.len(),
624 bytes,
625 })
626 }
627
628 pub fn name(&self) -> &str {
630 let bytes = self.bytes.get(..self.len).unwrap_or_default();
631 core::str::from_utf8(bytes).unwrap_or_default()
633 }
634}
635
636impl Ord for Layout {
637 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
638 self.name().cmp(other.name())
639 }
640}
641
642impl PartialOrd for Layout {
643 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
644 Some(self.cmp(other))
645 }
646}
647
648impl fmt::Debug for Layout {
649 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650 write!(f, "Layout({})", self.name())
651 }
652}
653
654impl fmt::Display for Layout {
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 f.write_str(self.name())
657 }
658}
659
660impl Serialize for Layout {
662 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
663 serializer.serialize_str(self.name())
664 }
665}
666
667impl<'de> Deserialize<'de> for Layout {
668 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
669 let name = String::deserialize(deserializer)?;
670 Self::from_name(&name)
671 .ok_or_else(|| serde::de::Error::custom(format!("{name:?} is not a layout name")))
672 }
673}
674
675pub type Bindings<A> = BTreeMap<Chord, A>;
678
679pub type LayoutKeys<A> = BTreeMap<A, Vec<Chord>>;
687
688pub type PadKeys<A> = BTreeMap<A, Vec<ButtonChord>>;
693
694#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct KeymapDef<A: ActionSet> {
699 pub layouts: BTreeMap<Layout, LayoutKeys<A>>,
703 pub layout_names: LayoutNames,
706 pub layout_picker: Bindings<A>,
709 pub pad: PadKeys<A>,
713 pub playdate: PadKeys<A>,
720 pub targets: BTreeMap<String, PadKeys<A>>,
727 pub repeat: RepeatDef,
729 pub chord: ChordDef,
731 pub stick: StickDef,
733 pub crank: CrankDef,
735}
736
737impl<A: ActionSet> Default for KeymapDef<A> {
738 fn default() -> Self {
739 Self {
740 layouts: BTreeMap::new(),
741 layout_names: Vec::new(),
742 layout_picker: BTreeMap::new(),
743 pad: BTreeMap::new(),
744 playdate: BTreeMap::new(),
745 targets: BTreeMap::new(),
746 repeat: RepeatDef::default(),
747 chord: ChordDef::default(),
748 stick: StickDef::default(),
749 crank: CrankDef::default(),
750 }
751 }
752}
753
754impl<A: ActionSet> AssetKind for KeymapDef<A> {
755 const NAME: &'static str = "keymap";
756}
757
758pub struct KeymapRon<A>(PhantomData<fn() -> A>);
761
762impl<A> KeymapRon<A> {
763 pub const fn new() -> Self {
765 Self(PhantomData)
766 }
767}
768
769impl<A> Default for KeymapRon<A> {
770 fn default() -> Self {
771 Self::new()
772 }
773}
774
775impl<A> fmt::Debug for KeymapRon<A> {
776 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
777 f.write_str("KeymapRon")
778 }
779}
780
781impl<A: ActionSet> Format for KeymapRon<A> {
782 type Asset = KeymapDef<A>;
783
784 fn id(&self) -> &'static str {
785 "keymap"
786 }
787
788 fn extensions(&self) -> &'static [&'static str] {
789 &["ron"]
790 }
791
792 fn max_bytes(&self) -> usize {
793 TEXT_MAX_BYTES
794 }
795
796 fn decode(
797 &self,
798 file: &AssetPath,
799 bytes: &[u8],
800 _cx: &DecodeCx<'_>,
801 ) -> Result<KeymapDef<A>, Vec<ContentError>> {
802 KeymapDef::from_source(file.shown(), text(file, bytes)?)
803 }
804}
805
806pub type LayoutNames = Vec<(Layout, String)>;
808
809#[derive(Deserialize)]
811#[serde(deny_unknown_fields)]
812struct RawKeymap {
813 layouts: Vec<RawLayout>,
814 layout_picker: BTreeMap<String, Vec<String>>,
815 pad: BTreeMap<String, Vec<String>>,
816 #[serde(default)]
818 playdate: BTreeMap<String, Vec<String>>,
819 #[serde(default)]
822 targets: BTreeMap<String, BTreeMap<String, Vec<String>>>,
823 repeat: RepeatDef,
824 #[serde(default)]
825 chord: ChordDef,
826 stick: StickDef,
827 #[serde(default)]
828 crank: CrankDef,
829}
830
831#[derive(Deserialize)]
833#[serde(deny_unknown_fields)]
834struct RawLayout {
835 id: String,
837 name: String,
839 keys: BTreeMap<String, Vec<String>>,
841}
842
843impl<A: ActionSet> KeymapDef<A> {
844 pub fn bindings(&self, layout: Layout, group: A::Group) -> Option<Bindings<A>> {
847 let keys = self.layouts.get(&layout)?;
848 Some(
849 keys.iter()
850 .filter(|(a, _)| a.group() == group)
851 .flat_map(|(&a, chords)| chords.iter().map(move |&c| (c, a)))
852 .collect(),
853 )
854 }
855
856 pub fn chords(&self, layout: Layout, action: A) -> &[Chord] {
859 self.layouts
860 .get(&layout)
861 .and_then(|keys| keys.get(&action))
862 .map_or(&[], Vec::as_slice)
863 }
864
865 pub fn buttons(&self, action: A) -> &[ButtonChord] {
869 self.pad.get(&action).map_or(&[], Vec::as_slice)
870 }
871
872 #[must_use]
875 pub fn for_playdate(mut self) -> Self {
876 self.pad.clone_from(&self.playdate);
877 self
878 }
879
880 pub fn target_buttons(&self, id: &str) -> Option<&PadKeys<A>> {
883 self.targets.get(id)
884 }
885
886 #[must_use]
889 pub fn for_target(mut self, id: &str) -> Self {
890 if let Some(buttons) = self.targets.get(id) {
891 self.pad = buttons.clone();
892 }
893 self
894 }
895
896 pub fn has_layout(&self, layout: Layout) -> bool {
899 self.layouts.contains_key(&layout)
900 }
901
902 pub fn layout_ids(&self) -> impl Iterator<Item = Layout> + '_ {
904 self.layout_names.iter().map(|&(layout, _)| layout)
905 }
906
907 pub fn first_layout(&self) -> Option<Layout> {
910 self.layout_ids().next()
911 }
912
913 pub fn layout_text(&self, layout: Layout) -> Option<&str> {
915 let (_, text) = self.layout_names.iter().find(|&&(l, _)| l == layout)?;
916 Some(text)
917 }
918
919 pub fn from_source(file: &str, source: &str) -> Result<Self, Vec<ContentError>> {
934 let raw: RawKeymap = parse_ron(file, source).map_err(|e| vec![e])?;
935 let mut errors = Vec::new();
936 let (layouts, layout_names) =
937 validate_layouts::<A>(file, source, &raw.layouts, &mut errors);
938 let picker_line = line_of_quoted(source, 0, "layout_picker");
939 let from = picker_line.map_or(0, |line| usize::try_from(line).unwrap_or(0));
940 let layout_picker = match validate_bindings::<A, Chord>(
941 file,
942 source,
943 from,
944 "layout_picker",
945 false,
946 &raw.layout_picker,
947 ) {
948 Ok(keys) => keys
949 .iter()
950 .flat_map(|(&a, chords)| chords.iter().map(move |&c| (c, a)))
951 .collect(),
952 Err(e) => {
953 errors.extend(e);
954 Bindings::new()
955 }
956 };
957 let pad = validate_pad::<A>(file, source, "pad", "pad", &raw.pad, &mut errors);
958 let mut targets = validate_targets::<A>(file, source, &raw.targets, &mut errors);
959 let playdate = if raw.playdate.is_empty() {
960 targets
961 .get(PLAYDATE)
962 .cloned()
963 .unwrap_or_else(|| pad.clone())
964 } else {
965 if raw.targets.contains_key(PLAYDATE) {
966 errors.push(positioned(
967 ContentError::new(
968 file,
969 "the Playdate's buttons are given twice: as `playdate` and as \
970 `targets.\"playdate\"` (keep one)",
971 ),
972 line_of_quoted(source, 0, PLAYDATE),
973 ));
974 }
975 let table =
976 validate_pad::<A>(file, source, PLAYDATE, PLAYDATE, &raw.playdate, &mut errors);
977 targets.insert(PLAYDATE.to_owned(), table.clone());
978 table
979 };
980 if raw.repeat.interval_ms == 0 {
981 errors.push(positioned(
982 ContentError::new(file, "repeat interval_ms must be at least 1")
983 .with_code(codes::OUT_OF_RANGE),
984 line_of_quoted(source, 0, "repeat"),
985 ));
986 }
987 let StickDef {
988 press_percent,
989 release_percent,
990 } = raw.stick;
991 if release_percent == 0 || release_percent >= press_percent || press_percent > 100 {
992 errors.push(positioned(
993 ContentError::new(
994 file,
995 "stick release_percent must be at least 1 and below press_percent, which \
996 must be at most 100",
997 ),
998 line_of_quoted(source, 0, "stick"),
999 ));
1000 }
1001 if !(1..=360).contains(&raw.crank.notch_degrees) {
1002 errors.push(positioned(
1003 ContentError::new(file, "crank notch_degrees must be 1 to 360")
1004 .with_code(codes::OUT_OF_RANGE),
1005 line_of_quoted(source, 0, "crank"),
1006 ));
1007 }
1008 if errors.is_empty() {
1009 Ok(Self {
1010 layouts,
1011 layout_names,
1012 layout_picker,
1013 pad,
1014 playdate,
1015 targets,
1016 repeat: raw.repeat,
1017 chord: raw.chord,
1018 stick: raw.stick,
1019 crank: raw.crank,
1020 })
1021 } else {
1022 Err(errors)
1023 }
1024 }
1025}
1026
1027fn validate_layouts<A: ActionSet>(
1032 file: &str,
1033 source: &str,
1034 raw: &[RawLayout],
1035 errors: &mut Vec<ContentError>,
1036) -> (BTreeMap<Layout, LayoutKeys<A>>, LayoutNames) {
1037 let mut layouts = BTreeMap::new();
1038 let mut layout_names = Vec::new();
1039 if raw.is_empty() {
1040 errors.push(positioned(
1041 ContentError::new(file, "layouts: list at least one layout"),
1042 line_of_quoted(source, 0, "layouts"),
1043 ));
1044 }
1045 let mut listed = BTreeSet::new();
1046 let mut search_from = 0;
1047 for RawLayout { id, name, keys } in raw {
1048 let start = line_of_quoted(source, search_from, id);
1049 let from = start.map_or(search_from, |line| {
1052 usize::try_from(line).unwrap_or(search_from)
1053 });
1054 search_from = from;
1055 let Some(layout) = Layout::from_name(id) else {
1056 let message = format!(
1057 "layout id \"{id}\" must be 1 to {LAYOUT_NAME_MAX} ASCII letters, digits or _"
1058 );
1059 errors.push(positioned(ContentError::new(file, message), start));
1060 continue;
1061 };
1062 if !listed.insert(layout) {
1063 let message = format!("layout \"{layout}\" is listed twice");
1064 errors.push(positioned(
1065 ContentError::new(file, message).with_code(codes::DUPLICATE),
1066 start,
1067 ));
1068 continue;
1069 }
1070 let label = format!("layout \"{layout}\"");
1071 match validate_bindings::<A, Chord>(file, source, from, &label, true, keys) {
1072 Ok(keys) => {
1073 layouts.insert(layout, keys);
1074 layout_names.push((layout, name.clone()));
1075 }
1076 Err(e) => errors.extend(e),
1077 }
1078 }
1079 (layouts, layout_names)
1080}
1081
1082const PLAYDATE: &str = "playdate";
1084
1085const TARGET_ID_MAX: usize = 32;
1087
1088fn is_target_id(id: &str) -> bool {
1092 (1..=TARGET_ID_MAX).contains(&id.len())
1093 && id
1094 .bytes()
1095 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
1096}
1097
1098fn validate_targets<A: ActionSet>(
1102 file: &str,
1103 source: &str,
1104 raw: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
1105 errors: &mut Vec<ContentError>,
1106) -> BTreeMap<String, PadKeys<A>> {
1107 let mut targets = BTreeMap::new();
1108 for (id, table) in raw {
1109 if !is_target_id(id) {
1110 let message = format!(
1111 "targets: target id \"{id}\" must be 1 to {TARGET_ID_MAX} lowercase ASCII \
1112 letters, digits or -"
1113 );
1114 errors.push(positioned(
1115 ContentError::new(file, message),
1116 line_of_quoted(source, 0, id),
1117 ));
1118 continue;
1119 }
1120 let label = format!("targets.\"{id}\"");
1121 let before = errors.len();
1122 let buttons = validate_pad::<A>(file, source, &label, id, table, errors);
1123 if errors.len() == before {
1124 targets.insert(id.clone(), buttons);
1125 }
1126 }
1127 targets
1128}
1129
1130fn validate_pad<A: ActionSet>(
1135 file: &str,
1136 source: &str,
1137 label: &str,
1138 anchor: &str,
1139 raw: &BTreeMap<String, Vec<String>>,
1140 errors: &mut Vec<ContentError>,
1141) -> PadKeys<A> {
1142 let line = line_of_quoted(source, 0, anchor);
1143 let from = line.map_or(0, |line| usize::try_from(line).unwrap_or(0));
1144 let pad = match validate_bindings::<A, ButtonChord>(file, source, from, label, true, raw) {
1145 Ok(buttons) => buttons,
1146 Err(e) => {
1147 errors.extend(e);
1148 return PadKeys::new();
1149 }
1150 };
1151 for (&action, buttons) in &pad {
1152 if !action.is_rebindable() && !buttons.is_empty() {
1153 let message = format!(
1154 "{label}: action \"{}\" can't have a button: it can't be rebound, so it is \
1155 keyboard only",
1156 action.name()
1157 );
1158 errors.push(positioned(
1159 ContentError::new(file, message),
1160 line_of_quoted(source, from, action.name()),
1161 ));
1162 }
1163 }
1164 pad
1165}
1166
1167trait Bound: Copy + Ord + fmt::Display {
1170 const NOUN: &'static str;
1172 const PLURAL: &'static str;
1174
1175 fn parse_name(text: &str) -> Result<Self, String>;
1177
1178 fn refused(self) -> Option<&'static str>;
1180}
1181
1182impl Bound for Chord {
1183 const NOUN: &'static str = "chord";
1184 const PLURAL: &'static str = "keys";
1185
1186 fn parse_name(text: &str) -> Result<Self, String> {
1187 Self::parse(text)
1188 }
1189
1190 fn refused(self) -> Option<&'static str> {
1191 self.is_reserved()
1192 .then_some("Esc and Delete are fixed, see controls.md")
1193 }
1194}
1195
1196impl Bound for ButtonChord {
1197 const NOUN: &'static str = "button";
1198 const PLURAL: &'static str = "buttons";
1199
1200 fn parse_name(text: &str) -> Result<Self, String> {
1201 Self::parse(text)
1202 }
1203
1204 fn refused(self) -> Option<&'static str> {
1205 self.buttons()
1206 .is_some_and(|(a, b)| a.system_menu_slot().or(b.system_menu_slot()).is_some())
1207 .then_some("a system menu item is picked on its own, never with another button")
1208 }
1209}
1210
1211fn validate_bindings<A: ActionSet, T: Bound>(
1218 file: &str,
1219 source: &str,
1220 from: usize,
1221 label: &str,
1222 complete: bool,
1223 actions: &BTreeMap<String, Vec<String>>,
1224) -> Result<BTreeMap<A, Vec<T>>, Vec<ContentError>> {
1225 let noun = T::NOUN;
1226 let mut errors = Vec::new();
1227 let mut bindings: BTreeMap<(A::Group, T), A> = BTreeMap::new();
1228 let mut keys: BTreeMap<A, Vec<T>> = BTreeMap::new();
1229 let err_at = |key: &str, message: String| {
1230 positioned(
1231 ContentError::new(file, format!("{label}: {message}")),
1232 line_of_quoted(source, from, key),
1233 )
1234 };
1235 let coded_at = |key: &str, message: String, code| err_at(key, message).with_code(code);
1236 for (action_name, names) in actions {
1237 let Some(action) = A::from_name(action_name) else {
1238 let known: Vec<&str> = A::ALL.iter().map(|a| a.name()).collect();
1239 errors.push(coded_at(
1240 action_name,
1241 format!(
1242 "unknown action \"{action_name}\"; known actions: {}",
1243 known.join(", ")
1244 ),
1245 codes::UNKNOWN_NAME,
1246 ));
1247 continue;
1248 };
1249 if complete && names.len() > SLOTS {
1250 errors.push(err_at(
1251 action_name,
1252 format!(
1253 "action \"{}\" has {} {}; at most {SLOTS} are allowed",
1254 action.name(),
1255 names.len(),
1256 T::PLURAL
1257 ),
1258 ));
1259 }
1260 let name = action.name();
1261 let listed = keys.entry(action).or_default();
1262 for text in names {
1263 let bound = match T::parse_name(text) {
1264 Ok(bound) => bound,
1265 Err(why) => {
1266 errors.push(coded_at(
1267 action_name,
1268 format!("action \"{action_name}\": {why}"),
1269 codes::UNKNOWN_NAME,
1270 ));
1271 continue;
1272 }
1273 };
1274 if let Some(why) = bound.refused() {
1275 errors.push(err_at(
1276 action_name,
1277 format!("action \"{name}\": \"{bound}\" can't be bound here: {why}"),
1278 ));
1279 } else if let Some(&other) = bindings.get(&(action.group(), bound)) {
1280 let message = if other == action {
1281 format!("{noun} \"{bound}\" is listed twice for \"{name}\"")
1282 } else {
1283 format!(
1284 "{noun} \"{bound}\" is bound to both \"{}\" and \"{name}\"",
1285 other.name()
1286 )
1287 };
1288 errors.push(coded_at(action_name, message, codes::DUPLICATE));
1289 } else {
1290 bindings.insert((action.group(), bound), action);
1291 listed.push(bound);
1292 }
1293 }
1294 }
1295 for action in A::ALL {
1296 if complete && !actions.contains_key(action.name()) {
1297 errors.push(ContentError::new(
1298 file,
1299 format!(
1300 "{label}: missing action \"{}\" (list it with [] to leave it \
1301 unbound)",
1302 action.name()
1303 ),
1304 ));
1305 }
1306 }
1307 if errors.is_empty() {
1308 Ok(keys)
1309 } else {
1310 Err(errors)
1311 }
1312}
1313
1314fn positioned(error: ContentError, line: Option<u32>) -> ContentError {
1316 match line {
1317 Some(line) => error.at(line, None),
1318 None => error,
1319 }
1320}
1321
1322fn line_of_quoted(source: &str, from: usize, key: &str) -> Option<u32> {
1326 let quoted = format!("\"{key}\"");
1327 let field = format!("{key}:");
1328 let (index, _) = source
1329 .lines()
1330 .enumerate()
1331 .skip(from)
1332 .find(|(_, l)| l.contains("ed) || l.trim_start().starts_with(&field))?;
1333 u32::try_from(index + 1).ok()
1334}