Keyboard Layout Switcher
macOS menu bar app that switches the keyboard input source automatically based on which physical keyboard is connected — e.g. one layout for an external keyboard, another for the built-in one, applied the moment you plug in or unplug.
Adds a per-keyboard layout picker in the menu bar and Settings, and remembers every keyboard it has ever seen — including ones that aren't currently plugged in. Requires no Input Monitoring or Accessibility permission; reads IOKit registry properties only, never opens a device.
See CLAUDE.md for the hard constraints a future change must not break, and PRODUCT.md / DESIGN.md for what the app is for and how it is meant to look. Implementation reasoning lives in comments next to the code it explains.
- Menu bar shows the active layout by name; menu lists every connected keyboard with a submenu to assign its layout, applied immediately
- Ordered priority list (Settings): the highest-ranked connected keyboard with an assigned layout wins; otherwise a fallback layout applies
- Remembers every keyboard it has seen, connected or not, keyed by vendor/product ID and product name
- Detects layout switches made outside the app (system input menu) and offers to remember them for the currently connected keyboard
- Launch-at-login toggle (
SMAppService), on by default - Re-asserts the correct layout on sleep/wake
Real screenshots, dark appearance. The status item carries the keycap mark when the layout name is switched off in Settings; with it on, the mark gives way to the name.
There is no notarized download. This project is distributed as source: you build it, so the copy you run was never downloaded and macOS never quarantines it. That is a deliberate choice — see below.
git clone https://github.com/nicks-passiert/rollover.git && cd rollover
./scripts/build-app.sh
cp -R build/Rollover.app /Applications/
open /Applications/Rollover.appYou need the Xcode Command Line Tools (xcode-select --install). Full Xcode is not
required. No Apple ID, no developer account, no certificate — the script signs ad-hoc when
it finds nothing better.
Reinstalling over a running copy: quit the running app first. macOS does not stop a live process when its bundle is replaced, so the old build keeps running and you end up looking at the previous version's windows, wondering why your change did not take. Quit it from the menu bar, or:
pkill -f 'Rollover.app/Contents/MacOS'Your remembered keyboards survive a reinstall — they live in Application Support, not in the bundle.
On first launch the app registers itself as a login item, so expect macOS to tell you a background item was added. It will also ask for notification permission, used only for the banner when a layout switches and the "remember this?" prompt. Deny it and the app works fine, silently.
Ad-hoc signing does not appear to cost you either of those. Tested on macOS 26: replacing
a certificate-signed install with an ad-hoc build left the login item enabled, and a
fresh SMAppService registration under an ad-hoc signature returned enabled with no
approval step. macOS keys these on the bundle identifier and path rather than the
signature hash. Notification permission showed no sign of resetting either, though that
half was observed rather than deliberately provoked.
macOS attaches the com.apple.quarantine attribute when a file arrives from a browser,
AirDrop, Mail, or Messages — not when you compile it locally. Quarantine is what makes
Gatekeeper evaluate an app, and Gatekeeper refuses anything not signed with a Developer
ID Application certificate and notarized by Apple. Since macOS 15 the old
Control-click → Open shortcut is gone: a user has to visit System Settings → Privacy &
Security and click Open Anyway past a warning that the app cannot be checked for
malicious software.
Developer ID requires a paid Apple Developer Program membership, which this project does not have. That leaves two honest options and one dishonest one. Building from source is the honest option taken here.
The dishonest one is shipping a binary with xattr -d com.apple.quarantine in the install
instructions. It works, and it teaches people to strip quarantine from downloaded
binaries — the exact habit that gets them compromised later. An app whose entire pitch is
that it reads no keystrokes and needs no Input Monitoring permission has no business
asking anyone to disable a security check to run it.
The App Store is not a third option regardless: TISSelectInputSource does not work
inside the App Sandbox, so the app is deliberately unsandboxed.
The build script already supports the notarized path, so enrolling is the only missing piece:
./scripts/build-app.sh --notarizeIt signs with Developer ID, submits to Apple, waits for the verdict, staples the ticket
into the bundle, re-zips so the archive carries it, and asks Gatekeeper for its own verdict
via spctl. It needs a Developer ID Application certificate and one-time stored
credentials:
xcrun notarytool store-credentials "rollover" \
--apple-id "you@example.com" \
--team-id "YOURTEAMID" \
--password "app-specific-password"The password is an app-specific password from appleid.apple.com, not your Apple ID
password. Override the profile name with NOTARY_PROFILE=…. The script fails with
instructions if either prerequisite is missing. This path has never been run end to end.
scripts/build-app.sh compiles, assembles the bundle, and signs it. The Command Line
Tools are enough — no Xcode installation required — and --help lists the options.
It picks a signing identity in this order, so a fresh clone builds whether or not you have an Apple account:
| Identity | When | Good for |
|---|---|---|
$SIGN_IDENTITY |
you set it | choosing explicitly |
| Developer ID Application | --notarize |
anyone's Mac |
| Apple Development | default, if present | your Mac |
ad-hoc (-) |
nothing else found | your Mac |
Hardened runtime is applied on every path — codesign -dv must show
flags=0x10000(runtime), or 0x10002(adhoc,runtime) when signing ad-hoc. Without it
any same-user process could inject code via DYLD_INSERT_LIBRARIES into something that
runs at every login.
Releases are tagged v1.2.3 and listed on GitHub, with notes taken from
CHANGELOG.md. main is kept buildable between them; the tags exist so
you can build from a known state rather than from whatever landed last.
The version you see in Settings → About is two numbers: the release, from the tag you built, and in brackets the build — the count of commits in your checkout. Quote both when reporting a problem; the second identifies the exact commit.
No release carries a binary. That is deliberate — see "Why there is no download".
Worth walking once after building from source, or after a change to switching, signing, or the login item. Steps 3 and 5 are the ones that catch real regressions.
codesign -dv build/Rollover.appreportsflags=0x10000(runtime), or0x10002(adhoc,runtime)when signed ad-hoc.- Launch it. The status item shows the active layout's name, and the menu lists every connected keyboard.
- Assign a layout to an external keyboard, then unplug it. The menu bar flips to the
fallback (or to the next connected keyboard that has one), and back on replug.
Cross-check with
defaults read com.apple.HIToolbox AppleCurrentKeyboardLayoutInputSourceID. - Assign a layout from the menu submenu. It applies immediately and survives a relaunch.
- Switch layout by hand from the system input menu. A "remember this?" prompt appears, attributed to the highest-ranked connected keyboard. Then confirm a switch the app makes produces no prompt — that suppression window is the only thing stopping a feedback loop.
- Toggle Launch at login, then log out and back in. The app is running, and the toggle still reads on.
- Sleep and wake the Mac. The layout is re-asserted.
Core switching behavior is implemented and verified end to end: build and sign, connect/disconnect, manual-layout-switch detection, login item across reboot, sleep/wake. Name, icon, and About are in place; nothing is knowingly missing.
Two things worth knowing about how this is put together:
- There is no About panel, by design.
LSUIElementplusMenuBarExtrameans the app has no app menu, so there is no standard "About Rollover" item and no panel to open. Settings carries it instead: identity at the top of the window, provenance at the bottom. - There is no update check. The app never opens a network connection. Pull the repository and run the build script again.
GPL-3.0-or-later — see LICENSE. Use it, study it, modify it, redistribute it. If you distribute a modified version, it has to carry the same licence and ship its source, so whoever receives it gets what you got.
That obligation is on distribution, not on use. Change it however you like for yourself and you owe nobody anything.
Worth being clear about what copyleft does and does not cover, because it is routinely
overstated: it protects this code, not the idea behind it. The part most worth learning
from — the approach in KeyboardMonitor.swift, which identifies keyboards from IOKit
registry properties without ever opening a device, and so needs no Input Monitoring or
Accessibility permission — is a technique, and techniques are not copyrightable. Read it,
understand it, and implement it yourself in whatever you like under whatever licence you
like. The licence only asks that you not take this code itself proprietary.
Two things sit outside the grant: the application icon in Resources/AppIcon.iconset/, and
the name Rollover. Both are artwork and identity rather than code. If you ship a fork,
please give it your own name and your own mark — not a trademark claim, just the ordinary
courtesy that keeps two different apps from looking like the same one.

