Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LayoutSwitcher

English | Русский

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.

Install

Build from source

You need Command Line Tools:

xcode-select --install

Build 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 .app

make 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.app

Update after a rebuild

After 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.app

Otherwise 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.

First launch and Gatekeeper

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.

  1. Click Done (not Move to Trash).
  2. System Settings → Privacy & Security → scroll to the Security section.
  3. Next to the LayoutSwitcher warning, click Open Anyway, confirm, then open the app again.

On older macOS, right-click LayoutSwitcher.appOpenOpen.

To drop the quarantine flag (same effect as Open Anyway):

xattr -cr /Applications/LayoutSwitcher.app
open /Applications/LayoutSwitcher.app

Launch at login

The app does not add itself to login items.

  1. Put LayoutSwitcher.app in /Applications.
  2. 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.

Start manually

If you quit the app (Exit in the menu, kill, crash):

open /Applications/LayoutSwitcher.app

or double-click the .app. Look for the layout abbreviation in the menu bar — there is no Dock icon.

Install on another Mac

  1. Copy LayoutSwitcher.app (AirDrop, USB, scp) or copy the sources and run make app on that machine (required for a different CPU architecture).
  2. Copy the .app to /Applications.
  3. Bypass Gatekeeper (see First launch and Gatekeeper: Open Anyway, not Move to Trash).
  4. Set the hotkey and colors in the app settings — they do not travel from another Mac.
  5. For modifier-only shortcuts (⌘⇧) and the cursor badge, enable LayoutSwitcher in Accessibility.
  6. Optionally add it to login items.

Settings

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.

Assign a shortcut

  1. Click the button that shows the current shortcut (or “Not set”).
  2. Press the keys you want. Esc cancels.
  3. 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-style layout switching (Alt+Shift / Ctrl+Shift)

Windows cycles the input language with Alt+Shift or Ctrl+Shift. LayoutSwitcher does the same on a Mac:

  1. Assign ⌥⇧ (Option+Shift — the Mac equivalent of Alt+Shift) or ⌃⇧ (Control+Shift).
  2. Release the keys to switch EN ↔ RU (or to the next source in the system list).
  3. 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.

Accessibility

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.

Conflict with system Control+Space

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.

Input sources

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).

Cursor badge

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.

Usage

  • 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 .app again.

Implementation

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.

Data flow

Hotkey  -->  HotkeyManager  -->  InputSourceSwitcher.switchToNext()
                                      |
                                      v
                              TISSelectInputSource
                                      |
                                      v
              kTISNotifySelectedKeyboardInputSourceChanged
                                      |
                    +-----------------+-----------------+
                    v                                   v
           menu bar (EN / РУ)                 CursorBadgeController
                                              (color from LayoutAppearanceStore)

Layout switching (InputSourceSwitcher)

Carbon Text Input Source Services:

  1. TISCreateInputSourceList with category kTISCategoryKeyboardInputSource.
  2. Filter: IsEnabled and IsSelectCapable.
  3. Current source: TISCopyCurrentKeyboardInputSource.
  4. Next in the list (wrapping around): TISSelectInputSource.
  5. Subscribe to kTISNotifySelectedKeyboardInputSourceChanged — refresh name, abbreviation, currentSourceID, and the layouts list.

Abbreviation: known InputSourceID values (com.apple.keylayout.USEN, RussianРУ), otherwise the language code, otherwise the first two letters of the localized name.

Global hotkey (HotkeyManager)

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).

Shortcut recording (SettingsView)

Local monitor for .keyDown + .flagsChanged:

  • a regular key with a modifier — Hotkey.fromKeyDown immediately;
  • a peak of ≥2 modifiers with no regular key — Hotkey.fromModifiers when all keys are released;
  • Esc — cancel;
  • Clear — drop the assigned shortcut.

Badge (CursorBadgeController + LayoutAppearanceStore)

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.

Limitations

  • 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.

License

MIT

About

macOS menu bar app for Windows-style keyboard layout switching (Alt+Shift / Ctrl+Shift)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages