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.
# Clone the repository
git clone https://github.com/your-username/ScrollSense.git
cd ScrollSense
# Build release binary
swift build -c release
# Copy to /usr/local/bin (optional)
cp .build/release/scrollSense /usr/local/bin/ScrollSense 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.
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 |
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.