ScrollSense is a lightweight macOS daemon that automatically switches the system's Natural Scrolling behavior based on the active input device — mouse or trackpad.
macOS uses a single global setting for natural scrolling. However, many users prefer:
- ✅ Natural scrolling ON for trackpad
- ❌ Natural scrolling OFF for mouse
ScrollSense intelligently detects which device is currently being used and dynamically updates the system preference — giving users the correct scrolling behavior automatically.
No manual toggling. No friction. No System Settings visits.
macOS treats scrolling direction as a global setting:
System Settings → Trackpad / Mouse → Natural Scrolling
But in reality:
- Trackpad scrolling feels natural when enabled
- Mouse wheel scrolling feels inverted when enabled
- Switching between devices requires manual toggling
- This becomes extremely frustrating for developers and power users
There is no native per-device solution.
ScrollSense runs as a background daemon that:
- Listens to low-level input events (scroll wheel) via
CGEventTap - Detects whether the event originated from an external mouse or trackpad
- Compares the detected device with user-defined preferences
- Updates macOS natural scrolling only if needed
- Avoids redundant system calls through internal state tracking
It simulates per-device scroll preferences — even though macOS does not support it natively.
brew tap jspw/scrollsense
brew install scrollsenseUse this when you want to validate the formula from this repo before publishing it to your tap:
brew install --build-from-source ./Formula/scrollsense.rb
brew test scrollsensegit clone https://github.com/jspw/ScrollSense.git
cd ScrollSense
# Run directly from the repo
swift run scrollSense --help
swift run scrollSense set --mouse false --trackpad true
swift run scrollSense run --debuggit clone https://github.com/jspw/ScrollSense.git
cd ScrollSense
swift build -c release
./.build/release/scrollSense --help
./.build/release/scrollSense set --mouse false --trackpad true
./.build/release/scrollSense run --debugOptional local install:
install -m 755 .build/release/scrollSense /usr/local/bin/scrollSenseScrollSense requires Accessibility permission to monitor input events:
- Open System Settings → Privacy & Security → Accessibility
- Add your terminal app (e.g., Terminal, iTerm2) or the
scrollSensebinary - Toggle the permission ON
This is required only once during setup.
If you are running from the repository without installing the binary, replace scrollSense with swift run scrollSense.
Define your preferred scroll behavior per device:
scrollSense set --mouse false --trackpad trueThis saves to ~/.scrollsense.json:
{
"mouseNatural" : false,
"trackpadNatural" : true
}scrollSense run --debugPrints real-time debug output:
[scrollSense] scrollSense daemon starting...
[scrollSense DEBUG 2024-01-15T10:30:00Z] Initial system natural scroll: true
[scrollSense DEBUG 2024-01-15T10:30:00Z] Config: mouse=false, trackpad=true
[scrollSense] scrollSense daemon running. Listening for scroll events...
[scrollSense] Debug mode enabled. Press Ctrl+C to stop.
[scrollSense DEBUG 2024-01-15T10:30:05Z] Device switch: none → mouse
[scrollSense DEBUG 2024-01-15T10:30:05Z] Applying scroll change: natural=false (for Mouse)
scrollSense startThis launches the daemon as a background process and tracks it via a PID file (/tmp/scrollsense.pid).
scrollSense stopSends SIGTERM to the running daemon and cleans up the PID file.
For manual/interactive use without backgrounding:
scrollSense runOr install as a LaunchAgent for auto-start at login:
scrollSense installscrollSense statusOutput:
scrollSense Status
──────────────────────────────────
Daemon: Running (PID: 12345)
Mouse natural scroll: OFF
Trackpad natural scroll: ON
System natural scroll: OFF
Config file: /Users/you/.scrollsense.json
LaunchAgent installed: No
──────────────────────────────────
scrollSense installTo specify a custom binary path:
scrollSense install --path /usr/local/bin/scrollSensescrollSense uninstallScrollSense is built natively using Swift and macOS system frameworks.
Sources/
├── ScrollSense/ # Core library (ScrollSenseCore)
│ ├── Models.swift # InputDevice, ScrollPreferences, DaemonState
│ ├── ConfigManager.swift # Preferences storage (~/.scrollsense.json)
│ ├── DeviceDetector.swift # CGEvent-based device detection
│ ├── ScrollController.swift # System scroll setting read/write
│ ├── StateManager.swift # Runtime state & optimization
│ ├── ScrollDaemon.swift # Main event loop & switching logic
│ ├── PIDManager.swift # PID file tracking for daemon state
│ ├── LaunchAgentManager.swift # LaunchAgent install/uninstall
│ ├── Logger.swift # Logging utility
│ └── ScrollSense.swift # CLI command definitions
├── ScrollSenseApp/
│ └── main.swift # Executable entry point
Tests/
└── ScrollSenseTests/
└── ScrollSenseTests.swift # Unit tests (Swift Testing)
| Module | Responsibility |
|---|---|
| Models | Data types: InputDevice, ScrollPreferences, DaemonState |
| ConfigManager | Load/save preferences from ~/.scrollsense.json |
| DeviceDetector | Detect mouse vs trackpad from CGEvent fields |
| ScrollController | Read/write macOS com.apple.swipescrolldirection via CoreFoundation CFPreferences API |
| StateManager | Track runtime state, optimize by avoiding redundant writes |
| ScrollDaemon | Main event tap loop, orchestrates detection → comparison → update |
| PIDManager | PID file tracking (/tmp/scrollsense.pid) for daemon lifecycle |
| LaunchAgentManager | Install/uninstall macOS LaunchAgent for auto-start |
| Logger | Structured logging with debug/info/warning/error levels |
On daemon start:
→ Load config
→ Read current system scroll state
→ Wait for first input event
On scroll event:
If desired_setting == last_applied_setting
→ Do nothing (skip)
Else
→ Update macOS scroll direction
→ Record new applied state
On device switch:
→ Log the switch (debug mode)
→ Evaluate if scroll change is needed
This ensures:
- ✅ No repeated system writes
- ✅ No unnecessary preference API calls
- ✅ Setting changes dispatched asynchronously — zero scroll lag
- ✅ No system-wide side effects (no process kills)
ScrollSense uses the CGEvent field .scrollWheelEventIsContinuous to distinguish devices:
| Value | Device | Description |
|---|---|---|
1 |
Trackpad | Continuous/momentum scrolling |
0 |
Mouse | Discrete scroll wheel steps |
Additional fields available for debug inspection:
scrollWheelEventMomentumPhasescrollWheelEventScrollPhasescrollWheelEventDeltaAxis1(vertical)scrollWheelEventDeltaAxis2(horizontal)
User preference: Mouse → Natural OFF, Trackpad → Natural ON
| Step | Action | Result |
|---|---|---|
| 1 | User scrolling with trackpad | Natural ON (already set) |
| 2 | User grabs mouse, scrolls | ScrollSense detects mouse → Natural OFF applied |
| 3 | User continues using mouse | No checks performed (optimized) |
| 4 | User touches trackpad | Device switch detected → Natural ON applied |
Seamless. Invisible. Instant.
| Component | Technology |
|---|---|
| Language | Swift 5.9+ |
| Event Monitoring | CoreGraphics (CGEventTap) |
| System Preferences | CoreFoundation CFPreferences API (com.apple.swipescrolldirection) |
| CLI Framework | Swift Argument Parser |
| Build System | Swift Package Manager |
| Auto-Start | macOS LaunchAgent |
| Testing | Swift Testing framework |
No Electron. No UI frameworks. Pure native macOS.
| Command | Description |
|---|---|
scrollSense start |
Start daemon in the background |
scrollSense stop |
Stop the running daemon |
scrollSense run |
Run daemon in the foreground |
scrollSense run --debug |
Run daemon with verbose debug logging |
scrollSense set --mouse <bool> --trackpad <bool> |
Set per-device preferences |
scrollSense status |
Show current preferences, daemon state, and system state |
scrollSense install |
Install LaunchAgent for auto-start at login |
scrollSense install --path <path> |
Install with custom binary path |
scrollSense uninstall |
Remove LaunchAgent |
scrollSense --version |
Show version |
scrollSense --help |
Show help |
The easiest way is to use the release helper:
./scripts/release-homebrew.sh v1.0.1 --tap-dir ../homebrew-scrollsenseThat script:
- creates and pushes the git tag
- downloads the GitHub release tarball for the tag
- computes the correct
sha256 - updates
Formula/scrollsense.rb - commits and pushes the formula update in this repo
- copies, commits, and pushes the formula into your tap repo if you pass
--tap-dir
Your source repo must be clean before running it, since the script tags the current commit. If you pass --tap-dir, the tap repo must also be clean.
- Create and push a new git tag:
git tag v1.0.1
git push origin v1.0.1- Download the release tarball and compute its SHA-256:
curl -L https://github.com/jspw/ScrollSense/archive/refs/tags/v1.0.1.tar.gz -o /tmp/scrollsense-v1.0.1.tar.gz
shasum -a 256 /tmp/scrollsense-v1.0.1.tar.gz- Update
Formula/scrollsense.rb:
- Set
urlto the new tag tarball - Set
sha256to the checksum fromshasum -a 256 - Update the version assertion in
test doif needed
- Verify the formula locally:
brew audit --strict ./Formula/scrollsense.rb
brew install --build-from-source ./Formula/scrollsense.rb
brew test scrollsense
scrollSense --version./scripts/release-homebrew.sh v1.0.1
./scripts/release-homebrew.sh 1.0.1 --tap-dir ../homebrew-scrollsense
./scripts/release-homebrew.sh v1.0.1 --repo jspw/ScrollSense --tap-dir ../homebrew-scrollsense
./scripts/release-homebrew.sh v1.0.1 --remote origin --tap-dir ../homebrew-scrollsense
./scripts/release-homebrew.sh v1.0.1 --tap-dir ../homebrew-scrollsense --tap-remote originIf you publish through a separate tap repository such as jspw/homebrew-scrollsense:
- Copy the updated
Formula/scrollsense.rbinto the tap repo underFormula/scrollsense.rb - Commit and push the formula change to the tap repo
- Users can then upgrade with:
brew update
brew upgrade scrollsenseIf this repository is your source of truth for the formula, keep Formula/scrollsense.rb updated here first and mirror the same file into the tap repo you publish from.
Preferences are stored in ~/.scrollsense.json:
{
"mouseNatural" : false,
"trackpadNatural" : true
}The daemon reloads this file every 2 seconds, so changes made via scrollSense set are picked up automatically without restarting.
- macOS has only one global natural scroll setting
- ScrollSense dynamically switches it — it cannot set per-device simultaneously
- Requires Accessibility permission
- Requires macOS 12.0 (Monterey) or later
- Menu bar app
- Device-specific sensitivity profiles
- GUI preference panel
- Homebrew distribution
- Notarized binary
- Strict mode (periodic system preference verification)
- Per-device custom scroll speed
- Scroll usage statistics
- Developers
- Designers
- MacBook users with external mouse
- Power users
- Anyone switching between devices daily
ScrollSense aims to feel like a native macOS behavior enhancement.
Invisible. Instant. Reliable.
It removes friction from daily workflow by intelligently adapting to the user's current input device.
MIT
ScrollSense is a native macOS daemon that automatically switches scroll direction based on whether you're using a mouse or trackpad.