1use std::collections::{BTreeMap, HashMap};
101
102use keymap_core::{KeyInput, Keymap, ParseKeyInputError};
103use keymap_seq::{SeqBindError, SequenceKeymap};
104use serde::Deserialize;
105
106pub const GLOBAL_LAYER: &str = "global";
115
116#[derive(Debug, Clone)]
118#[non_exhaustive]
119pub struct BuildOutput<A> {
120 pub layers: BTreeMap<String, Keymap<A>>,
131 pub sequences: SequenceKeymap<A>,
135 pub warnings: Vec<Warning>,
138}
139
140impl<A> BuildOutput<A> {
141 #[must_use]
153 pub fn global(&self) -> &Keymap<A> {
154 self.layers
155 .get(GLOBAL_LAYER)
156 .expect("the global layer is always inserted")
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162#[non_exhaustive]
163pub enum Warning {
164 Conflict {
171 chord: String,
173 contenders: Vec<String>,
175 winner: String,
177 },
178 UnknownAction {
181 key: String,
184 action: String,
186 },
187 PrefixShadow {
192 prefix: Vec<String>,
194 prefix_action: String,
196 shadowed: Vec<String>,
198 shadowed_action: String,
200 },
201 EmptySequence {
203 action: String,
205 },
206 SequenceShadow {
218 chord: String,
220 chord_action: String,
222 sequence: Vec<String>,
226 sequence_action: String,
228 },
229}
230
231#[derive(Debug)]
233#[non_exhaustive]
234pub enum BuildError {
235 Toml(toml::de::Error),
237 KeyParse {
239 key: String,
241 source: ParseKeyInputError,
243 },
244}
245
246impl core::fmt::Display for BuildError {
247 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
248 match self {
249 BuildError::Toml(_) => f.write_str("invalid TOML"),
250 BuildError::KeyParse { key, .. } => write!(f, "invalid key string {key:?}"),
251 }
252 }
253}
254
255impl std::error::Error for BuildError {
256 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
257 match self {
258 BuildError::Toml(e) => Some(e),
259 BuildError::KeyParse { source, .. } => Some(source),
260 }
261 }
262}
263
264impl From<toml::de::Error> for BuildError {
265 fn from(e: toml::de::Error) -> Self {
266 BuildError::Toml(e)
267 }
268}
269
270#[derive(Deserialize)]
276#[serde(deny_unknown_fields)]
277struct RawConfig {
278 #[serde(default)]
279 keys: BTreeMap<String, String>,
280 #[serde(default)]
281 layers: BTreeMap<String, BTreeMap<String, String>>,
282 #[serde(default)]
283 sequences: Vec<RawSequence>,
284}
285
286#[derive(Deserialize)]
287#[serde(deny_unknown_fields)]
288struct RawSequence {
289 keys: Vec<String>,
290 action: String,
291}
292
293type SequenceBuild<A> = (SequenceKeymap<A>, HashMap<Vec<KeyInput>, String>);
296
297pub fn from_str<A, F>(toml_str: &str, mut resolve: F) -> Result<BuildOutput<A>, BuildError>
309where
310 F: FnMut(&str) -> Option<A>,
311{
312 let RawConfig {
313 keys,
314 mut layers,
315 sequences: raw_sequences,
316 } = toml::from_str(toml_str)?;
317
318 let mut warnings = Vec::new();
319 let mut built: BTreeMap<String, Keymap<A>> = BTreeMap::new();
320
321 let explicit_global = layers.remove(GLOBAL_LAYER).unwrap_or_default();
328 let global_entries = keys.into_iter().chain(explicit_global);
329 let (global_keymap, global_names) = build_layer(global_entries, &mut resolve, &mut warnings)?;
330 built.insert(GLOBAL_LAYER.to_string(), global_keymap);
331
332 for (name, raw_keys) in layers {
336 let (keymap, _names) = build_layer(raw_keys, &mut resolve, &mut warnings)?;
339 built.insert(name, keymap);
340 }
341
342 let (sequences, seq_names) = build_sequences(raw_sequences, &mut resolve, &mut warnings)?;
345 detect_cross_shadows(&global_names, &seq_names, &mut warnings);
346
347 Ok(BuildOutput {
348 layers: built,
349 sequences,
350 warnings,
351 })
352}
353
354fn build_layer<A, I, F>(
367 entries: I,
368 resolve: &mut F,
369 warnings: &mut Vec<Warning>,
370) -> Result<(Keymap<A>, HashMap<KeyInput, String>), BuildError>
371where
372 I: IntoIterator<Item = (String, String)>,
373 F: FnMut(&str) -> Option<A>,
374{
375 let mut order: Vec<KeyInput> = Vec::new();
378 let mut groups: HashMap<KeyInput, Vec<(String, String)>> = HashMap::new();
379 for (raw_key, action_name) in entries {
380 let chord = raw_key
381 .parse::<KeyInput>()
382 .map_err(|source| BuildError::KeyParse {
383 key: raw_key.clone(),
384 source,
385 })?;
386 let entry = groups.entry(chord).or_default();
387 if entry.is_empty() {
388 order.push(chord);
389 }
390 entry.push((raw_key, action_name));
391 }
392
393 let mut keymap = Keymap::new();
394 let mut names: HashMap<KeyInput, String> = HashMap::new();
395 for chord in order {
396 let Some(entries) = groups.remove(&chord) else {
397 continue;
398 };
399
400 let mut resolved: Vec<(String, A)> = Vec::new();
401 for (raw_key, action_name) in entries {
402 match resolve(&action_name) {
403 Some(action) => resolved.push((action_name, action)),
404 None => warnings.push(Warning::UnknownAction {
405 key: raw_key,
406 action: action_name,
407 }),
408 }
409 }
410
411 if resolved.len() > 1 {
412 let contenders: Vec<String> = resolved.iter().map(|(name, _)| name.clone()).collect();
413 if let Some(winner) = contenders.last().cloned() {
414 warnings.push(Warning::Conflict {
415 chord: chord.to_string(),
416 contenders,
417 winner,
418 });
419 }
420 }
421
422 if let Some((name, action)) = resolved.pop() {
424 keymap.bind(chord, action);
425 names.insert(chord, name);
426 }
427 }
428
429 Ok((keymap, names))
430}
431
432pub fn to_toml<A, F>(keymap: &Keymap<A>, sequences: &SequenceKeymap<A>, mut name_of: F) -> String
472where
473 F: FnMut(&A) -> Option<&str>,
474{
475 let mut root = toml::Table::new();
476
477 let keys = keymap_to_table(keymap, &mut name_of);
478 if !keys.is_empty() {
479 root.insert("keys".to_string(), toml::Value::Table(keys));
480 }
481 insert_sequences(&mut root, sequences, &mut name_of);
482
483 toml::to_string(&root).expect("string-only TOML value always serializes")
485}
486
487pub fn to_toml_layered<A, F>(
502 layers: &BTreeMap<String, Keymap<A>>,
503 sequences: &SequenceKeymap<A>,
504 mut name_of: F,
505) -> String
506where
507 F: FnMut(&A) -> Option<&str>,
508{
509 let mut root = toml::Table::new();
510 let mut named = toml::Table::new();
511
512 for (name, keymap) in layers {
513 let table = keymap_to_table(keymap, &mut name_of);
514 if table.is_empty() {
515 continue;
516 }
517 if name == GLOBAL_LAYER {
518 root.insert("keys".to_string(), toml::Value::Table(table));
519 } else {
520 named.insert(name.clone(), toml::Value::Table(table));
521 }
522 }
523
524 insert_sequences(&mut root, sequences, &mut name_of);
525 if !named.is_empty() {
526 root.insert("layers".to_string(), toml::Value::Table(named));
527 }
528
529 toml::to_string(&root).expect("string-only TOML value always serializes")
531}
532
533fn keymap_to_table<A, F>(keymap: &Keymap<A>, name_of: &mut F) -> toml::Table
537where
538 F: FnMut(&A) -> Option<&str>,
539{
540 let mut table = toml::Table::new();
541 for (chord, action) in keymap.iter() {
542 if let Some(name) = name_of(action) {
543 table.insert(chord.to_string(), toml::Value::String(name.to_string()));
544 }
545 }
546 table
547}
548
549fn insert_sequences<A, F>(root: &mut toml::Table, sequences: &SequenceKeymap<A>, name_of: &mut F)
553where
554 F: FnMut(&A) -> Option<&str>,
555{
556 let mut seqs: Vec<(Vec<String>, String)> = sequences
557 .bindings()
558 .filter_map(|(path, action)| {
559 name_of(action).map(|name| (render_sequence(&path), name.to_string()))
560 })
561 .collect();
562 seqs.sort_by_key(|(chords, _)| chords.join(" "));
563
564 if seqs.is_empty() {
565 return;
566 }
567 let array = seqs
568 .into_iter()
569 .map(|(chords, name)| {
570 let mut entry = toml::Table::new();
571 entry.insert(
572 "keys".to_string(),
573 toml::Value::Array(chords.into_iter().map(toml::Value::String).collect()),
574 );
575 entry.insert("action".to_string(), toml::Value::String(name));
576 toml::Value::Table(entry)
577 })
578 .collect();
579 root.insert("sequences".to_string(), toml::Value::Array(array));
580}
581
582fn detect_cross_shadows(
591 single_names: &HashMap<KeyInput, String>,
592 seq_names: &HashMap<Vec<KeyInput>, String>,
593 warnings: &mut Vec<Warning>,
594) {
595 let mut singles: Vec<(&KeyInput, &String)> = single_names.iter().collect();
596 singles.sort_by_key(|(chord, _)| chord.to_string());
597
598 for (chord, chord_action) in singles {
599 let mut shadowed: Vec<(&Vec<KeyInput>, &String)> = seq_names
600 .iter()
601 .filter(|(seq, _)| seq.first() == Some(chord))
602 .collect();
603 shadowed.sort_by_key(|(seq, _)| render_sequence(seq).join(" "));
604
605 if let Some((sequence, sequence_action)) = shadowed.first() {
606 warnings.push(Warning::SequenceShadow {
607 chord: chord.to_string(),
608 chord_action: chord_action.clone(),
609 sequence: render_sequence(sequence),
610 sequence_action: (*sequence_action).clone(),
611 });
612 }
613 }
614}
615
616fn build_sequences<A, F>(
635 raw_sequences: Vec<RawSequence>,
636 resolve: &mut F,
637 warnings: &mut Vec<Warning>,
638) -> Result<SequenceBuild<A>, BuildError>
639where
640 F: FnMut(&str) -> Option<A>,
641{
642 let mut sequences = SequenceKeymap::new();
643 let mut names: HashMap<Vec<KeyInput>, String> = HashMap::new();
646
647 for raw_seq in raw_sequences {
648 let mut keys = Vec::with_capacity(raw_seq.keys.len());
649 for raw_key in &raw_seq.keys {
650 let chord = raw_key
651 .parse::<KeyInput>()
652 .map_err(|source| BuildError::KeyParse {
653 key: raw_key.clone(),
654 source,
655 })?;
656 keys.push(chord);
657 }
658
659 let Some(action) = resolve(&raw_seq.action) else {
660 warnings.push(Warning::UnknownAction {
661 key: render_sequence(&keys).join(" "),
662 action: raw_seq.action,
663 });
664 continue;
665 };
666
667 match sequences.bind(keys.iter().copied(), action) {
668 Ok(None) => {
669 names.insert(keys, raw_seq.action);
670 }
671 Ok(Some(_)) => {
672 let previous = names.insert(keys.clone(), raw_seq.action.clone());
674 warnings.push(Warning::Conflict {
675 chord: render_sequence(&keys).join(" "),
676 contenders: vec![previous.unwrap_or_default(), raw_seq.action.clone()],
677 winner: raw_seq.action,
678 });
679 }
680 Err(SeqBindError::Empty) => {
681 warnings.push(Warning::EmptySequence {
682 action: raw_seq.action,
683 });
684 }
685 Err(SeqBindError::PrefixShadow { sequence, conflict }) => {
686 let conflict_action = names.get(&conflict).cloned().unwrap_or_default();
687 let (prefix, prefix_action, shadowed, shadowed_action) =
689 if sequence.len() <= conflict.len() {
690 (sequence, raw_seq.action, conflict, conflict_action)
691 } else {
692 (conflict, conflict_action, sequence, raw_seq.action)
693 };
694 warnings.push(Warning::PrefixShadow {
695 prefix: render_sequence(&prefix),
696 prefix_action,
697 shadowed: render_sequence(&shadowed),
698 shadowed_action,
699 });
700 }
701 Err(_) => {}
704 }
705 }
706
707 Ok((sequences, names))
708}
709
710fn render_sequence(keys: &[KeyInput]) -> Vec<String> {
712 keys.iter().map(ToString::to_string).collect()
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use keymap_core::{Key, Modifiers};
719
720 #[derive(Debug, Clone, PartialEq)]
721 enum Action {
722 Quit,
723 Save,
724 Split,
725 Top,
726 }
727
728 fn resolver(name: &str) -> Option<Action> {
729 match name {
730 "quit" => Some(Action::Quit),
731 "save" => Some(Action::Save),
732 "split" => Some(Action::Split),
733 "top" => Some(Action::Top),
734 _ => None,
735 }
736 }
737
738 use keymap_seq::Match;
739
740 fn seq(keys: &[(char, Modifiers)]) -> Vec<KeyInput> {
741 keys.iter()
742 .map(|&(c, m)| KeyInput::new(Key::Char(c), m))
743 .collect()
744 }
745
746 #[test]
747 fn builds_bindings_and_resolves_actions() {
748 let toml = "[keys]\n\"ctrl+q\" = \"quit\"\n\"ctrl+s\" = \"save\"\n";
749 let out = from_str(toml, resolver).unwrap();
750 assert!(out.warnings.is_empty());
751 let q = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
752 assert_eq!(out.global().get(&q), Some(&Action::Quit));
753 }
754
755 #[test]
756 fn bare_keys_build_the_global_layer_which_is_always_present() {
757 let empty: BuildOutput<Action> = from_str("", resolver).unwrap();
760 assert_eq!(empty.layers.keys().collect::<Vec<_>>(), vec![GLOBAL_LAYER]);
761 assert!(empty.global().is_empty());
762
763 let out = from_str("[keys]\n\"ctrl+q\" = \"quit\"\n", resolver).unwrap();
764 assert_eq!(out.layers.keys().collect::<Vec<_>>(), vec![GLOBAL_LAYER]);
765 }
766
767 #[test]
768 fn named_layers_are_parsed_under_their_names() {
769 let toml = "\
770[keys]\n\"ctrl+q\" = \"quit\"\n\
771[layers.panel]\n\"ctrl+s\" = \"split\"\n";
772 let out = from_str(toml, resolver).unwrap();
773 assert!(out.warnings.is_empty());
774 assert_eq!(
776 out.layers.keys().map(String::as_str).collect::<Vec<_>>(),
777 vec!["global", "panel"]
778 );
779 let q = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
780 let s = KeyInput::new(Key::Char('s'), Modifiers::CTRL);
781 assert_eq!(out.global().get(&q), Some(&Action::Quit));
782 assert_eq!(out.layers["panel"].get(&s), Some(&Action::Split));
783 assert_eq!(out.global().get(&s), None);
785 assert_eq!(out.layers["panel"].get(&q), None);
786 }
787
788 #[test]
789 fn a_layer_with_no_keys_section_still_gets_an_empty_global() {
790 let toml = "[layers.panel]\n\"ctrl+s\" = \"split\"\n";
791 let out = from_str(toml, resolver).unwrap();
792 assert!(out.global().is_empty());
793 assert!(!out.layers["panel"].is_empty());
794 }
795
796 #[test]
797 fn same_chord_in_two_layers_is_an_override_not_a_conflict() {
798 let toml = "\
802[keys]\n\"ctrl+s\" = \"save\"\n\
803[layers.panel]\n\"ctrl+s\" = \"split\"\n";
804 let out = from_str(toml, resolver).unwrap();
805 assert!(
806 out.warnings.is_empty(),
807 "cross-layer override must not warn: {:?}",
808 out.warnings
809 );
810 let s = KeyInput::new(Key::Char('s'), Modifiers::CTRL);
811 assert_eq!(out.global().get(&s), Some(&Action::Save));
812 assert_eq!(out.layers["panel"].get(&s), Some(&Action::Split));
813 }
814
815 #[test]
816 fn explicit_global_layer_merges_into_the_top_level_keys() {
817 let toml = "\
818[keys]\n\"ctrl+q\" = \"quit\"\n\
819[layers.global]\n\"ctrl+s\" = \"save\"\n";
820 let out = from_str(toml, resolver).unwrap();
821 assert!(out.warnings.is_empty());
822 assert_eq!(out.layers.keys().collect::<Vec<_>>(), vec![GLOBAL_LAYER]);
824 let q = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
825 let s = KeyInput::new(Key::Char('s'), Modifiers::CTRL);
826 assert_eq!(out.global().get(&q), Some(&Action::Quit));
827 assert_eq!(out.global().get(&s), Some(&Action::Save));
828 }
829
830 #[test]
831 fn keys_and_explicit_global_colliding_on_a_chord_conflict_last_wins() {
832 let toml = "\
836[keys]\n\"ctrl+q\" = \"quit\"\n\
837[layers.global]\n\"ctrl+q\" = \"save\"\n";
838 let out = from_str(toml, resolver).unwrap();
839 assert_eq!(
840 out.warnings,
841 vec![Warning::Conflict {
842 chord: "ctrl+q".to_string(),
843 contenders: vec!["quit".to_string(), "save".to_string()],
844 winner: "save".to_string(),
845 }]
846 );
847 let q = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
848 assert_eq!(out.global().get(&q), Some(&Action::Save));
849 }
850
851 #[test]
852 fn conflict_within_a_named_layer_is_reported() {
853 let toml = "\
856[layers.panel]\n\"ctrl+a\" = \"quit\"\n\"control+a\" = \"save\"\n";
857 let out = from_str(toml, resolver).unwrap();
858 assert_eq!(
859 out.warnings,
860 vec![Warning::Conflict {
861 chord: "ctrl+a".to_string(),
862 contenders: vec!["save".to_string(), "quit".to_string()],
863 winner: "quit".to_string(),
864 }]
865 );
866 }
867
868 #[test]
869 fn cross_shadow_is_checked_against_global_only() {
870 let toml = "\
874[layers.panel]\n\"j\" = \"top\"\n\
875[[sequences]]\nkeys = [\"j\", \"k\"]\naction = \"save\"\n";
876 let out = from_str(toml, resolver).unwrap();
877 assert!(
878 out.warnings.is_empty(),
879 "a non-global chord must not cross-shadow a global sequence: {:?}",
880 out.warnings
881 );
882 }
883
884 #[test]
885 fn unknown_action_in_a_named_layer_is_a_warning_carrying_no_layer_name() {
886 let toml = "[layers.panel]\n\"ctrl+z\" = \"undo\"\n";
890 let out = from_str(toml, resolver).unwrap();
891 assert_eq!(
892 out.warnings,
893 vec![Warning::UnknownAction {
894 key: "ctrl+z".to_string(),
895 action: "undo".to_string(),
896 }]
897 );
898 assert!(out.layers["panel"].is_empty());
899 }
900
901 #[test]
902 fn malformed_key_in_a_named_layer_is_a_fatal_error() {
903 let toml = "[layers.panel]\n\"ctrl+nope\" = \"quit\"\n";
905 let err = from_str(toml, resolver).unwrap_err();
906 assert!(matches!(err, BuildError::KeyParse { .. }));
907 }
908
909 #[test]
910 fn sequences_do_not_create_extra_layers() {
911 let toml = "\
914[keys]\n\"ctrl+q\" = \"quit\"\n\
915[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+s\"]\naction = \"save\"\n";
916 let out = from_str(toml, resolver).unwrap();
917 assert!(out.warnings.is_empty());
918 assert_eq!(out.layers.keys().collect::<Vec<_>>(), vec![GLOBAL_LAYER]);
919 assert!(!out.sequences.is_empty());
920 }
921
922 #[test]
923 fn unknown_actions_across_layers_warn_global_first_then_name_order() {
924 let toml = "\
927[keys]\n\"a\" = \"nope_global\"\n\
928[layers.zeta]\n\"b\" = \"nope_zeta\"\n\
929[layers.alpha]\n\"c\" = \"nope_alpha\"\n";
930 let out = from_str(toml, resolver).unwrap();
931 let unknown_actions: Vec<&str> = out
932 .warnings
933 .iter()
934 .filter_map(|w| match w {
935 Warning::UnknownAction { action, .. } => Some(action.as_str()),
936 _ => None,
937 })
938 .collect();
939 assert_eq!(
940 unknown_actions,
941 vec!["nope_global", "nope_alpha", "nope_zeta"]
942 );
943 }
944
945 #[test]
946 fn unknown_top_level_field_is_a_fatal_error() {
947 let err = from_str("[kesy]\n\"ctrl+q\" = \"quit\"\n", resolver).unwrap_err();
949 assert!(matches!(err, BuildError::Toml(_)));
950 }
951
952 #[test]
953 fn unknown_sequence_field_is_a_fatal_error() {
954 let toml = "[[sequences]]\nkeys = [\"g\"]\naction = \"top\"\nlayer = \"panel\"\n";
957 let err = from_str(toml, resolver).unwrap_err();
958 assert!(matches!(err, BuildError::Toml(_)));
959 }
960
961 #[test]
962 fn unknown_action_is_a_warning_not_a_failure() {
963 let toml = "[keys]\n\"ctrl+q\" = \"quit\"\n\"ctrl+z\" = \"undo\"\n";
964 let out = from_str(toml, resolver).unwrap();
965 let q = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
967 assert_eq!(out.global().get(&q), Some(&Action::Quit));
968 assert_eq!(
969 out.warnings,
970 vec![Warning::UnknownAction {
971 key: "ctrl+z".to_string(),
972 action: "undo".to_string(),
973 }]
974 );
975 }
976
977 #[test]
978 fn distinct_spellings_of_same_chord_conflict() {
979 let toml = "[keys]\n\"ctrl+a\" = \"quit\"\n\"control+a\" = \"save\"\n";
983 let out = from_str(toml, resolver).unwrap();
984 let a = KeyInput::new(Key::Char('a'), Modifiers::CTRL);
985 assert!(out.global().get(&a).is_some());
987 let conflicts: Vec<_> = out
988 .warnings
989 .iter()
990 .filter(|w| matches!(w, Warning::Conflict { .. }))
991 .collect();
992 assert_eq!(conflicts.len(), 1);
993 }
994
995 #[test]
996 fn legacy_lints_are_opt_in_and_separate_from_warnings() {
997 let toml = "[keys]\n\"cmd+s\" = \"save\"\n";
1001 let out = from_str(toml, resolver).unwrap();
1002 assert!(out.warnings.is_empty());
1003 assert_eq!(
1004 keymap_core::legacy_lints(out.global()),
1005 vec![keymap_core::LegacyLint::Unrepresentable {
1006 chord: "super+s".to_string(),
1007 }]
1008 );
1009 }
1010
1011 #[test]
1012 fn malformed_key_is_a_fatal_error() {
1013 let toml = "[keys]\n\"ctrl+nope\" = \"quit\"\n";
1014 let err = from_str(toml, resolver).unwrap_err();
1015 assert!(matches!(err, BuildError::KeyParse { .. }));
1016 }
1017
1018 #[test]
1019 fn malformed_toml_is_a_fatal_error() {
1020 let err = from_str("this is not toml", resolver).unwrap_err();
1021 assert!(matches!(err, BuildError::Toml(_)));
1022 }
1023
1024 #[test]
1025 fn empty_config_builds_empty_map() {
1026 let out: BuildOutput<Action> = from_str("", resolver).unwrap();
1027 assert!(out.global().is_empty());
1028 assert!(out.sequences.is_empty());
1029 assert!(out.warnings.is_empty());
1030 }
1031
1032 #[test]
1033 fn builds_sequence_bindings() {
1034 let toml = "\
1035[keys]\n\"ctrl+q\" = \"quit\"\n\
1036[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+s\"]\naction = \"save\"\n\
1037[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+c\"]\naction = \"quit\"\n";
1038 let out = from_str(toml, resolver).unwrap();
1039 assert!(out.warnings.is_empty());
1040 let q = KeyInput::new(Key::Char('q'), Modifiers::CTRL);
1042 assert_eq!(out.global().get(&q), Some(&Action::Quit));
1043 let save = seq(&[('x', Modifiers::CTRL), ('s', Modifiers::CTRL)]);
1045 assert_eq!(out.sequences.lookup(&save), Match::Exact(&Action::Save));
1046 assert_eq!(out.sequences.lookup(&save[..1]), Match::Prefix);
1047 }
1048
1049 #[test]
1050 fn sequence_unknown_action_is_a_warning() {
1051 let toml = "[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+z\"]\naction = \"undo\"\n";
1052 let out = from_str(toml, resolver).unwrap();
1053 assert!(out.sequences.is_empty());
1054 assert_eq!(
1055 out.warnings,
1056 vec![Warning::UnknownAction {
1057 key: "ctrl+x ctrl+z".to_string(),
1058 action: "undo".to_string(),
1059 }]
1060 );
1061 }
1062
1063 #[test]
1064 fn sequence_prefix_shadow_is_a_warning_and_drops_the_later() {
1065 let toml = "\
1067[[sequences]]\nkeys = [\"g\"]\naction = \"top\"\n\
1068[[sequences]]\nkeys = [\"g\", \"g\"]\naction = \"split\"\n";
1069 let out = from_str(toml, resolver).unwrap();
1070 assert_eq!(
1072 out.sequences.lookup(&seq(&[('g', Modifiers::NONE)])),
1073 Match::Exact(&Action::Top)
1074 );
1075 assert_eq!(
1076 out.warnings,
1077 vec![Warning::PrefixShadow {
1078 prefix: vec!["g".to_string()],
1079 prefix_action: "top".to_string(),
1080 shadowed: vec!["g".to_string(), "g".to_string()],
1081 shadowed_action: "split".to_string(),
1082 }]
1083 );
1084 }
1085
1086 #[test]
1087 fn sequence_prefix_shadow_reverse_order_drops_the_later_short_one() {
1088 let toml = "\
1091[[sequences]]\nkeys = [\"g\", \"g\"]\naction = \"split\"\n\
1092[[sequences]]\nkeys = [\"g\"]\naction = \"top\"\n";
1093 let out = from_str(toml, resolver).unwrap();
1094 assert_eq!(
1096 out.sequences
1097 .lookup(&seq(&[('g', Modifiers::NONE), ('g', Modifiers::NONE)])),
1098 Match::Exact(&Action::Split)
1099 );
1100 assert_eq!(
1101 out.warnings,
1102 vec![Warning::PrefixShadow {
1103 prefix: vec!["g".to_string()],
1104 prefix_action: "top".to_string(),
1105 shadowed: vec!["g".to_string(), "g".to_string()],
1106 shadowed_action: "split".to_string(),
1107 }]
1108 );
1109 }
1110
1111 #[test]
1112 fn same_sequence_three_times_reports_pairwise_conflicts() {
1113 let toml = "\
1114[[sequences]]\nkeys = [\"ctrl+x\"]\naction = \"save\"\n\
1115[[sequences]]\nkeys = [\"ctrl+x\"]\naction = \"split\"\n\
1116[[sequences]]\nkeys = [\"ctrl+x\"]\naction = \"quit\"\n";
1117 let out = from_str(toml, resolver).unwrap();
1118 assert_eq!(
1120 out.sequences.lookup(&seq(&[('x', Modifiers::CTRL)])),
1121 Match::Exact(&Action::Quit)
1122 );
1123 assert_eq!(
1125 out.warnings,
1126 vec![
1127 Warning::Conflict {
1128 chord: "ctrl+x".to_string(),
1129 contenders: vec!["save".to_string(), "split".to_string()],
1130 winner: "split".to_string(),
1131 },
1132 Warning::Conflict {
1133 chord: "ctrl+x".to_string(),
1134 contenders: vec!["split".to_string(), "quit".to_string()],
1135 winner: "quit".to_string(),
1136 },
1137 ]
1138 );
1139 }
1140
1141 #[test]
1142 fn empty_sequence_is_a_warning() {
1143 let toml = "[[sequences]]\nkeys = []\naction = \"save\"\n";
1144 let out = from_str(toml, resolver).unwrap();
1145 assert!(out.sequences.is_empty());
1146 assert_eq!(
1147 out.warnings,
1148 vec![Warning::EmptySequence {
1149 action: "save".to_string(),
1150 }]
1151 );
1152 }
1153
1154 #[test]
1155 fn duplicate_sequence_conflicts_and_last_wins() {
1156 let toml = "\
1157[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+s\"]\naction = \"save\"\n\
1158[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+s\"]\naction = \"split\"\n";
1159 let out = from_str(toml, resolver).unwrap();
1160 let s = seq(&[('x', Modifiers::CTRL), ('s', Modifiers::CTRL)]);
1161 assert_eq!(out.sequences.lookup(&s), Match::Exact(&Action::Split));
1162 assert_eq!(
1163 out.warnings,
1164 vec![Warning::Conflict {
1165 chord: "ctrl+x ctrl+s".to_string(),
1166 contenders: vec!["save".to_string(), "split".to_string()],
1167 winner: "split".to_string(),
1168 }]
1169 );
1170 }
1171
1172 #[test]
1173 fn malformed_sequence_key_is_a_fatal_error() {
1174 let toml = "[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+nope\"]\naction = \"save\"\n";
1175 let err = from_str(toml, resolver).unwrap_err();
1176 assert!(matches!(err, BuildError::KeyParse { .. }));
1177 }
1178
1179 #[test]
1180 fn single_chord_shadowing_a_sequence_is_an_advisory_warning() {
1181 let toml = "\
1184[keys]\n\"j\" = \"top\"\n\
1185[[sequences]]\nkeys = [\"j\", \"k\"]\naction = \"save\"\n";
1186 let out = from_str(toml, resolver).unwrap();
1187 assert_eq!(
1189 out.global()
1190 .get(&KeyInput::new(Key::Char('j'), Modifiers::NONE)),
1191 Some(&Action::Top)
1192 );
1193 assert_eq!(
1194 out.sequences
1195 .lookup(&seq(&[('j', Modifiers::NONE), ('k', Modifiers::NONE)])),
1196 Match::Exact(&Action::Save)
1197 );
1198 assert_eq!(
1199 out.warnings,
1200 vec![Warning::SequenceShadow {
1201 chord: "j".to_string(),
1202 chord_action: "top".to_string(),
1203 sequence: vec!["j".to_string(), "k".to_string()],
1204 sequence_action: "save".to_string(),
1205 }]
1206 );
1207 }
1208
1209 #[test]
1210 fn length_one_sequence_equal_to_a_single_chord_shadows() {
1211 let toml = "\
1214[keys]\n\"q\" = \"quit\"\n\
1215[[sequences]]\nkeys = [\"q\"]\naction = \"save\"\n";
1216 let out = from_str(toml, resolver).unwrap();
1217 assert_eq!(
1218 out.warnings,
1219 vec![Warning::SequenceShadow {
1220 chord: "q".to_string(),
1221 chord_action: "quit".to_string(),
1222 sequence: vec!["q".to_string()],
1223 sequence_action: "save".to_string(),
1224 }]
1225 );
1226 }
1227
1228 #[test]
1229 fn disjoint_first_keys_do_not_shadow() {
1230 let toml = "\
1232[keys]\n\"j\" = \"top\"\n\
1233[[sequences]]\nkeys = [\"g\", \"g\"]\naction = \"save\"\n";
1234 let out = from_str(toml, resolver).unwrap();
1235 assert!(out.warnings.is_empty());
1236 }
1237
1238 #[test]
1239 fn one_chord_shadowing_several_sequences_reports_the_lex_first() {
1240 let toml = "\
1242[keys]\n\"j\" = \"top\"\n\
1243[[sequences]]\nkeys = [\"j\", \"k\"]\naction = \"save\"\n\
1244[[sequences]]\nkeys = [\"j\", \"a\"]\naction = \"quit\"\n";
1245 let out = from_str(toml, resolver).unwrap();
1246 assert_eq!(
1247 out.warnings,
1248 vec![Warning::SequenceShadow {
1249 chord: "j".to_string(),
1250 chord_action: "top".to_string(),
1251 sequence: vec!["j".to_string(), "a".to_string()],
1252 sequence_action: "quit".to_string(),
1253 }]
1254 );
1255 }
1256
1257 #[test]
1258 fn multiple_shadowing_chords_emit_in_canonical_chord_order() {
1259 let toml = "\
1263[keys]\n\"z\" = \"top\"\n\"j\" = \"quit\"\n\
1264[[sequences]]\nkeys = [\"z\", \"x\"]\naction = \"save\"\n\
1265[[sequences]]\nkeys = [\"j\", \"k\"]\naction = \"split\"\n";
1266 let out = from_str(toml, resolver).unwrap();
1267 assert_eq!(
1268 out.warnings,
1269 vec![
1270 Warning::SequenceShadow {
1271 chord: "j".to_string(),
1272 chord_action: "quit".to_string(),
1273 sequence: vec!["j".to_string(), "k".to_string()],
1274 sequence_action: "split".to_string(),
1275 },
1276 Warning::SequenceShadow {
1277 chord: "z".to_string(),
1278 chord_action: "top".to_string(),
1279 sequence: vec!["z".to_string(), "x".to_string()],
1280 sequence_action: "save".to_string(),
1281 },
1282 ]
1283 );
1284 }
1285
1286 #[test]
1287 fn unknown_single_chord_does_not_shadow() {
1288 let toml = "\
1291[keys]\n\"j\" = \"nonexistent\"\n\
1292[[sequences]]\nkeys = [\"j\", \"k\"]\naction = \"save\"\n";
1293 let out = from_str(toml, resolver).unwrap();
1294 assert_eq!(
1295 out.warnings,
1296 vec![Warning::UnknownAction {
1297 key: "j".to_string(),
1298 action: "nonexistent".to_string(),
1299 }]
1300 );
1301 }
1302
1303 #[test]
1304 fn cross_shadow_coexists_with_conflict_and_comes_last() {
1305 let toml = "\
1308[keys]\n\"ctrl+a\" = \"quit\"\n\"control+a\" = \"save\"\n\"j\" = \"top\"\n\
1309[[sequences]]\nkeys = [\"j\", \"k\"]\naction = \"split\"\n";
1310 let out = from_str(toml, resolver).unwrap();
1311 assert_eq!(
1312 out.warnings,
1313 vec![
1314 Warning::Conflict {
1317 chord: "ctrl+a".to_string(),
1318 contenders: vec!["save".to_string(), "quit".to_string()],
1319 winner: "quit".to_string(),
1320 },
1321 Warning::SequenceShadow {
1322 chord: "j".to_string(),
1323 chord_action: "top".to_string(),
1324 sequence: vec!["j".to_string(), "k".to_string()],
1325 sequence_action: "split".to_string(),
1326 },
1327 ]
1328 );
1329 }
1330
1331 #[test]
1332 fn chord_matching_a_non_first_sequence_key_does_not_shadow() {
1333 let toml = "\
1336[keys]\n\"j\" = \"top\"\n\
1337[[sequences]]\nkeys = [\"g\", \"j\"]\naction = \"save\"\n";
1338 let out = from_str(toml, resolver).unwrap();
1339 assert!(out.warnings.is_empty());
1340 }
1341
1342 fn km_pairs(km: &Keymap<String>) -> Vec<(KeyInput, String)> {
1349 let mut v: Vec<_> = km.iter().map(|(k, a)| (*k, a.clone())).collect();
1350 v.sort_by_key(|(k, _)| k.to_string());
1351 v
1352 }
1353
1354 fn seq_pairs(s: &SequenceKeymap<String>) -> Vec<(Vec<KeyInput>, String)> {
1355 let mut v: Vec<_> = s.bindings().map(|(p, a)| (p, a.clone())).collect();
1356 v.sort_by_key(|(p, _)| render_sequence(p).join(" "));
1357 v
1358 }
1359
1360 fn assert_round_trips(km: &Keymap<String>, seq: &SequenceKeymap<String>) {
1362 let toml = to_toml(km, seq, |a: &String| Some(a.as_str()));
1363 let out = from_str(&toml, |name: &str| Some(name.to_owned())).unwrap();
1364 assert!(
1365 out.warnings.is_empty(),
1366 "round-trip warned: {:?}",
1367 out.warnings
1368 );
1369 assert_eq!(km_pairs(km), km_pairs(out.global()));
1370 assert_eq!(seq_pairs(seq), seq_pairs(&out.sequences));
1371 }
1372
1373 fn norm(key: Key, mods: Modifiers) -> KeyInput {
1374 KeyInput::normalized(key, mods)
1375 }
1376
1377 #[test]
1378 fn to_toml_round_trips_keys_and_sequences() {
1379 let mut km = Keymap::new();
1380 km.bind(norm(Key::Char('q'), Modifiers::CTRL), "quit".to_owned());
1381 km.bind(norm(Key::Char('s'), Modifiers::CTRL), "save".to_owned());
1382 km.bind(norm(Key::Char('a'), Modifiers::SHIFT), "all".to_owned());
1385 km.bind(
1387 norm(Key::Char('a'), Modifiers::CTRL | Modifiers::SHIFT),
1388 "alt_all".to_owned(),
1389 );
1390
1391 let mut seq = SequenceKeymap::new();
1392 seq.bind(
1393 [
1394 norm(Key::Char('x'), Modifiers::CTRL),
1395 norm(Key::Char('s'), Modifiers::CTRL),
1396 ],
1397 "seq_save".to_owned(),
1398 )
1399 .unwrap();
1400 seq.bind(
1401 [
1402 norm(Key::Char('g'), Modifiers::NONE),
1403 norm(Key::Char('g'), Modifiers::NONE),
1404 ],
1405 "top".to_owned(),
1406 )
1407 .unwrap();
1408
1409 assert_round_trips(&km, &seq);
1410 }
1411
1412 #[test]
1413 fn to_toml_is_deterministic_and_canonically_ordered() {
1414 let mut km = Keymap::new();
1415 km.bind(norm(Key::Char('z'), Modifiers::CTRL), "z".to_owned());
1416 km.bind(norm(Key::Char('a'), Modifiers::CTRL), "a".to_owned());
1417 let seq = SequenceKeymap::new();
1418
1419 let first = to_toml(&km, &seq, |a: &String| Some(a.as_str()));
1420 let second = to_toml(&km, &seq, |a: &String| Some(a.as_str()));
1421 assert_eq!(first, second, "output must be deterministic");
1422 let a_at = first.find("ctrl+a").unwrap();
1424 let z_at = first.find("ctrl+z").unwrap();
1425 assert!(
1426 a_at < z_at,
1427 "keys must be emitted in canonical order:\n{first}"
1428 );
1429 }
1430
1431 #[test]
1432 fn to_toml_omits_unnameable_bindings() {
1433 let mut km = Keymap::new();
1434 km.bind(norm(Key::Char('q'), Modifiers::CTRL), "quit".to_owned());
1435 km.bind(norm(Key::Char('x'), Modifiers::CTRL), "secret".to_owned());
1436 let seq = SequenceKeymap::new();
1437
1438 let toml = to_toml(&km, &seq, |a: &String| {
1440 (a != "secret").then_some(a.as_str())
1441 });
1442 let out = from_str(&toml, |name: &str| Some(name.to_owned())).unwrap();
1443 assert_eq!(
1444 out.global().get(&norm(Key::Char('q'), Modifiers::CTRL)),
1445 Some(&"quit".to_owned())
1446 );
1447 assert_eq!(
1448 out.global().get(&norm(Key::Char('x'), Modifiers::CTRL)),
1449 None
1450 );
1451 }
1452
1453 #[test]
1454 fn to_toml_empty_maps_emit_empty_string() {
1455 let km: Keymap<String> = Keymap::new();
1456 let seq: SequenceKeymap<String> = SequenceKeymap::new();
1457 assert_eq!(to_toml(&km, &seq, |a: &String| Some(a.as_str())), "");
1458 }
1459
1460 #[test]
1461 fn to_toml_round_trips_adversarial_names_and_chords() {
1462 let mut km = Keymap::new();
1465 km.bind(
1466 norm(Key::Char('a'), Modifiers::NONE),
1467 "quit\"; [injected]\nx = \"oops".to_owned(),
1468 );
1469 km.bind(norm(Key::Char('+'), Modifiers::NONE), "plus".to_owned());
1471 km.bind(norm(Key::Char(' '), Modifiers::NONE), "space".to_owned());
1472 km.bind(norm(Key::Char('"'), Modifiers::NONE), "quote".to_owned());
1473 km.bind(norm(Key::Char('あ'), Modifiers::NONE), "hira".to_owned());
1474 km.bind(norm(Key::Char('F'), Modifiers::NONE), "cap_f".to_owned());
1475
1476 let mut seq = SequenceKeymap::new();
1480 seq.bind(
1481 [
1482 norm(Key::Char('z'), Modifiers::NONE),
1483 norm(Key::Char(' '), Modifiers::NONE),
1484 norm(Key::Char('+'), Modifiers::NONE),
1485 ],
1486 "z_space_plus".to_owned(),
1487 )
1488 .unwrap();
1489
1490 assert_round_trips(&km, &seq);
1491 }
1492
1493 #[test]
1494 fn shadow_matching_is_on_the_parsed_chord_not_the_label() {
1495 let toml = "\
1497[keys]\n\"ctrl+x\" = \"top\"\n\
1498[[sequences]]\nkeys = [\"ctrl+x\", \"ctrl+s\"]\naction = \"save\"\n";
1499 let out = from_str(toml, resolver).unwrap();
1500 assert_eq!(
1501 out.warnings,
1502 vec![Warning::SequenceShadow {
1503 chord: "ctrl+x".to_string(),
1504 chord_action: "top".to_string(),
1505 sequence: vec!["ctrl+x".to_string(), "ctrl+s".to_string()],
1506 sequence_action: "save".to_string(),
1507 }]
1508 );
1509
1510 let toml = "\
1512[keys]\n\"ctrl+x\" = \"top\"\n\
1513[[sequences]]\nkeys = [\"ctrl+shift+x\", \"ctrl+s\"]\naction = \"save\"\n";
1514 let out = from_str(toml, resolver).unwrap();
1515 assert!(out.warnings.is_empty());
1516 }
1517
1518 #[test]
1519 fn to_toml_layered_round_trips_named_layers() {
1520 let mut global = Keymap::new();
1521 global.bind(norm(Key::Char('q'), Modifiers::CTRL), "quit".to_owned());
1522 let mut panel = Keymap::new();
1523 panel.bind(norm(Key::Char('s'), Modifiers::CTRL), "split".to_owned());
1524 panel.bind(
1526 norm(Key::Char('q'), Modifiers::CTRL),
1527 "panel_quit".to_owned(),
1528 );
1529
1530 let mut layers = BTreeMap::new();
1531 layers.insert(GLOBAL_LAYER.to_string(), global);
1532 layers.insert("panel".to_string(), panel);
1533
1534 let mut seq = SequenceKeymap::new();
1535 seq.bind(
1536 [
1537 norm(Key::Char('x'), Modifiers::CTRL),
1538 norm(Key::Char('s'), Modifiers::CTRL),
1539 ],
1540 "seq_save".to_owned(),
1541 )
1542 .unwrap();
1543
1544 let toml = to_toml_layered(&layers, &seq, |a: &String| Some(a.as_str()));
1545 let out = from_str(&toml, |name: &str| Some(name.to_owned())).unwrap();
1546 assert!(
1547 out.warnings.is_empty(),
1548 "round-trip warned: {:?}",
1549 out.warnings
1550 );
1551 assert_eq!(km_pairs(&layers["global"]), km_pairs(out.global()));
1552 assert_eq!(km_pairs(&layers["panel"]), km_pairs(&out.layers["panel"]));
1553 assert_eq!(seq_pairs(&seq), seq_pairs(&out.sequences));
1554 }
1555
1556 #[test]
1557 fn to_toml_layered_matches_to_toml_for_a_global_only_set() {
1558 let mut global = Keymap::new();
1561 global.bind(norm(Key::Char('q'), Modifiers::CTRL), "quit".to_owned());
1562 global.bind(norm(Key::Char('s'), Modifiers::CTRL), "save".to_owned());
1563 let mut seq = SequenceKeymap::new();
1564 seq.bind(
1565 [
1566 norm(Key::Char('g'), Modifiers::NONE),
1567 norm(Key::Char('g'), Modifiers::NONE),
1568 ],
1569 "top".to_owned(),
1570 )
1571 .unwrap();
1572
1573 let mut layers = BTreeMap::new();
1574 layers.insert(GLOBAL_LAYER.to_string(), global.clone());
1575
1576 let plain = to_toml(&global, &seq, |a: &String| Some(a.as_str()));
1577 let layered = to_toml_layered(&layers, &seq, |a: &String| Some(a.as_str()));
1578 assert_eq!(plain, layered);
1579 let empty: BTreeMap<String, Keymap<String>> = BTreeMap::new();
1581 assert_eq!(
1582 to_toml_layered(&empty, &SequenceKeymap::new(), |a: &String| Some(
1583 a.as_str()
1584 )),
1585 ""
1586 );
1587 }
1588
1589 #[test]
1590 fn to_toml_layered_drops_empty_layers() {
1591 let mut global = Keymap::new();
1595 global.bind(norm(Key::Char('q'), Modifiers::CTRL), "quit".to_owned());
1596 let mut layers = BTreeMap::new();
1597 layers.insert(GLOBAL_LAYER.to_string(), global);
1598 layers.insert("panel".to_string(), Keymap::<String>::new());
1599
1600 let toml = to_toml_layered(&layers, &SequenceKeymap::new(), |a: &String| {
1601 Some(a.as_str())
1602 });
1603 assert!(
1604 !toml.contains("panel"),
1605 "empty layer must not be emitted:\n{toml}"
1606 );
1607 let out = from_str(&toml, |name: &str| Some(name.to_owned())).unwrap();
1608 assert_eq!(out.layers.keys().collect::<Vec<_>>(), vec![GLOBAL_LAYER]);
1609 }
1610}