Imported from vovarevenko/mouse-hater (
AGENTS.md). Install upstream withnpx skills add vovarevenko/mouse-hater. Copyright stays with the author.
Mouse Hater
macOS menu-bar agent (LSUIElement) for keyboard-driven mouse clicking: a SwiftPM
executable assembled into a .app by build.sh. Flow: HotkeyMonitor (a
CGEventTap) detects the Command-tap trigger and feeds keys to OverlayController
(selection state machine + geometry), which draws via OverlayView and clicks via
Clicker. Pure trigger recognition and grid subdivision live in
MouseHaterCore and are covered by unit tests.
Commands
swift build— compile check.swift test— run the pure core state-machine and geometry tests../build.sh [debug|release]— build, assemble, and signbuild/Mouse Hater.app(release by default), lintResources/Info.plist, and verify the signature;openit to run.SANDBOX=1 ./build.sh release— build the same app withResources/MouseHater.entitlementsfor Mac App Store sandbox smoke testing.SANDBOX=1 ./build.sh release— build an Apple Silicon (arm64) release bundle for smoke testing. Intel (x86_64) release builds are intentionally unsupported../scripts/package-app-store.sh— build a signed Mac App Store.pkg; needsAPP_SIGN_IDENTITY,INSTALLER_SIGN_IDENTITY,TEAM_ID, andPROVISIONING_PROFILEin the environment.
Gotchas
- Coordinates: all geometry is in global display coordinates (top-left origin,
points). The click point goes straight to
Clicker; only the rect handed to the flippedOverlayViewis offset by the display origin. Don't introduce AppKit's bottom-left coordinates. - Key matching is by virtual keycode (physical position), not character — keep it keyboard-layout-independent. Overlay labels are QWERTY position labels, not localized output characters.
- The
CGEventTaplives on the main run loop, must be re-enabled on.tapDisabledByTimeout/.tapDisabledByUserInput, and a keyDown swallowed while the overlay is active must also swallow its keyUp. - Swift 6 language mode is enabled in
Package.swift. UI, timers, and the event-tap bridge are intentionally main-actor-bound; don't remove those annotations unless the concurrency model is reworked. - Names: the display name has a space (
Mouse Hater); the executable, bundle, and SwiftPM target don't (MouseHater) — don't unify them or the build breaks. - Signing/Accessibility: a stable signing identity lives in untracked
.signing.local; an ad-hoc build, or changing the bundle id, means re-granting Accessibility. - Privacy manifest: keep
Resources/PrivacyInfo.xcprivacyin the bundle. The app currently declares no data collection/tracking and a UserDefaults required-reason API entry for local preferences. - Login item: the app must never auto-register as Open at Login. First-run
setup may offer it, but
SMAppService.mainApp.register()should only run after an explicit user action in setup or the menu.