Skip to content
t-millotPublic

About

Better control for external mice on macOS: smooth scrolling, click-and-drag trackpad gestures, and actions for extra buttons. Swift port of the Mac Mouse Fix engine.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Leios

Smooth scrolling on macOS

CI

From λεῖος (leîos), ancient Greek for smooth — said of a polished surface, of level ground, of anything without a catch in it. Its opposite is τραχύς (trakhús), rough or jagged. That contrast is the whole idea: a mouse wheel turns in notches, and the engine's job is to take the notches out.

Better control for external mice: smooth scrolling, click-and-drag trackpad gestures (scroll / navigate, Spaces / Mission Control), and click actions for extra buttons. Scrolling can be tuned per application, and the settings you don't change keep following the global ones. Turn on Sync settings with iCloud in General and your scrolling, buttons and per-app profiles follow your Apple Account to your other Macs; the kill switches and the menu bar item stay on the Mac you set them on.

Statistics counts what the mouse has actually done — distance scrolled, wheel ticks, clicks by button, drag gestures, and every action the engine fired — over a week, a month, a year or the lifetime of the install. Counting costs nothing on the input path: the engine adds to a few integers and hands them to a background thread once a minute. It can be switched off, the history reset, and the figures from your other Macs added in or left out.

Leios is a Swift port of the input engine of Mac Mouse Fix by Noah Nuebling. The engine (Packages/LeiosKit/Sources/LeiosEngine) is derived from the MMF source and is used under the MMF License. Leios is not sold.

Installing

Requires macOS 27 or later on Apple silicon.

  1. Download Leios-<version>.dmg from Releases and drag Leios to Applications.
  2. Open it and turn on Enable.
  3. Turn on LeiosHelper in System Settings → Privacy & Security → Device Control and Data Access (called Accessibility before macOS 27; the TCC service is still kTCCServiceAccessibility). The engine starts as soon as permission is granted.

The app and its embedded helper are both signed with a Developer ID and notarized, so there is no Gatekeeper warning to click through. Neither is sandboxed: the engine owns CGEventTaps, which the App Sandbox forbids.

Leios checks for updates on its own and offers them; it never installs one without being asked. General → Updates has the switch for that, a Check Now button, and Include pre-release versions for anyone who wants the betas. Both settings stay on the Mac they were set on. Versions before 1.0 beta 6 have no updater, so they need one manual download to reach it.

Layout

  • Leios/ — the SwiftUI settings app. Writes ~/Library/Application Support/Leios/config.json, enables and disables the helper through SMAppService, shows its status, and draws the usage statistics the helper records to Statistics/statistics.json beside it.
  • LeiosHelper/ — a background login item (LSUIElement) launched by launchd. It owns every CGEventTap and runs the engine. Needs Device Control and Data Access.
  • Packages/LeiosKit/ — local Swift package:
    • LeiosShared — config model, constants, XPC protocol.
    • LeiosEngine — the Engine: scrolling (Scroll/), buttons (Buttons/), drag gestures (Drag/), gesture synthesis (Touch/), one-shot actions (Actions/), math and curves (Math/).
    • CPrivateShim — C declarations for the private-but-exported macOS APIs the engine needs.
  • SupportFiles/ — launchd agent plist and entitlements.

Building

Open Leios.xcodeproj in Xcode 27, set your Development Team on both targets, and run the Leios scheme; the helper is built automatically and embedded at Leios.app/Contents/Library/LoginItems/LeiosHelper.app. Then enable it and grant the permission as above. A stable signing identity keeps that permission across rebuilds; after changing the bundle identifier or switching identities, clear the stale grant with tccutil reset Accessibility com.tmillot.Leios.Helper.

xcodebuild -project Leios.xcodeproj -scheme Leios -configuration Debug build
xcodebuild test -workspace Leios.xcworkspace -scheme Leios -destination 'platform=macOS'
cd Packages/LeiosKit && swift test

The app target carries an iCloud entitlement for the iCloud.com.tmillot.Leios CloudKit container, which is restricted and so needs a provisioning profile. In Xcode that means the iCloud capability with CloudKit ticked on the Leios target. To build without provisioning it — which is what CI does, signing ad-hoc — swap in the entitlements that leave iCloud out:

xcodebuild -project Leios.xcodeproj -scheme Leios -configuration Debug build \
    LEIOS_ENTITLEMENTS=SupportFiles/Leios-CI.entitlements

The app then reports sync as unavailable instead of offering it. Only the app target is affected; the helper has no iCloud entitlement, so it never needs re-provisioning.

Tests live in three targets: LeiosTests (app-hosted — capture rules and the embedded-helper bundle layout), plus the package's LeiosEngineTests and LeiosSharedTests. Run them through Leios.xcworkspace, not the project: Xcode leaves a local package's test targets out of a plain project scheme, so xcodebuild test -project … silently runs only LeiosTests. Everything else works from Leios.xcodeproj.

Lint with swiftlint lint --strict and swiftformat --lint (brew install swiftlint swiftformat). Both report zero, so anything they flag is new; rules the Mac Mouse Fix port does not fit are relaxed in Packages/LeiosKit/.swiftlint.yml rather than worked around in the source.

The app accepts --enable-helper / --disable-helper launch arguments for scripted testing.

Releasing

Scripts/release.sh archives Release, exports with the Developer ID identity, notarizes and staples both the app and the disk image, then re-checks the result with spctl the way a recipient's Mac will. Stapling the app too means a copy dragged to /Applications carries its own ticket and validates offline; the embedded helper is checked separately, since launchd loads it rather than the user opening it. It needs a Developer ID Application certificate in the login keychain (Xcode → Settings → Accounts → Manage Certificates) and notarization credentials under a keychain profile:

xcrun notarytool store-credentials leios-notary --apple-id <apple-id> --team-id WE9Q98XU4V
./Scripts/release.sh --publish

Version, tag and image name come from MARKETING_VERSION — bump it on every configuration first. Bump CURRENT_PROJECT_VERSION too: Sparkle orders updates by it, so it is a build counter that only ever rises (1.0 final is build 6, not build 1). The script refuses to publish a build the appcast already offers.

--publish also uploads a ZIP of the stapled app — what Sparkle downloads, next to the DMG people download by hand — and adds the release to appcast.xml on the gh-pages branch, tagged as a beta channel item when the version has more than one word. That needs Sparkle's EdDSA key; the script's header has the one-time setup, and its preflight refuses to go near a build without it. Sparkle's tools need no install — they ship inside the package the project already depends on, so they always match the framework the app links, and Scripts/release.sh --sparkle-bin prints where they are. Without --publish the script stops at the built image, creating no tag, no release and no appcast entry.

Architecture notes

  • One dedicated engine thread hosts every event tap, timer and display link, so the engine is single-threaded.
  • Config changes flow app → config.json → helper (file watcher + XPC reloadConfig).
  • Tap lifetimes are decided by SwitchMaster: unused input paths cost nothing.
  • Usage statistics are counted on the engine thread with plain integer adds and flushed to disk from a background queue; the file is written by the helper and only read by the app.
  • Adding a button captures a press: the app arms the helper over XPC while the pointer is in the capture zone, and the helper swallows the next press and reports its number — which is what makes a button already consuming its own press capturable.

About

Better control for external mice on macOS: smooth scrolling, click-and-drag trackpad gestures, and actions for extra buttons. Swift port of the Mac Mouse Fix engine.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages