Skip to content

jspw/ScrollSense

Repository files navigation

ScrollSense

Intelligent Natural Scroll Switching for macOS


Overview

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.


The Problem

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.


The Solution

ScrollSense runs as a background daemon that:

  1. Listens to low-level input events (scroll wheel) via CGEventTap
  2. Detects whether the event originated from an external mouse or trackpad
  3. Compares the detected device with user-defined preferences
  4. Updates macOS natural scrolling only if needed
  5. Avoids redundant system calls through internal state tracking

It simulates per-device scroll preferences — even though macOS does not support it natively.


Installation

Build from Source

# 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/

Permissions Required

ScrollSense requires Accessibility permission to monitor input events:

  1. Open System Settings → Privacy & Security → Accessibility
  2. Add your terminal app (e.g., Terminal, iTerm2) or the scrollSense binary
  3. Toggle the permission ON

This is required only once during setup.


Usage

Set Preferences

Define your preferred scroll behavior per device:

scrollSense set --mouse false --trackpad true

This saves to ~/.scrollsense.json:

{
  "mouseNatural" : false,
  "trackpadNatural" : true
}

Start Daemon (Foreground / Debug Mode)

scrollSense run --debug

Prints 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)

Start Daemon (Background)

scrollSense start

This launches the daemon as a background process and tracks it via a PID file (/tmp/scrollsense.pid).

Stop Daemon

scrollSense stop

Sends SIGTERM to the running daemon and cleans up the PID file.

Run Daemon (Foreground)

For manual/interactive use without backgrounding:

scrollSense run

Or install as a LaunchAgent for auto-start at login:

scrollSense install

Check Status

scrollSense status

Output:

  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
  ──────────────────────────────────

Install LaunchAgent (Auto-Start at Login)

scrollSense install

To specify a custom binary path:

scrollSense install --path /usr/local/bin/scrollSense

Uninstall LaunchAgent

scrollSense uninstall

Architecture

ScrollSense is built natively using Swift and macOS system frameworks.

Project Structure

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 Responsibilities

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

Optimized Runtime Logic

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)

Device Detection

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:

  • scrollWheelEventMomentumPhase
  • scrollWheelEventScrollPhase
  • scrollWheelEventDeltaAxis1 (vertical)
  • scrollWheelEventDeltaAxis2 (horizontal)

Example Scenario

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.


Technical Stack

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 Reference

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

Configuration

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.


Limitations

  • 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

Future Enhancements

  • 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

Target Users

  • Developers
  • Designers
  • MacBook users with external mouse
  • Power users
  • Anyone switching between devices daily

Vision

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.


License

MIT


One-Line Pitch

ScrollSense is a native macOS daemon that automatically switches scroll direction based on whether you're using a mouse or trackpad.

About

ScrollSense is a native macOS daemon that silently detects whether you're using a mouse or trackpad and automatically applies your preferred scroll direction — no manual toggling, no lag, no system interference.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages