Files
craft-libs/crates/keymap/tests/chord.rs
dotCooCoo fc37b20235 keymap: the ISO key next to left Shift can be bound to shortcuts
craft_keymap::Key gains IntlBackslash, the key between left Shift and Z
on ISO keyboards (`<` and `>` on Turkish Q). Key::from_egui and to_egui
map it to egui's IntlBackslash, so the shortcut recorder records it and
the dispatcher runs its bindings. Its shortcut text is `IntlBackslash`
(`Cmd+IntlBackslash`, `Shift+IntlBackslash`), read without regard to
case; `<` and `>` still parse as no key.

The key counts as a symbol position: Shift stays in the chord, and once
the layout has typed `<` there, display_for shows `Ctrl+<`.
MAX_CHORD_TEXT goes from 64 to 72 bytes so that the longest canonical
trigger, `Cmd+Ctrl+Alt+Shift+IntlBackslash` pressed twice, reads back.
2026-10-30 23:00:40 -07:00

220 lines
9.8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use craft_keymap::chord::foreign::{Flavor, ForeignError, ReadNote, read_foreign};
use craft_keymap::{Chord, Key, MAX_CHORD_TEXT, Mods, ParseError, Platform, Trigger};
fn chord(text: &str) -> Chord {
Chord::parse(text).unwrap_or_else(|e| panic!("{text}: {e}"))
}
fn mods(cmd: bool, ctrl: bool, alt: bool, shift: bool) -> Mods {
Mods { cmd, ctrl, alt, shift }
}
#[test]
fn canonical_text_round_trips_for_every_key_and_modifier_set() {
for &key in Key::ALL {
for bits in 0..16u8 {
let c = Chord::new(mods(bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0), key);
let text = c.to_string();
assert_eq!(Chord::parse(&text), Ok(c), "{text}");
assert_eq!(Trigger::parse(&format!("{text} {text}")), Ok(Trigger::double(c)), "{text} double");
let json = serde_json::to_string(&Trigger::single(c)).unwrap();
assert_eq!(serde_json::from_str::<Trigger>(&json).unwrap(), Trigger::single(c));
// macOS display text reads back as the same chord.
assert_eq!(Chord::parse(&c.display(Platform::Mac)), Ok(c), "{}", c.display(Platform::Mac));
}
}
assert_eq!(chord("Cmd+Ctrl+Alt+Shift+K").to_string(), "Cmd+Ctrl+Alt+Shift+K");
assert_eq!(serde_json::to_string(&chord("shift+cmd+k")).unwrap(), "\"Cmd+Shift+K\"");
}
#[test]
fn plus_and_minus_keys_parse_from_their_glyphs() {
assert_eq!(Key::parse("+"), Some(Key::Plus));
assert_eq!(Key::parse("-"), Some(Key::Minus));
assert_eq!(Key::parse("−"), Some(Key::Minus), "U+2212");
}
#[test]
fn the_iso_key_next_to_left_shift_reads_and_writes_as_intl_backslash() {
assert_eq!(Key::parse("IntlBackslash"), Some(Key::IntlBackslash));
assert_eq!(Key::parse("intlbackslash"), Some(Key::IntlBackslash));
for text in ["IntlBackslash", "Cmd+IntlBackslash", "Shift+IntlBackslash"] {
let c = chord(text);
assert_eq!(c.to_string(), text);
assert_eq!(Trigger::parse(text).map(|t| t.to_string()).as_deref(), Ok(text));
let json = serde_json::to_string(&c).unwrap();
assert_eq!(json, format!("\"{text}\""));
assert_eq!(serde_json::from_str::<Chord>(&json).unwrap(), c);
}
assert_eq!(chord("Cmd+IntlBackslash").display(Platform::Windows), "Ctrl+IntlBackslash");
assert_eq!(chord("Cmd+IntlBackslash").display(Platform::Mac), "⌘IntlBackslash");
assert!(Key::IntlBackslash.is_symbol());
// It is not the usual backslash key, and `<` and `>` have no key of their own.
assert_eq!(Key::parse("\\"), Some(Key::Backslash));
assert_ne!(chord("Shift+IntlBackslash"), chord("Shift+\\"));
assert_eq!(Key::parse("<"), None);
assert_eq!(Key::from_char('<'), None);
assert_eq!(Key::from_char('>'), None);
}
#[test]
fn arrow_glyphs_parse_as_arrow_keys() {
for (glyph, key) in [("←", Key::Left), ("→", Key::Right), ("↑", Key::Up), ("↓", Key::Down)] {
assert_eq!(Key::parse(glyph), Some(key), "{glyph}");
}
assert_eq!(Chord::parse("⌘←").unwrap().to_string(), "Cmd+Left");
assert_eq!(Chord::parse("Alt+Shift+→").unwrap().to_string(), "Alt+Shift+Right");
}
#[test]
fn aliases_parse_to_canonical_chords() {
let cases = [
("cmd+shift+z", "Cmd+Shift+Z"),
("Shift+Command+Z", "Cmd+Shift+Z"),
("meta+z", "Cmd+Z"),
("Super+Z", "Cmd+Z"),
("mod+z", "Cmd+Z"),
("⌘⇧Z", "Cmd+Shift+Z"),
("⌃⌥⇧⌘K", "Cmd+Ctrl+Alt+Shift+K"),
("⌘+K", "Cmd+K"),
("⌘Shift+K", "Cmd+Shift+K"),
("control+opt+left", "Ctrl+Alt+Left"),
("ctl+option+ArrowUp", "Ctrl+Alt+Up"),
// craft-menu writes Ctrl before Cmd.
("Ctrl+Cmd+Alt+Shift+M", "Cmd+Ctrl+Alt+Shift+M"),
("Ctrl+Cmd+F", "Cmd+Ctrl+F"),
("Cmd++", "Cmd++"),
("⌘+", "Cmd++"),
("+", "+"),
("Cmd+Plus", "Cmd++"),
("cmd+Equals", "Cmd+="),
("Cmd+-", "Cmd+-"),
("Cmd+−", "Cmd+-"),
("⌘−", "Cmd+-"),
("Cmd + K", "Cmd+K"),
("esc", "Escape"),
("return", "Enter"),
("del", "Delete"),
("pgup", "PageUp"),
("pgdn", "PageDown"),
("arrowdown", "Down"),
("spacebar", "Space"),
("ins", "Insert"),
("f12", "F12"),
("Num5", "5"),
("Semicolon", ";"),
("Questionmark", "?"),
("OpenCurlyBracket", "{"),
("Backtick", "`"),
("⌘⌫", "Cmd+Backspace"),
];
for (text, canonical) in cases {
assert_eq!(chord(text).to_string(), canonical, "{text}");
}
}
#[test]
fn hostile_text_is_refused() {
let long = "Cmd+".repeat(MAX_CHORD_TEXT / 4) + "K";
assert!(long.len() > MAX_CHORD_TEXT);
let cases: &[(&str, ParseError)] = &[
("", ParseError::Empty),
(" ", ParseError::Empty),
(&long, ParseError::TooLong),
("+++", ParseError::UnknownModifier("+".into())),
("++", ParseError::MissingKey),
("Cmd+", ParseError::MissingKey),
("Cmd", ParseError::MissingKey),
("⌘", ParseError::MissingKey),
("Shift+Alt", ParseError::MissingKey),
("Cmd+Command+K", ParseError::DuplicateModifier("Command".into())),
("⌘⌘K", ParseError::DuplicateModifier("⌘".into())),
("Cmd+é", ParseError::UnknownKey("é".into())),
("Ctrl+ж+K", ParseError::UnknownModifier("ж".into())),
("Hyper+K", ParseError::UnknownModifier("Hyper".into())),
("A B", ParseError::Sequence),
("Cmd+K Cmd+J", ParseError::Sequence),
("A A A", ParseError::UnknownKey("A A A".into())),
];
for (text, err) in cases {
assert_eq!(Trigger::parse(text).as_ref(), Err(err), "{text:?}");
}
let wide = "⌘".repeat(30);
assert_eq!(Chord::parse(&wide), Err(ParseError::TooLong));
assert!(serde_json::from_str::<Chord>("\"Cmd+Nope\"").is_err());
}
#[test]
fn display_follows_the_platform() {
let c = chord("Cmd+Ctrl+Alt+Shift+K");
assert_eq!(c.display(Platform::Mac), "⌃⌥⇧⌘K");
assert_eq!(c.display(Platform::Windows), "Ctrl+Alt+Shift+K");
assert_eq!(chord("Ctrl+Up").display(Platform::Mac), "⌃↑");
assert_eq!(chord("Ctrl+Up").display(Platform::Linux), "Ctrl+Up");
assert_eq!(chord("Cmd+Enter").display(Platform::Mac), "⌘⏎");
assert_eq!(chord("Shift+Tab").display(Platform::Mac), "⇧⇥");
assert_eq!(chord("Space").display(Platform::Mac), "Space");
assert_eq!(chord("Alt+PageDown").display(Platform::Windows), "Alt+Page Down");
assert_eq!(Trigger::parse("Cmd+K Cmd+K").unwrap().display(Platform::Mac), "⌘K ⌘K");
assert_eq!(Trigger::parse("A A").unwrap().to_string(), "A A");
}
#[test]
fn ctrl_folds_into_cmd_off_macos() {
let ctrl = chord("Ctrl+S");
let cmd = chord("Cmd+S");
assert_ne!(ctrl.effective(Platform::Mac), cmd.effective(Platform::Mac));
for p in [Platform::Windows, Platform::Linux] {
assert_eq!(ctrl.effective(p), cmd);
assert_eq!(chord("Cmd+Ctrl+S").effective(p), cmd);
}
assert!(ctrl.has_command() && cmd.has_command());
assert!(chord("Shift+B").is_plain() && !chord("Alt+B").is_plain());
assert!(chord("Shift+F35").is_function_key() && !chord("F").is_function_key());
}
#[test]
fn foreign_windows_text() {
let read = |t: &str| read_foreign(t, Flavor::Windows);
let ok = |t: &str| read(t).unwrap_or_else(|e| panic!("{t}: {e}"));
assert_eq!(ok("Shift+Ctrl+S"), (Trigger::single(chord("Cmd+Shift+S")), vec![]));
assert_eq!(ok("Ctrl+,").0, Trigger::single(chord("Cmd+,")));
assert_eq!(ok(",").0, Trigger::single(chord(",")));
assert_eq!(ok("Ctrl++").0, Trigger::single(chord("Cmd++")));
assert_eq!(ok("Ctrl+-").0, Trigger::single(chord("Cmd+-")));
assert_eq!(ok("Alt-F4").0, Trigger::single(chord("Alt+F4")));
assert_eq!(ok("Ctrl + S").0, Trigger::single(chord("Cmd+S")));
assert_eq!(ok("Shift + Ctrl + S").0, Trigger::single(chord("Cmd+Shift+S")));
assert_eq!(ok("Ctrl + +").0, Trigger::single(chord("Cmd++")));
assert_eq!(ok("Alt - F4").0, Trigger::single(chord("Alt+F4")));
assert_eq!(ok("Ctrl Page Down").0, Trigger::single(chord("Cmd+PageDown")));
assert_eq!(ok("Shift+Up Arrow").0, Trigger::single(chord("Shift+Up")));
assert_eq!(ok("Num +"), (Trigger::single(chord("+")), vec![ReadNote::Numpad]));
assert_eq!(ok("Ctrl+Num /"), (Trigger::single(chord("Cmd+/")), vec![ReadNote::Numpad]));
assert_eq!(ok("Numpad 5"), (Trigger::single(chord("5")), vec![ReadNote::Numpad]));
assert_eq!(ok("Num Enter"), (Trigger::single(chord("Enter")), vec![ReadNote::Numpad]));
assert_eq!(read("Win+E"), Err(ForeignError::UnsupportedModifier("Win".into())));
assert_eq!(read("Meta+E"), Err(ForeignError::UnsupportedModifier("Meta".into())));
assert_eq!(read(""), Err(ForeignError::Empty));
assert_eq!(read(&"x".repeat(MAX_CHORD_TEXT + 1)), Err(ForeignError::TooLong));
assert_eq!(read("Ctrl+Num Lock"), Err(ForeignError::UnknownKey("Num Lock".into())));
assert_eq!(read("Alt"), Err(ForeignError::UnknownKey("Alt".into())));
}
#[test]
fn foreign_mac_text() {
let texts = ["Shift+Cmd+S", "Ctrl+Opt+Left"];
assert_eq!(Flavor::detect(texts), Flavor::Mac);
assert_eq!(Flavor::detect(["Shift+Ctrl+S", "Alt+F4"]), Flavor::Windows);
assert_eq!(Flavor::detect(["⌥⌘K"]), Flavor::Mac);
let ok = |t: &str| read_foreign(t, Flavor::Mac).unwrap_or_else(|e| panic!("{t}: {e}")).0;
assert_eq!(ok("Shift+Cmd+S"), Trigger::single(chord("Cmd+Shift+S")));
assert_eq!(ok("Ctrl+Opt+Left"), Trigger::single(chord("Ctrl+Alt+Left")));
assert_eq!(ok("Control-Option-Space"), Trigger::single(chord("Ctrl+Alt+Space")));
assert_eq!(ok("⇧⌘S"), Trigger::single(chord("Cmd+Shift+S")));
assert_eq!(ok("⌃⌥⇞"), Trigger::single(chord("Ctrl+Alt+PageUp")));
assert_eq!(ok("Cmd+,"), Trigger::single(chord("Cmd+,")));
// In a Windows set the same ⌃ is the command modifier.
assert_eq!(read_foreign("⌃S", Flavor::Windows).map(|r| r.0), Ok(Trigger::single(chord("Cmd+S"))));
}