Skip to content

Add Layout Hotkeys extension - #30140

Open
pavel-kh wants to merge 3 commits into
raycast:mainfrom
pavel-kh:layout-hotkeys
Open

Add Layout Hotkeys extension#30140
pavel-kh wants to merge 3 commits into
raycast:mainfrom
pavel-kh:layout-hotkeys

Conversation

@pavel-kh

Copy link
Copy Markdown

Description

Switch macOS keyboard layouts directly with a hotkey instead of cycling through them, and rewrite text that was typed with the wrong layout active.

macOS only offers "select next input source", so reaching a specific layout out of four means pressing the switch key three times. This gives four commands you can bind independent global hotkeys to, each landing directly on the layout assigned to it.

Commands

  • Switch to Layout 1–4 — switch straight to the input source assigned to that slot
  • Convert Selection to Layout 1–4 — rewrite the selected text as if it had been typed with that slot's layout
  • Convert Selection — preview the selection rewritten into each layout and pick one
  • Configure Layout Slots — choose which input source each slot selects
  • Cycle Input Source, Show Current Input Source, Switch Input Source

Notes for review

No hardcoded layout tables. Text conversion derives its mapping at runtime from each layout's own key tables via UCKeyTranslate and kTISPropertyUnicodeKeyLayoutData. For each character it resolves which physical key and modifiers produced it under the detected layout, then asks the target layout what that key produces. Any pair of enabled layouts therefore works, including custom .keylayout bundles built with Ukelele, with nothing to maintain per language. Round-trips (A → B → A) return the original exactly, including Option-key diacritics such as Polish ą ć ż.

Permissions. The switching commands need none — no Accessibility, no Input Monitoring, no synthesized keystrokes. The Convert Selection commands read the frontmost app's selection, which requires Raycast's own Accessibility permission.

Whole-field fallback. With nothing selected, the convert commands read the focused field through the Accessibility API rather than synthesizing ⌘A, since a synthetic keystroke would land on Raycast while it briefly holds focus. Because "the focused field" can be an entire document, anything over 200 characters asks for confirmation first, and a When Nothing Is Selected preference disables the fallback entirely.

Focus handling. A Raycast command runs with Raycast briefly frontmost. With macOS's "Automatically switch to a document's input source" enabled, switching then would be undone once focus returns, so the Swift side waits up to 400 ms for Raycast to stop being frontmost before calling TISSelectInputSource. The hotkey commands deliberately do not dismiss the Raycast window, so they also work while typing in Raycast's own search field.

Swift is bundled as source and compiled by extensions-swift-tools; no prebuilt binary is committed.

🤖 Generated with Claude Code

Switch macOS keyboard layouts directly with a hotkey instead of cycling
through them, and rewrite text that was typed with the wrong layout active.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@raycastbot raycastbot added new extension Label for PRs with new extensions platform: macOS labels Aug 11, 2026
@raycastbot

Copy link
Copy Markdown
Collaborator

Congratulations on your new Raycast extension! 🚀

We're currently experiencing a high volume of incoming requests. As a result, the initial review may take up to 15 business days.

Once the PR is approved and merged, the extension will be available on our Store.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a macOS extension for direct keyboard-layout switching and runtime key-layout-based text conversion.

  • Adds four configurable layout slots and direct-switch commands.
  • Adds selection and whole-field conversion commands backed by Swift Accessibility and Carbon APIs.
  • Adds configuration, current-source, cycle, and searchable source-picker views.
  • Includes Store metadata, documentation, and extension build configuration.

Confidence Score: 4/5

The PR does not yet appear safe to merge because whole-field conversion can overwrite a different application's focused field than the one whose contents were converted.

The picker separates field reading from replacement, while the apply path discards the original target identity and resolves the currently frontmost non-Raycast field again before selecting all and pasting.

Files Needing Attention: extensions/layout-hotkeys/src/lib/convert.ts and extensions/layout-hotkeys/swift/Sources/FocusedField.swift

Important Files Changed

Filename Overview
extensions/layout-hotkeys/src/lib/convert.ts Coordinates selection reading and replacement, but whole-field application can retarget a different field than the one originally converted.
extensions/layout-hotkeys/swift/Sources/FocusedField.swift Implements Accessibility field access but independently resolves the target application for each read and selection operation.
extensions/layout-hotkeys/src/lib/input-source.ts Provides typed Swift bindings, slot persistence, and generated preference access; the prior duplicated preference types are removed.
extensions/layout-hotkeys/src/convert.tsx Implements the conversion picker whose delayed action exposes the read-versus-write target-identity gap.
extensions/layout-hotkeys/package.json Declares the macOS commands, preferences, dependencies, and required Store metadata.
Prompt To Fix All With AI
### Issue 1
extensions/layout-hotkeys/src/lib/convert.ts:116-119
**Whole-Field Replacement Changes Target**

When another application becomes frontmost while the conversion picker is open, `selectAllInField()` re-resolves that application's focused field instead of retaining the field read at picker load, causing the converted text to overwrite an unrelated field or fail when the new field is not editable.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (3): Last reviewed commit: "Spell out how this differs from Keyboard..." | Re-trigger Greptile

Comment thread extensions/layout-hotkeys/src/lib/convert.ts
Comment thread extensions/layout-hotkeys/src/lib/convert.ts Outdated
Comment thread extensions/layout-hotkeys/src/lib/input-source.ts
… app

Convert Selection is a view command, so its window stays frontmost while
the command runs and waitForRaycastToYieldFocus can never resolve. Both
readFocusedField and selectAllInFocusedField therefore timed out and
queried Raycast's own search field, so the fallback that converts the
whole focused field failed with "No text field is focused." there. It
worked only from the no-view hotkeys, which dismiss the window first.

Resolve the target through the frontmost window that Raycast does not
own, keeping the existing frontmostApplication fast path, and drop the
wait from this path: Accessibility reads and writes do not need the
target app to be active. Hand focus back before rewriting the field so
the selection and the paste act on the same window.

Also treat a selection of nothing but whitespace as an explicit
selection rather than as nothing selected, so it can no longer escalate
to a whole-field rewrite, and type the preference helpers with the
generated Preferences so renaming a preference fails the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pernielsentikaer

Copy link
Copy Markdown
Collaborator

Similar extensions already in the Store

Automated first pass. Overlap is not a blocker on its own, but the README should make the difference clear.

  • Keyboard Layout Switcher (input-switcher, 4,530 installs by @lucaschultz) — Extension to switch keyboard layouts
    similarity 0.66

The store duplicate check flagged the overlap. Name the existing
extension in the README and say plainly what it does and does not do,
so the difference does not have to be inferred from the command list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pavel-kh

Copy link
Copy Markdown
Author

Thanks for the pointer — I've updated the README to name Keyboard Layout Switcher and spell out the difference, in 52e38fc.

For the record on the overlap itself: Keyboard Layout Switcher has two commands, a picker listing your layouts and "Toggle Next Layout". Neither of the things this extension is for is among them.

  • A hotkey that lands on one specific layout. Its only command that switches without further interaction is the toggle, which advances to the next layout in sequence; reaching a particular one means opening the picker and choosing from the list. Here, Switch to Layout 1–4 are four separate commands, so one hotkey goes straight to Hebrew and another straight to Russian.
  • Repairing text typed with the wrong layout active. The Convert Selection commands turn ghbdtn into привет by mapping each character through the physical key that produced it, derived from the layouts themselves rather than per-language tables. Keyboard Layout Switcher has no equivalent.

There's also no external dependency: it drives the separately installed keyboardSwitcher binary, while this calls Carbon's TISSelectInputSource from bundled Swift.

The genuine overlap is Switch Input Source and Cycle Input Source, which do roughly what its two commands do. Those are here so the four hotkeys aren't the only way in, and I'm happy to drop either if you'd rather keep the Store tidy.

Comment on lines +116 to +119
await closeMainWindow();

try {
await selectAllInField();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Whole-Field Replacement Changes Target

When another application becomes frontmost while the conversion picker is open, selectAllInField() re-resolves that application's focused field instead of retaining the field read at picker load, causing the converted text to overwrite an unrelated field or fail when the new field is not editable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/layout-hotkeys/src/lib/convert.ts
Line: 116-119

Comment:
**Whole-Field Replacement Changes Target**

When another application becomes frontmost while the conversion picker is open, `selectAllInField()` re-resolves that application's focused field instead of retaining the field read at picker load, causing the converted text to overwrite an unrelated field or fail when the new field is not editable.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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

Labels

new extension Label for PRs with new extensions platform: macOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants