A native macOS menu bar app for Windows-style keyboard layout switching. Assign Alt+Shift (⌥⇧), Ctrl+Shift (⌃⇧), or any other shortcut to cycle the keyboard to the next input source (same as “Select next source in Input menu”). With two layouts (EN/RU) this is a simple language toggle, like on Windows.
There is no Dock icon. The menu bar shows the current layout abbreviation (EN, РУ, and so on). The same label can follow the pointer in a per-layout color.
Requires macOS 13+. Bundle ID: ru.ebybin.LayoutSwitcher.
You need Command Line Tools:
xcode-select --installBuild and run:
cd /path/to/LayoutSwitcher
make app # builds LayoutSwitcher.app and ad-hoc codesigns it
make run # rebuilds if needed and opens the app
make clean # removes the .appmake builds for the current architecture (arm64 on Apple Silicon, x86_64 on Intel). There is no universal binary: on an Intel Mac, build on that machine.
make run opens the .app from the source directory. A copy in Applications is not updated automatically.
Copy the built app into Applications:
cp -R LayoutSwitcher.app /Applications/
open /Applications/LayoutSwitcher.appAfter make app / make run, copy the app again if you use the Applications copy or a login item:
killall LayoutSwitcher 2>/dev/null
cp -R LayoutSwitcher.app /Applications/
open /Applications/LayoutSwitcher.appOtherwise the menu bar may keep an old build. The hotkey, badge colors, badge offset, and UI language live in the user’s UserDefaults — replacing the .app does not reset them.
If the login item points at /Applications/LayoutSwitcher.app, launching that copy (or logging out and back in) is enough after cp. A second launch with the same bundle ID force-quits the already running instance, so the source-tree build and the Applications copy cannot both handle the hotkey.
The app is ad-hoc signed (codesign -s -), not notarized. There is no Apple Developer ID. After a download, AirDrop, or copy into /Applications, macOS 15+ often shows:
Apple could not verify “LayoutSwitcher” is free of malware that may harm your Mac or compromise your privacy
with Move to Trash. That is Gatekeeper, not a real malware scan. Do not delete the app.
- Click Done (not Move to Trash).
- System Settings → Privacy & Security → scroll to the Security section.
- Next to the LayoutSwitcher warning, click Open Anyway, confirm, then open the app again.
On older macOS, right-click LayoutSwitcher.app → Open → Open.
To drop the quarantine flag (same effect as Open Anyway):
xattr -cr /Applications/LayoutSwitcher.app
open /Applications/LayoutSwitcher.appThe app does not add itself to login items.
- Put
LayoutSwitcher.appin/Applications. - System Settings → General → Login Items & Extensions → + → LayoutSwitcher.
After login it starts on its own. The hotkey, badge colors, and offset persist across restarts and .app replacements.
If you quit the app (Exit in the menu, kill, crash):
open /Applications/LayoutSwitcher.appor double-click the .app. Look for the layout abbreviation in the menu bar — there is no Dock icon.
- Copy
LayoutSwitcher.app(AirDrop, USB,scp) or copy the sources and runmake appon that machine (required for a different CPU architecture). - Copy the
.appto/Applications. - Bypass Gatekeeper (see First launch and Gatekeeper: Open Anyway, not Move to Trash).
- Set the hotkey and colors in the app settings — they do not travel from another Mac.
- For modifier-only shortcuts (⌘⇧) and the cursor badge, enable LayoutSwitcher in Accessibility.
- Optionally add it to login items.
On first launch (no hotkey yet) the settings window opens. Later: click the abbreviation in the menu bar → Settings….
The interface language defaults to English. Change it in settings with the Language picker (English and Russian ship in the repo). The choice is stored in UserDefaults (appLanguage) and does not follow the macOS UI language.
To add another language, put Locales/<code>.json next to en.json (same keys, plus "name" for the picker). Rebuild with make app — the new file is copied into the .app and appears in the list. Missing keys fall back to English.
- Click the button that shows the current shortcut (or “Not set”).
- Press the keys you want. Esc cancels.
- Clear removes the shortcut so the app no longer switches layouts.
Two modes:
| Type | Example | How to assign | When it fires |
|---|---|---|---|
| Modifier + key | ⌃Space, ⌥⌘L | Hold modifiers and press a key | Immediately on key down |
| Modifiers only | ⌘⇧, ⌃⇧ | Hold at least two modifiers and release | On release, if no third key was pressed |
⌘⇧Z remains Redo: the layout does not switch if a regular key was pressed while ⌘⇧ was held.
You cannot assign a single key with no modifier, or a single modifier with no second key (for example ⌘ alone).
Windows cycles the input language with Alt+Shift or Ctrl+Shift. LayoutSwitcher does the same on a Mac:
- Assign ⌥⇧ (Option+Shift — the Mac equivalent of Alt+Shift) or ⌃⇧ (Control+Shift).
- Release the keys to switch EN ↔ RU (or to the next source in the system list).
- Shortcuts that add another key still work: ⌥⇧⌘C does not change the layout.
This is for anyone who wants Alt+Shift or Ctrl+Shift to change the keyboard language on macOS the way Windows does, instead of Caps Lock or Control+Space.
For ⌘⇧ and for the badge to follow the pointer in other apps, macOS will not expose those events without Accessibility access:
System Settings → Privacy & Security → Accessibility → enable LayoutSwitcher.
The settings window has an Open Accessibility button. After enabling it, return to the app — the status is rechecked when the app becomes active. If the hotkey or badge still does nothing, restart LayoutSwitcher.
Shortcuts that include a regular key (Carbon RegisterEventHotKey) do not need this permission.
The system “Select the previous input source” action steals Control+Space before this app. To use that combo:
System Settings → Keyboard → Keyboard Shortcuts → Input Sources — uncheck it.
Add the layouts you need:
System Settings → Keyboard → Text Input → Edit…
The app cycles enabled, selectable Keyboard-category sources (what appears in the Input menu).
There is no public API to recolor the system pointer, so the layout abbreviation (EN, РУ) is drawn next to the cursor and follows the mouse.
In settings:
- checkbox Show abbreviation near the cursor (on by default);
- sliders Right and Down — offset from the cursor, range −60…80 pt (negative means left / up). Defaults are 14 right and 32 down. Reset position restores those values;
- a color per layout (
ColorPicker). Default is black; the text has a white outline so it stays visible on dark backgrounds.
Following the pointer in other apps needs Accessibility (same as ⌘⇧). Without it, the badge only tracks the mouse over LayoutSwitcher’s own windows.
- Click the menu bar abbreviation: current layout, current hotkey, Settings…, Quit.
- Changing the layout (hotkey or the system input menu) updates the menu bar text and the cursor badge.
- Quit fully exits the app; the hotkey and badge stop until you open the
.appagain.
No third-party dependencies. Build: Makefile + swiftc (an Xcode project is not required).
LayoutSwitcher/
Makefile
Info.plist
README.md # English (GitHub default)
README.ru.md # Russian
Locales/
en.json # UI strings (default)
ru.json # UI strings (Russian)
Sources/
LayoutSwitcherApp.swift # @main, MenuBarExtra, settings window
SettingsView.swift # shortcut recording, badge colors, AX hint
LocalizationStore.swift # loads Locales/*.json, in-app language
Hotkey.swift # keyCode + modifiers model
HotkeyManager.swift # hotkey registration, persistence
InputSourceSwitcher.swift # TIS source cycle, layout list
LayoutAppearanceStore.swift # per-sourceID colors, badge flag and offset
CursorBadgeController.swift # NSPanel with abbreviation near the cursor
LayoutSwitcher.app/ # make app output
Info.plist: LSUIElement=true (no Dock icon), LSMinimumSystemVersion=13.0.
Hotkey --> HotkeyManager --> InputSourceSwitcher.switchToNext()
|
v
TISSelectInputSource
|
v
kTISNotifySelectedKeyboardInputSourceChanged
|
+-----------------+-----------------+
v v
menu bar (EN / РУ) CursorBadgeController
(color from LayoutAppearanceStore)
Carbon Text Input Source Services:
TISCreateInputSourceListwith categorykTISCategoryKeyboardInputSource.- Filter:
IsEnabledandIsSelectCapable. - Current source:
TISCopyCurrentKeyboardInputSource. - Next in the list (wrapping around):
TISSelectInputSource. - Subscribe to
kTISNotifySelectedKeyboardInputSourceChanged— refresh name, abbreviation,currentSourceID, and thelayoutslist.
Abbreviation: known InputSourceID values (com.apple.keylayout.US → EN, Russian → РУ), otherwise the language code, otherwise the first two letters of the localized name.
Two mechanisms, depending on the shortcut type.
Modifier + key. RegisterEventHotKey (Carbon). No Accessibility permission. System shortcuts with the same combo win. Cannot capture Fn or bare modifiers.
Modifiers only. Carbon cannot register that. Event monitors are installed:
NSEvent.addGlobalMonitorForEvents— events in other apps (needs Accessibility);NSEvent.addLocalMonitorForEvents— when LayoutSwitcher itself is focused (the global monitor does not see those).
Firing: exact modifier set match → “combo is held”; a keyDown in that window marks “extra key”; on modifier release, if there was no extra key and the new set is a subset of the assigned combo, the layout switches.
The hotkey is stored in UserDefaults (key hotkey) as JSON: keyCode (null for modifier-only) and modifiers (NSEvent.ModifierFlags.rawValue).
Local monitor for .keyDown + .flagsChanged:
- a regular key with a modifier —
Hotkey.fromKeyDownimmediately; - a peak of ≥2 modifiers with no regular key —
Hotkey.fromModifierswhen all keys are released; - Esc — cancel;
- Clear — drop the assigned shortcut.
A transparent NSPanel (borderless, nonactivating, above the cursor window level, all Spaces) with an NSTextField abbreviation. It does not intercept clicks. Position: NSEvent.mouseLocation plus the settings offset: badgeOffsetX to the right, badgeOffsetY down (defaults 14 and 32; in AppKit, down is negative Y). Monitors: .mouseMoved / drag (global + local). Panel creation is deferred to the next run-loop turn so SwiftUI App startup does not crash.
Colors: UserDefaults key layoutColors — map sourceID → {r,g,b,a}; missing entry = black. Flag showCursorBadge (on by default). Offset: badgeOffsetX / badgeOffsetY.
- Carbon hotkeys do not override already taken system shortcuts.
- Fn and a single modifier are not captured.
- macOS 13+ is required (
MenuBarExtra). - The binary is built for one architecture.
- Ad-hoc signature: Gatekeeper may prompt on another Mac.
- After granting Accessibility, a restart is sometimes required.
- The badge does not recolor the system cursor (I-beam, resize, and so on) — it only draws a label next to it.
- In some full-screen games that capture the pointer, the badge may be hidden.
- Without Accessibility, badge movement is not tracked in other apps.