Skip to content

[Bug]: extension action shortcuts on arrow keys never fire (ASCII recovery accepts UCKeyTranslate's control characters) #1071

Description

@FezVrasta

Existing issues

  • I searched open and closed issues and didn't find this already reported

What happened?

An extension action whose shortcut key is an arrow never fires. The action panel renders the shortcut correctly, so it looks bound, and pressing it does nothing. Picking the same action from the panel with the mouse works, so the handler is fine.

Snake (tonka3000) is the clearest case because every direction is ⌘⇧+arrow, which makes it unplayable. It isn't one extension though: 8 of my 106 installed extensions declare an arrow shortcut (snake, color-picker, homeassistant, hue, producthunt, rss-reader, translate, cleanshotx).

Cause: ASCIIKeyboardLayout.keyEquivalent(fallingBackTo:).

Extension action shortcuts are its only caller (ExtensionPaletteModifiers.swift:12). It recovers the logical ASCII key from the current layout so ⌘K still works on Dvorak, guarded by this:

let character = character(for: event)?.lowercased().first,
character.unicodeScalars.allSatisfy(\.isASCII)

That guard is there to reject a non-ASCII layout character. But UCKeyTranslate maps the arrow keycodes to ASCII control characters, and those pass isASCII happily. So the recovery returns the control character instead of falling through to SwiftUI's key, and ExtensionAction.matches compares it against KeyEquivalent.upArrow, which is U+F700. Never equal, so the action is never found.

Measured on this Mac, translating each keycode with cmdKey the way character(for:) does:

Raycast key UCKeyTranslate SwiftUI KeyEquivalent passes isASCII fires
arrowUp U+1E U+F700 yes no
arrowDown U+1F U+F701 yes no
arrowLeft U+1C U+F702 yes no
arrowRight U+1D U+F703 yes no
deleteForward U+7F U+F728 yes no
pageUp U+B U+F72C yes no
pageDown U+C U+F72D yes no
home U+1 U+F729 yes no
end U+4 U+F72B yes no
return U+D U+D yes yes
delete U+8 U+8 yes yes
tab U+9 U+9 yes yes
escape U+1B U+1B yes yes
space U+20 U+20 yes yes
c U+63 U+63 yes yes

So it's every key whose SwiftUI KeyEquivalent lives in the private-use area. return, delete, tab, escape and space survive by luck: the control character UCKeyTranslate hands back happens to be the one SwiftUI uses for them.

One more wrinkle worth knowing before anyone tries to reproduce it. The ASCII recovery only runs when the chord carries ⌘ or ⌃:

!event.modifierFlags.isDisjoint(with: [.command, .control])

So ⌥⇧↑ works today and ⌘⇧↑ doesn't, on the same action. That's why this reads as intermittent if you go looking at a few extensions rather than at the keycodes.

Scope. Only extension action shortcuts. keyEquivalent(fallingBackTo:) has that single call site, and ASCIIKeyboardLayout.matches(_:character:) always compares against a literal letter ("k", "p"), so the palette's own bindings can't hit this. character(for:) on its own is fine everywhere it's used.

Suggested fix. Return key untouched when it's already one of SwiftUI's named special keys, before consulting the layout at all. The recovery exists for letters and punctuation on a non-QWERTY layout, and a private-use key has no layout meaning to recover. Filtering on "printable ASCII only" would also work, but the private-use test says what's actually meant.

Nothing in Tests/ covers dispatchShortcut or ExtensionAction.matches right now, which is presumably how it went unnoticed.

Steps to reproduce

  1. Install Snake (tonka3000).
  2. Run Play Snake.
  3. Press ⌘⇧↑, ⌘⇧↓, ⌘⇧← or ⌘⇧→. Nothing happens, and the snake keeps going until it dies.
  4. Open the action panel and click Up. It moves, so the action itself works.

Screenshots or recording

The action panel showing the four direction actions with their shortcuts rendered correctly, none of which fire:

The keycode table above is the more useful artefact. It came from calling UCKeyTranslate directly with the same arguments ASCIIKeyboardLayout.character(for:) passes, then comparing against what ExtensionAction.keyEquivalent returns for each Raycast key name.

Tinycast version + channel

0.11.3 stable

macOS version

27.0 (26A428)

Contribution

  • I'm willing to implement this myself once this issue is labelled approved

Small one: an early return in keyEquivalent(fallingBackTo:) plus a harness covering ExtensionAction.matches across that key table, since there's nothing there today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    approvedGreen-lit. A PR may link this issue and will pass the gate.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions