mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #73015 from NousResearch/bb/layout-aware-keybinds
fix(desktop): resolve keybinds through the active keyboard layout
This commit is contained in:
commit
094a558635
2 changed files with 75 additions and 3 deletions
|
|
@ -32,6 +32,50 @@ describe('comboFromEvent — ctrl as a distinct modifier on macOS', () => {
|
|||
expect(comboFromEvent(keydown({ code: 'KeyK', ctrlKey: true }))).toBe('ctrl+k')
|
||||
})
|
||||
|
||||
it('uses layout-aware letters for Cmd shortcuts on non-QWERTY layouts', async () => {
|
||||
const { comboFromEvent } = await loadCombo('MacIntel')
|
||||
|
||||
expect(comboFromEvent(keydown({ code: 'KeyI', key: 'c', metaKey: true }))).toBe('mod+c')
|
||||
expect(comboFromEvent(keydown({ code: 'KeyI', key: 'C', metaKey: true, shiftKey: true }))).toBe('mod+shift+c')
|
||||
})
|
||||
|
||||
it('keeps shifted punctuation anchored to the physical key token', async () => {
|
||||
const { comboFromEvent } = await loadCombo('MacIntel')
|
||||
|
||||
expect(comboFromEvent(keydown({ code: 'Slash', key: '?', metaKey: true, shiftKey: true }))).toBe('mod+shift+/')
|
||||
})
|
||||
|
||||
it('uses layout-aware punctuation for Cmd shortcuts on non-QWERTY layouts', async () => {
|
||||
const { comboFromEvent } = await loadCombo('MacIntel')
|
||||
|
||||
// Dvorak puts "." on the physical QWERTY V key — ⌘. must still reach the
|
||||
// command center rather than resolving to the physical token.
|
||||
expect(comboFromEvent(keydown({ code: 'KeyV', key: '.', metaKey: true }))).toBe('mod+.')
|
||||
expect(comboFromEvent(keydown({ code: 'KeyW', key: ',', metaKey: true }))).toBe('mod+,')
|
||||
expect(comboFromEvent(keydown({ code: 'BracketLeft', key: '/', metaKey: true }))).toBe('mod+/')
|
||||
// AZERTY reaches "," from the physical QWERTY M key.
|
||||
expect(comboFromEvent(keydown({ code: 'KeyM', key: ',', metaKey: true }))).toBe('mod+,')
|
||||
})
|
||||
|
||||
it('keeps digits physical so AZERTY\'s shifted number row still binds', async () => {
|
||||
const { comboFromEvent } = await loadCombo('MacIntel')
|
||||
|
||||
// On AZERTY the unshifted "1" key types "&", and "1" only with Shift held.
|
||||
// Both must resolve to the same `mod+1` the QWERTY user gets.
|
||||
expect(comboFromEvent(keydown({ code: 'Digit1', key: '&', metaKey: true }))).toBe('mod+1')
|
||||
expect(comboFromEvent(keydown({ code: 'Digit1', key: '1', metaKey: true }))).toBe('mod+1')
|
||||
})
|
||||
|
||||
it('falls back to the physical key for glyphs we do not ship as tokens', async () => {
|
||||
const { comboFromEvent } = await loadCombo('MacIntel')
|
||||
|
||||
// Option-modified glyphs, dead keys, and non-Latin scripts are not combo
|
||||
// tokens, so the physical code keeps the binding reachable.
|
||||
expect(comboFromEvent(keydown({ code: 'KeyK', key: '˚', metaKey: true, altKey: true }))).toBe('mod+alt+k')
|
||||
expect(comboFromEvent(keydown({ code: 'KeyN', key: 'Dead', metaKey: true, altKey: true }))).toBe('mod+alt+n')
|
||||
expect(comboFromEvent(keydown({ code: 'KeyK', key: 'л', metaKey: true }))).toBe('mod+k')
|
||||
})
|
||||
|
||||
it('treats Control as the "mod" accelerator off macOS', async () => {
|
||||
const { comboFromEvent } = await loadCombo('Win32')
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
//
|
||||
// A combo is a canonical lowercase string like "mod+k", "mod+shift+]", "shift+x",
|
||||
// or "r". `mod` is Cmd on macOS / Ctrl elsewhere, so a single binding works on
|
||||
// both. We derive the base key from `event.code` (not `event.key`) so Shift never
|
||||
// mutates it ("shift+/" stays "shift+/" instead of becoming "shift+?").
|
||||
// both. We derive the base key from `event.key` where the layout matters
|
||||
// (letters, unshifted punctuation) and from `event.code` otherwise, so a
|
||||
// binding follows the character the user's layout actually types while Shift
|
||||
// never mutates it ("shift+/" stays "shift+/" instead of becoming "shift+?").
|
||||
//
|
||||
// `ctrl` is physical Control, distinct from `mod`. It only matters on macOS,
|
||||
// where `mod` is Cmd and Cmd+Tab is OS-reserved — so `ctrl+tab` is literally
|
||||
|
|
@ -70,6 +72,32 @@ function baseKeyFromCode(code: string): string | null {
|
|||
return CODE_TO_KEY[code] ?? null
|
||||
}
|
||||
|
||||
// Punctuation we ship as combo tokens, derived from CODE_TO_KEY so the two
|
||||
// can't drift. Named tokens (space, tab, …) are excluded by the length check.
|
||||
const PUNCTUATION_KEYS = new Set(Object.values(CODE_TO_KEY).filter(token => token.length === 1))
|
||||
|
||||
// The layout-aware half of the base key. `event.key` carries the character the
|
||||
// user's layout actually produces, which is what a binding should match:
|
||||
//
|
||||
// - Letters always win, shifted or not — `toLowerCase` normalizes the case.
|
||||
// - Punctuation only when Shift is UP, because a shifted `event.key` is the
|
||||
// shifted glyph ("?" for "/"), and combos stay anchored to the unshifted
|
||||
// token. Shifted punctuation falls through to `event.code` below.
|
||||
//
|
||||
// Digits deliberately stay physical: on AZERTY the number row is shifted, so
|
||||
// `event.key` for the "1" key is "&" and only yields "1" with Shift held —
|
||||
// `event.code` is what keeps `mod+1` reachable there.
|
||||
//
|
||||
// Anything else (Option glyphs like "˚", dead keys, non-Latin scripts) isn't a
|
||||
// token we ship, so it fails both checks and falls back to the physical code.
|
||||
function baseKeyFromEventKey(key: string, shiftKey: boolean): string | null {
|
||||
if (/^[a-z]$/i.test(key)) {
|
||||
return key.toLowerCase()
|
||||
}
|
||||
|
||||
return !shiftKey && PUNCTUATION_KEYS.has(key) ? key : null
|
||||
}
|
||||
|
||||
// Returns the canonical combo for a keydown, or null while only modifiers are
|
||||
// held (so capture mode keeps waiting for a real key).
|
||||
export function comboFromEvent(event: KeyboardEvent): string | null {
|
||||
|
|
@ -77,7 +105,7 @@ export function comboFromEvent(event: KeyboardEvent): string | null {
|
|||
return null
|
||||
}
|
||||
|
||||
const base = baseKeyFromCode(event.code)
|
||||
const base = baseKeyFromEventKey(event.key, event.shiftKey) ?? baseKeyFromCode(event.code)
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue