macoc/README.md
root 1e50fae6c8
Some checks failed
macoc CI (Docker) / swift-core (push) Failing after 3s
ci: run Swift core tests on available Docker runner
2026-10-26 10:04:29 +03:00

5.5 KiB

macoc

Native macOS menu-bar layout switcher for macOS Sonoma (14+) without the stock Caps Lock delay. Plain Caps switches to the next configured input source on release, including during fast typing. Option+Caps in either order toggles the real system Caps Lock state.

The icon is a SFW cartoon of someone thoroughly fed up with keyboard layout switching, complete with a literally hot seat.

Architecture

Caps Lock is remapped to F19 via in-process Apple IOKit APIs, then suppressed by a CoreGraphics event tap. Layouts switch through Carbon TIS. No drivers, root daemons, third-party input libraries or hidutil permission required.

Apple technical note: https://developer.apple.com/library/archive/technotes/tn2450/

Build and install

Requires Apple's Command Line Tools (not the Xcode IDE). Task is optional.

Quit any running macoc or legacy Capswitch before rebuilding. From this repository:

git pull origin main
bash scripts/test.sh
bash scripts/install.sh

With Task installed:

task test
task install

Installation creates a signed macoc.app with the .icns icon in the stable user-owned location ~/Applications/macoc.app, then starts it through LaunchServices. No sudo is needed.

For development without installing: bash scripts/bundle.sh or task bundle.

The menu offers Restart macoc, Install to ~/Applications… and Launch at Login, registered via Apple's SMAppService.mainApp. If macOS requests approval, open System Settings -> General -> Login Items.

Migrating from Capswitch

The name and executable are now macoc; the existing bundle identifier remains dev.local.Capswitch to preserve old Accessibility grants and HID ownership data where macOS permits. Do not run the old Capswitch and macoc simultaneously. If the old ~/Applications/Capswitch.app is installed, quit it and disable its Login Item before installing macoc. Then you may delete the old app bundle.

Code signing and repeat Accessibility prompts

Restart now uses LaunchServices (open -n -a) instead of executing a raw binary under Contents/MacOS. That preserves the macOS .app attribution.

For identical app binaries at the same path, Accessibility approval normally persists through restarts. But ad-hoc signing does not provide a stable designated requirement across code changes: each rebuild may require approval again.

For stable identity across rebuilt versions, create one persistent local Code Signing certificate in Keychain Access named macoc Local Signing (Certificate Assistant -> Create a Certificate -> Self Signed Root -> Code Signing). Run bash scripts/setup-signing.sh or task signing-setup for the instructions. The build auto-detects that identity, or accepts MACOC_CODESIGN_IDENTITY for an existing signing certificate. It falls back to ad-hoc signing with an explicit warning otherwise.

Moving to the installed path or changing the signer can still require one new approval. Keep the signing identity and app path stable.

Reference: https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements

macoc cannot silently grant itself TCC permissions. macOS still asks the user to allow Accessibility (and optionally Input Monitoring) for the app, not for a system binary.

Disable macOS's own "Use Caps Lock to switch to/from ABC" under System Settings -> Keyboard -> Text Input -> Edit. Keep physical Caps assigned its normal modifier role rather than No Action.

Taskfile commands and shell equivalents

Taskfile No Task required
task lint bash scripts/lint.sh
task test bash scripts/test.sh
task verify bash scripts/verify.sh
task build bash scripts/build.sh
task icon bash scripts/icon.sh
task sign bash scripts/sign.sh
task bundle bash scripts/bundle.sh
task install bash scripts/install.sh
task doctor bash scripts/doctor.sh
task clean bash scripts/clean.sh
task package bash scripts/package.sh
task publish bash scripts/publish-release.sh
task signing-setup bash scripts/setup-signing.sh

Forgejo CI and releases

CI workflow: .forgejo/workflows/ci.yml. It uses the existing Docker runner and the official Linux Swift image. Pushes to main run shell validation and actual XCTest for pure Swift core logic. On version tags it also checks the tag against the application version.

Linux Docker cannot compile macOS .app binaries because Apple AppKit, IOKit and macOS SDK are unavailable there. Docker CI does not publish nonexistent macOS binaries.

For a release archive, check out the tag on a real Mac and run bash scripts/package.sh (or task package), then execute bash scripts/publish-release.sh with a repository-scoped Forgejo token. See CI and release instructions. Fully automated macOS binary releases require a real Mac runner or another Mac build service.

Ad-hoc artifacts are suited to local evaluation; public distribution requires Developer ID signing and Apple notarization.

Diagnostics and recovery

The status-bar menu shows layout, keyboard HID mapping count, event-tap state, permission grants, and F19 down/up counts (no typed characters are recorded). Run bash scripts/doctor.sh for read-only native HID diagnostics.

Pause or Quit normally restores Caps and preserves unrelated remappings. If the process is killed during a remap, restart and Quit normally; macOS usually resets mappings when a keyboard service disappears or after reboot.

The Linux development VM verifies shell syntax and static source checks, not macOS compilation or live TCC behavior.