[Feature] Allow Shift+Arrow / Shift+PageUp / Shift+PageDown keybindings — Shift-only chords on keys that produce no text
Problem or use case
Binding Shift+Up or Shift+Down to an action is rejected with "Include at least one modifier key.", so these chords cannot be used for any shortcut.
Shift is deliberately excluded from the modifier check in src/shared/keybindings/normalization.ts:
if (
!parsed.mod &&
!parsed.meta &&
!parsed.control &&
!parsed.alt &&
!isShiftInsert &&
!isBareAllowed &&
!isShiftOnlyAllowed
) {
return { ok: false, error: 'Include at least one modifier key.' }
}
parsed.shift is absent from that list, so only Mod, Meta/Cmd, Control and Alt satisfy the requirement.
The reason for that is sound, and this request does not ask to change it. A comment on the one action that opts out states it:
macOS uses
Shift+Spaceas an input-source shortcut; Orca otherwise rejects Shift-only bindings to avoid stealing typed text.
Shift+<letter> is how capitals and shifted symbols are typed, so a global Shift-only binding on a text-producing key would swallow typed input. That rationale is correct.
But it does not apply to keys that produce no text. isSafeBareKey in src/shared/keybindings/parser.ts already treats the arrows and paging keys as safe when bare:
return (
isFunctionKeyToken(parsed.key) ||
[
'Backspace', 'Delete', 'Enter', 'Escape', 'Tab',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
'PageUp', 'PageDown'
].includes(parsed.key)
)
Yet the moment Shift is held, the same function narrows to function keys only:
// Function keys produce no text, so they're safe bare or with Shift (Shift+letter stays unsafe).
if (parsed.shift) {
return isFunctionKeyToken(parsed.key)
}
So Shift+F7 is accepted while Shift+ArrowUp is not — even though an arrow key produces no text either, and is already trusted bare one branch below. The stated justification for the Shift branch ("function keys produce no text") is equally true of arrows and PageUp/PageDown.
Use case: Shift+Up / Shift+Down are natural bindings for directional navigation, and are the conventional shape for shift-extended movement in editors and terminals. Today they are unavailable for any action.
Proposed solution
Widen the Shift branch of isSafeBareKey so it admits the non-text keys the same function already trusts when bare, rather than function keys alone:
if (parsed.shift) {
return isFunctionKeyToken(parsed.key) || NON_TEXT_NAVIGATION_KEYS.includes(parsed.key)
}
where NON_TEXT_NAVIGATION_KEYS is the arrow and paging subset of the existing bare-key list (ArrowLeft, ArrowRight, ArrowUp, ArrowDown, PageUp, PageDown).
Deliberately not proposed:
- No change to the
Include at least one modifier key.rule innormalization.ts. Shift-only chords stay rejected by default. - No relaxation for
Shift+<letter>,Shift+<digit>or shifted punctuation. Those produce text and must stay unsafe. - No change to
Backspace,Delete,Enter,EscapeorTab. They produce no text either, butShift+TabandShift+Entercarry existing terminal meaning, so leaving them out keeps the change minimal. Happy to include them if maintainers prefer.
This stays opt-in per action: isSafeBareKey is only consulted when a definition sets allowBareKeybindings, so no existing binding changes behaviour and no action gains a Shift chord unless it already opts in. Actions wanting Shift+Arrow would set that flag, exactly as the F7 / Shift+F7 diff-navigation actions in definitions-core-3.ts do today.
Happy to open a PR for this, with tests covering the accepted and still-rejected cases.
Alternatives or additional context
Alternative considered — per-action allowShiftOnlyKeybindings. This flag already exists and permits a Shift-only chord, but only one action in the codebase uses it (the IME input-source switch in definitions-core-4.ts, added because macOS uses Shift+Space). Extending it action by action would work, but it grants any Shift chord including text-producing ones, which is a wider grant than needed. Narrowing isSafeBareKey instead keeps the text-swallowing protection intact.
No user-facing workaround exists. allowBareKeybindings and allowShiftOnlyKeybindings are properties of action definitions compiled into the app, not settings. Nothing in the Shortcuts pane, global-settings-types.ts, or a keybindings file exposes them, so a user cannot relax this for an action that does not already declare it.
Existing exception for precedent: Shift+Insert is already allowed unconditionally via the isShiftInsert check in normalization.ts, so a targeted carve-out for non-text keys is consistent with how this has been handled before.
Related, but not duplicates:
- #9338 — iTerm2-style key bindings mapping a key to arbitrary text or escape sequences. Different feature (binding to output rather than relaxing chord validation).
- #4388 — macOS
Option+Tabterminal tab shortcuts fail in the recorder. A different validation path.
Verified against main at the time of writing. Searched open and closed issues, pull requests, and all 73 GitHub Discussions; no existing report covers this restriction.
Source: stablyai/orca