From fc37b202350aad2eb4d1b25444ece5b2b5037f6f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 30 Oct 2026 22:57:37 -0700 Subject: [PATCH 1/2] 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. --- crates/keymap/src/chord.rs | 8 ++++--- crates/keymap/src/egui.rs | 6 +++--- crates/keymap/src/layout.rs | 11 +++++++--- crates/keymap/src/ui/capture.rs | 18 ++++++++++++++++ crates/keymap/tests/chord.rs | 23 ++++++++++++++++++++ crates/keymap/tests/egui.rs | 38 ++++++++++++++++++++++++++++++++- crates/keymap/tests/layout.rs | 33 ++++++++++++++++++++++++++++ docs/language-testing.md | 4 ++++ 8 files changed, 131 insertions(+), 10 deletions(-) diff --git a/crates/keymap/src/chord.rs b/crates/keymap/src/chord.rs index df1ecb51..33e07e94 100644 --- a/crates/keymap/src/chord.rs +++ b/crates/keymap/src/chord.rs @@ -12,7 +12,7 @@ use crate::layout::Layout; pub mod foreign; /// The longest chord or trigger text [`Chord::parse`] and [`Trigger::parse`] accept, in bytes. -pub const MAX_CHORD_TEXT: usize = 64; +pub const MAX_CHORD_TEXT: usize = 72; /// The platform a keymap is used on. It decides display text and whether `ctrl` folds into `cmd`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] @@ -92,7 +92,7 @@ keys! { Up => "Up", Down => "Down", Left => "Left", Right => "Right", Minus => "-", Equals => "=", Plus => "+", OpenBracket => "[", CloseBracket => "]", Backslash => "\\", Semicolon => ";", Quote => "'", Backtick => "`", - Comma => ",", Period => ".", Slash => "/", + Comma => ",", Period => ".", Slash => "/", IntlBackslash => "IntlBackslash", Colon => ":", Pipe => "|", Questionmark => "?", Exclamationmark => "!", OpenCurlyBracket => "{", CloseCurlyBracket => "}", } @@ -181,7 +181,8 @@ impl Key { } /// Punctuation and symbol keys: `-`, `=`, `+`, brackets, `\`, `;`, `'`, `` ` ``, `,`, `.`, - /// `/` and the shifted symbols `:`, `|`, `?`, `!`, `{`, `}`. + /// `/`, the ISO key next to left Shift (`IntlBackslash`) and the shifted symbols `:`, `|`, + /// `?`, `!`, `{`, `}`. pub const fn is_symbol(self) -> bool { matches!( self, @@ -197,6 +198,7 @@ impl Key { | Key::Comma | Key::Period | Key::Slash + | Key::IntlBackslash | Key::Colon | Key::Pipe | Key::Questionmark diff --git a/crates/keymap/src/egui.rs b/crates/keymap/src/egui.rs index 16a28ddb..d3400039 100644 --- a/crates/keymap/src/egui.rs +++ b/crates/keymap/src/egui.rs @@ -35,8 +35,8 @@ impl From for Platform { macro_rules! egui_keys { ($($ours:ident = $theirs:ident),* ; $($same:ident),* $(,)?) => { impl Key { - /// The key for an egui key. `None` for modifier keys, Copy, Cut, Paste, BrowserBack - /// and IntlBackslash. + /// The key for an egui key. `None` for modifier keys, Copy, Cut, Paste and + /// BrowserBack. pub fn from_egui(key: EguiKey) -> Option { match key { $(EguiKey::$theirs => Some(Key::$ours),)* @@ -63,7 +63,7 @@ egui_keys! { F21, F22, F23, F24, F25, F26, F27, F28, F29, F30, F31, F32, F33, F34, F35, Escape, Tab, Backspace, Enter, Space, Insert, Delete, Home, End, PageUp, PageDown, Minus, Equals, Plus, OpenBracket, CloseBracket, Backslash, Semicolon, Quote, Backtick, - Comma, Period, Slash, Colon, Pipe, Questionmark, Exclamationmark, OpenCurlyBracket, CloseCurlyBracket, + Comma, Period, Slash, IntlBackslash, Colon, Pipe, Questionmark, Exclamationmark, OpenCurlyBracket, CloseCurlyBracket, } impl Mods { diff --git a/crates/keymap/src/layout.rs b/crates/keymap/src/layout.rs index ba8c9eee..66700fca 100644 --- a/crates/keymap/src/layout.rs +++ b/crates/keymap/src/layout.rs @@ -15,7 +15,9 @@ //! 3. **Punctuation:** the character typed, without Shift when the layout needs Shift for it //! (German `/` is ⇧7, so ⇧⌘7 is ⌘/; US `:` is ⇧;, so ⇧; is `:`). A Shift binding on a US //! base key also matches the same press (`Shift+;` matches `:`), and `=` and `+` stand for -//! each other so zoom keys work with and without Shift. +//! each other so zoom keys work with and without Shift. egui has no key for the characters the +//! ISO key next to left Shift types (`<` and `>` on Turkish Q) and reports both as that key, +//! `IntlBackslash`, so Shift stays in the chord: ⇧ on that key is `Shift+IntlBackslash`. //! 4. **Whether a layout is Latin** is decided for the whole layout ([`Layout::is_latin`]), never //! per key: Dvorak's `'` key on the Q position stays `'`. //! 5. **No position fallback for unnamed characters on moved keys:** egui reports a character @@ -293,8 +295,11 @@ impl Layout { if Key::from_char(typed).is_some() { return None; } - let own = key.name().chars().next()?; - if self.chars.values().any(|&c| c == own) { + // `IntlBackslash` types no character of its own, so only a one-character name is checked. + let mut name = key.name().chars(); + if let (Some(own), None) = (name.next(), name.next()) + && self.chars.values().any(|&c| c == own) + { return None; } let mut upper = typed.to_uppercase(); diff --git a/crates/keymap/src/ui/capture.rs b/crates/keymap/src/ui/capture.rs index 612458fc..f9fa5544 100644 --- a/crates/keymap/src/ui/capture.rs +++ b/crates/keymap/src/ui/capture.rs @@ -99,4 +99,22 @@ mod tests { assert_eq!(chords, ["Cmd+E", "Tab", "Cmd+C", "Cmd+V"]); assert_eq!(left, 1, "only the pointer event stays"); } + + #[test] + fn records_the_iso_key_next_to_left_shift() { + // Turkish Q: the key types `<`, and `>` with Shift. + let release = |modifiers| Event::Key { key: EguiKey::IntlBackslash, physical_key: None, pressed: false, repeat: false, modifiers }; + let events = vec![ + press(EguiKey::IntlBackslash, Modifiers::NONE), + Event::Text("<".into()), + release(Modifiers::NONE), + press(EguiKey::IntlBackslash, Modifiers::SHIFT), + Event::Text(">".into()), + release(Modifiers::SHIFT), + press(EguiKey::IntlBackslash, Modifiers::MAC_CMD), + ]; + let (chords, left) = run(events); + assert_eq!(chords, ["IntlBackslash", "Shift+IntlBackslash", "Cmd+IntlBackslash"]); + assert_eq!(left, 0); + } } diff --git a/crates/keymap/tests/chord.rs b/crates/keymap/tests/chord.rs index 4a8440d7..3059aae1 100644 --- a/crates/keymap/tests/chord.rs +++ b/crates/keymap/tests/chord.rs @@ -34,6 +34,29 @@ fn plus_and_minus_keys_parse_from_their_glyphs() { 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::(&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)] { diff --git a/crates/keymap/tests/egui.rs b/crates/keymap/tests/egui.rs index e5364518..ef7cdc5d 100644 --- a/crates/keymap/tests/egui.rs +++ b/crates/keymap/tests/egui.rs @@ -22,9 +22,11 @@ fn every_key_converts_both_ways() { for &key in Key::ALL { assert_eq!(Key::from_egui(key.to_egui()), Some(key), "{key}"); } - for k in [EguiKey::Copy, EguiKey::Cut, EguiKey::Paste, EguiKey::BrowserBack, EguiKey::ShiftLeft, EguiKey::IntlBackslash] { + for k in [EguiKey::Copy, EguiKey::Cut, EguiKey::Paste, EguiKey::BrowserBack, EguiKey::ShiftLeft] { assert_eq!(Key::from_egui(k), None); } + assert_eq!(Key::from_egui(EguiKey::IntlBackslash), Some(Key::IntlBackslash)); + assert_eq!(Key::IntlBackslash.to_egui(), EguiKey::IntlBackslash); } #[test] @@ -508,6 +510,40 @@ fn the_dispatcher_reads_presses_by_the_layout() { assert_eq!(ran, ["edit.duplicate"]); } +#[test] +fn the_iso_key_next_to_left_shift_runs_its_bindings() { + let app = |id: &str, key: &str| Bindable::new(id, id, Scope::app()).with_default(Binding::parse(key).unwrap()); + let km = Keymap::new( + vec![ + app("iso", "IntlBackslash"), + app("iso.shift", "Shift+IntlBackslash"), + app("iso.ctrl", "Cmd+IntlBackslash"), + app("backslash", "\\"), + app("pipe", "Shift+\\"), + app("comma.shift", "Shift+,"), + ], + Scopes::new(), + Platform::Windows, + ); + // Turkish Q: the key types `<`, and `>` with Shift. egui has no key for either character and + // reports the press as the key itself. + let at = + |modifiers| Event::Key { key: EguiKey::IntlBackslash, physical_key: Some(EguiKey::IntlBackslash), pressed: true, repeat: false, modifiers }; + let shift = Modifiers::SHIFT; + let mut d = Dispatcher::new(); + let (ran, left) = run(&mut d, &km, &Active::NONE, frame(1.0, Modifiers::NONE, vec![at(Modifiers::NONE), Event::Text("<".into())])); + assert_eq!(ran, ["iso"]); + assert!(left.is_empty(), "{left:?}"); + let (ran, left) = run(&mut d, &km, &Active::NONE, frame(2.0, shift, vec![at(shift), Event::Text(">".into())])); + assert_eq!(ran, ["iso.shift"]); + assert!(left.is_empty(), "{left:?}"); + let (ran, _) = run(&mut d, &km, &Active::NONE, frame(3.0, CTRL, vec![at(CTRL)])); + assert_eq!(ran, ["iso.ctrl"]); + // The press without Shift taught the layout the key's character. + assert_eq!(chord("Cmd+IntlBackslash").display_for(Platform::Windows, d.layout()), "Ctrl+<"); + assert_eq!(chord("Shift+IntlBackslash").display_for(Platform::Windows, d.layout()), "Shift+<"); +} + #[test] fn letters_follow_the_latin_layout_from_egui_winit_to_the_dispatcher() { let app = |id: &str, key: &str| Bindable::new(id, id, Scope::app()).with_default(Binding::parse(key).unwrap()); diff --git a/crates/keymap/tests/layout.rs b/crates/keymap/tests/layout.rs index 46ff8876..0f13f810 100644 --- a/crates/keymap/tests/layout.rs +++ b/crates/keymap/tests/layout.rs @@ -224,3 +224,36 @@ fn labels_show_the_layouts_own_punctuation() { assert_eq!(craft_keymap::Trigger::double(chord("[")).display_for(Platform::Linux, &russian), "Х Х"); assert_eq!(label(&Layout::new(), "Cmd+[", Platform::Linux), "Ctrl+["); } + +#[test] +fn the_iso_key_next_to_left_shift_keeps_shift_and_shows_the_layouts_character() { + let p = Platform::Windows; + // Turkish Q: the key types `<`, and `>` with Shift, which egui reports as the key itself. + let plain = at("IntlBackslash", Key::IntlBackslash); + let shifted = at("Shift+IntlBackslash", Key::IntlBackslash); + let mut turkish = Layout::new(); + assert_eq!(turkish.resolve(&plain), Some(chord("IntlBackslash"))); + assert_eq!(turkish.resolve(&shifted), Some(chord("Shift+IntlBackslash"))); + assert_eq!(texts(&turkish.candidates(&shifted)), ["Shift+IntlBackslash"]); + assert_eq!(turkish.resolve(&at("Cmd+IntlBackslash", Key::IntlBackslash)), Some(chord("Cmd+IntlBackslash"))); + assert!(turkish.matches(&chord("IntlBackslash"), &plain, p)); + assert!(turkish.matches(&chord("Shift+IntlBackslash"), &shifted, p)); + for other in ["Shift+IntlBackslash", "\\", ",", "Shift+,"] { + assert!(!turkish.matches(&chord(other), &plain, p), "{other}"); + } + for other in ["IntlBackslash", "\\", "Shift+\\", "|", ",", "Shift+,"] { + assert!(!turkish.matches(&chord(other), &shifted, p), "Shift: {other}"); + } + + let label = |layout: &Layout, text: &str, p| chord(text).display_for(p, layout); + assert_eq!(label(&turkish, "Cmd+IntlBackslash", p), "Ctrl+IntlBackslash", "nothing typed yet"); + turkish.observe(Key::IntlBackslash, Mods::SHIFT, ">"); + assert_eq!(turkish.char_at(Key::IntlBackslash), None, "a Shift press teaches nothing"); + turkish.observe(Key::IntlBackslash, Mods::NONE, "<"); + assert_eq!(label(&turkish, "Cmd+IntlBackslash", p), "Ctrl+<"); + assert_eq!(label(&turkish, "Shift+IntlBackslash", p), "Shift+<"); + assert_eq!(label(&turkish, "Cmd+IntlBackslash", Platform::Mac), "⌘<"); + // The key's name is no character, so a position that types `I` does not hide the label. + let typed_i = Layout::new().with_char(Key::IntlBackslash, '<').with_char(Key::Quote, 'I'); + assert_eq!(label(&typed_i, "IntlBackslash", p), "<"); +} diff --git a/docs/language-testing.md b/docs/language-testing.md index 84fe9b9b..d5642a65 100644 --- a/docs/language-testing.md +++ b/docs/language-testing.md @@ -117,6 +117,10 @@ shortcut as listed. On macOS read Ctrl as Cmd. Q position, which types `'`, does not run Ctrl+Q. - [ ] **QWERTZ** (German). Ctrl+Z undoes and Ctrl+Y redoes, on the keys labelled Z and Y. Ctrl+⇧7 runs Ctrl+/. The menu shows Ctrl+[ as Ctrl+Ü. +- [ ] **Turkish Q.** The Keyboard Shortcuts dialog records the key next to left Shift, which + types `<`, as `IntlBackslash`, and with Shift held as `Shift+IntlBackslash`. Each runs its + command, and neither runs a `\` or Shift+, binding. Once `<` has been typed, the menu shows + Ctrl+IntlBackslash as Ctrl+<. - [ ] **Dvorak – QWERTY ⌘** (macOS). Typing gives Dvorak letters. With Cmd held, the keys follow QWERTY: Cmd on the QWERTY C position copies, and the menus show ⌘C. From 970fd6fa396951587a714deb61df13b1a4c35d95 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sun, 1 Nov 2026 03:31:51 +0000 Subject: [PATCH 2/2] keymap: test the UK ISO key; the Turkish Q item applies to Windows and Linux --- crates/keymap/tests/layout.rs | 8 ++++++++ docs/language-testing.md | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/keymap/tests/layout.rs b/crates/keymap/tests/layout.rs index 0f13f810..e2e7f2b2 100644 --- a/crates/keymap/tests/layout.rs +++ b/crates/keymap/tests/layout.rs @@ -256,4 +256,12 @@ fn the_iso_key_next_to_left_shift_keeps_shift_and_shows_the_layouts_character() // The key's name is no character, so a position that types `I` does not hide the label. let typed_i = Layout::new().with_char(Key::IntlBackslash, '<').with_char(Key::Quote, 'I'); assert_eq!(label(&typed_i, "IntlBackslash", p), "<"); + + // UK: the key types `\` and `|`, keys egui has, so it runs the `\` bindings. + let uk = Layout::new(); + let backslash = at("\\", Key::IntlBackslash); + let pipe = at("Shift+|", Key::IntlBackslash); + assert_eq!(uk.resolve(&backslash), Some(chord("\\"))); + assert!(uk.matches(&chord("\\"), &backslash, p)); + assert!(uk.matches(&chord("Shift+\\"), &pipe, p)); } diff --git a/docs/language-testing.md b/docs/language-testing.md index d5642a65..d7cd2e08 100644 --- a/docs/language-testing.md +++ b/docs/language-testing.md @@ -117,10 +117,10 @@ shortcut as listed. On macOS read Ctrl as Cmd. Q position, which types `'`, does not run Ctrl+Q. - [ ] **QWERTZ** (German). Ctrl+Z undoes and Ctrl+Y redoes, on the keys labelled Z and Y. Ctrl+⇧7 runs Ctrl+/. The menu shows Ctrl+[ as Ctrl+Ü. -- [ ] **Turkish Q.** The Keyboard Shortcuts dialog records the key next to left Shift, which - types `<`, as `IntlBackslash`, and with Shift held as `Shift+IntlBackslash`. Each runs its - command, and neither runs a `\` or Shift+, binding. Once `<` has been typed, the menu shows - Ctrl+IntlBackslash as Ctrl+<. +- [ ] **Turkish Q** (Windows and Linux; macOS reports this key as `` ` ``). The Keyboard + Shortcuts dialog records the key next to left Shift, which types `<`, as `IntlBackslash`, + and with Shift held as `Shift+IntlBackslash`. Each runs its command, and neither runs a `\` + or Shift+, binding. Once `<` has been typed, the menu shows Ctrl+IntlBackslash as Ctrl+<. - [ ] **Dvorak – QWERTY ⌘** (macOS). Typing gives Dvorak letters. With Cmd held, the keys follow QWERTY: Cmd on the QWERTY C position copies, and the menus show ⌘C.