Skip to content

Repository files navigation

AppInputSourceSwitcher

A minimal, auditable macOS menu-bar utility that selects a configured keyboard input source when an application becomes frontmost.

The project is intentionally narrow:

  • exact per-app bundle-ID rules plus one default rule;
  • switch on activation, not a continuous layout lock;
  • App Sandbox enabled;
  • no Accessibility or Input Monitoring permission;
  • no keyboard event taps, clipboard access, browser inspection, or telemetry;
  • no networking, updater, subprocesses, or third-party dependencies.

Per-app input-source switching is not new. The reason this implementation exists is to provide a small source-first option whose security boundaries are easy to inspect and mechanically test. See docs/comparison.md for the positioning against related projects.

Why build another switcher?

We reviewed the main open-source alternatives before implementing this project:

  • LockIME is the closest and most full-featured option, but it is designed for continuous locking, per-URL rules, optional Accessibility-powered features, and Sparkle updates. We needed a one-shot per-app switcher with a much smaller runtime and audit surface.
  • TypeSwitch has a strong per-app rules model, but its application architecture includes several third-party packages, Sparkle, and a LaunchAgent fallback. Our requirements excluded updaters, subprocesses, custom LaunchAgents, and external dependencies.
  • Input Source Pro intentionally covers a broader product surface, including indicators and browser-aware features. Those capabilities require permissions and APIs that this project deliberately avoids.
  • KeyMinder and SwitchKey use broader permission or login-item approaches than we wanted, while macism is a useful command-line building block rather than a per-app menu-bar service.

This is a requirements decision, not a claim that those projects are inferior. Choose them when their UI and broader features are useful. This implementation exists for environments where the priority order is: no TCC permissions, App Sandbox, no network or updater, no third-party dependencies, deterministic switching, and source that can be audited quickly.

How it behaves

On each application activation, the app:

  1. waits for a 40 ms cancellable debounce;
  2. re-reads the actual frontmost application's bundle identifier;
  3. resolves an exact rule, an ignored app, or the configured default;
  4. switches only if the desired input source differs;
  5. verifies once after 100 ms and may retry once.

Delayed work is cancelled and generation-checked, so a retry from an earlier app cannot overwrite the rule for a newer frontmost app. After the one verification, the helper goes idle. A manual layout change inside the same app is respected until that app is activated again.

Safe first run

The first-run configuration is deliberately disabled, contains no application rules, and knows only the standard U.S. layout identifier. A fresh build does not change the keyboard layout until you inspect the generated JSON, add your own identifiers, set "enabled": true, and reload it.

The helper always ignores its own activation, independently of the JSON file.

Requirements

  • macOS 13 or later
  • Xcode Command Line Tools or full Xcode
  • Swift 6 toolchain

No Homebrew packages or external Swift packages are required.

Build and test

make test       # deterministic unit and stress tests
make audit      # static scan for forbidden APIs
make app        # release .app, ad-hoc signed, App Sandbox enabled
make verify     # signature, entitlement, linkage, and string checks
make install    # copy the app to ~/Applications

Launch the locally built app:

open "$HOME/Applications/AppInputSourceSwitcher.app"

The repository is source-only. It does not currently publish prebuilt, notarized, or auto-updating binaries.

Configure

List the enabled keyboard input sources on your Mac:

make diagnostic

Discover exact application bundle identifiers rather than guessing them:

./scripts/discover-app-bundle-ids.sh "Telegram" "Visual Studio Code"

On first launch the app creates:

~/Library/Containers/org.appinputsourceswitcher.AppInputSourceSwitcher/Data/Library/Application Support/AppInputSourceSwitcher/config.json

The Open Configuration Folder menu item is the safest way to find it. Start from config/config.example.json, replace every placeholder with diagnostic output from your Mac, then set "enabled": true. Choose Reload Configuration from the menu.

Configuration validation rejects duplicate bundle IDs, unknown aliases, empty identifiers, unsupported versions, and out-of-range timings. A failed reload never overwrites the file or replaces the last valid in-memory configuration.

Launch at login

The menu's Launch at Login item uses SMAppService.mainApp. It does not write a custom plist to ~/Library/LaunchAgents and has no shell fallback. If macOS reports that approval is required, review the item under System Settings > General > Login Items.

The installed executable also supports:

APP="$HOME/Applications/AppInputSourceSwitcher.app/Contents/MacOS/AppInputSourceSwitcher"
"$APP" --launch-at-login-status
"$APP" --enable-launch-at-login
"$APP" --disable-launch-at-login

Disable launch at login before deleting the app.

Logs

The app uses unified logging. Records contain only bundle identifiers, input-source identifiers, outcomes, and durations:

log show --last 1h \
  --predicate 'subsystem == "org.appinputsourceswitcher.AppInputSourceSwitcher"' \
  --info --debug

See SECURITY.md for the full policy and docs/architecture.md for implementation details.

License

MIT

About

Minimal sandboxed macOS per-app input-source switcher with no permissions, network, updater, or third-party dependencies

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages