ther0n/UnnaturalScrollWheels
Invert scroll direction for physical scroll wheels while maintaining "Natural" scrolling for trackpads on MacOS
Mixed signals — read the receipts
worst of 4 axescopyleft license (GPL-3.0) — review compatibility; no tests detected
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
No critical CVEs, sane security posture — runnable as-is.
- ✓Last commit 5w ago
- ✓11 active contributors
- ✓GPL-3.0 licensed
Show 4 more →Show less
- ✓CI configured
- ⚠Concentrated ownership — top contributor handles 53% of recent commits
- ⚠GPL-3.0 is copyleft — check downstream compatibility
- ⚠No test directory detected
What would change the summary?
- →Use as dependency Concerns → Mixed if: relicense under MIT/Apache-2.0 (rare for established libs)
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Forkable" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/ther0n/unnaturalscrollwheels)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/ther0n/unnaturalscrollwheels on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: ther0n/UnnaturalScrollWheels
Generated by RepoPilot · 2026-05-10 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/ther0n/UnnaturalScrollWheels shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
WAIT — Mixed signals — read the receipts
- Last commit 5w ago
- 11 active contributors
- GPL-3.0 licensed
- CI configured
- ⚠ Concentrated ownership — top contributor handles 53% of recent commits
- ⚠ GPL-3.0 is copyleft — check downstream compatibility
- ⚠ No test directory detected
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live ther0n/UnnaturalScrollWheels
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/ther0n/UnnaturalScrollWheels.
What it runs against: a local clone of ther0n/UnnaturalScrollWheels — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in ther0n/UnnaturalScrollWheels | Confirms the artifact applies here, not a fork |
| 2 | License is still GPL-3.0 | Catches relicense before you depend on it |
| 3 | Default branch main exists | Catches branch renames |
| 4 | Last commit ≤ 67 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of ther0n/UnnaturalScrollWheels. If you don't
# have one yet, run these first:
#
# git clone https://github.com/ther0n/UnnaturalScrollWheels.git
# cd UnnaturalScrollWheels
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of ther0n/UnnaturalScrollWheels and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "ther0n/UnnaturalScrollWheels(\\.git)?\\b" \\
&& ok "origin remote is ther0n/UnnaturalScrollWheels" \\
|| miss "origin remote is not ther0n/UnnaturalScrollWheels (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(GPL-3\\.0)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"GPL-3\\.0\"" package.json 2>/dev/null) \\
&& ok "license is GPL-3.0" \\
|| miss "license drift — was GPL-3.0 at generation time"
# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
&& ok "default branch main exists" \\
|| miss "default branch main no longer exists"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 67 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~37d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/ther0n/UnnaturalScrollWheels"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
UnnaturalScrollWheels is a macOS menu bar application that inverts scroll direction for physical mouse wheels while preserving "natural" (inverted) scrolling for trackpads. It solves the macOS design flaw where toggling "Scroll direction: Natural" in Mouse settings unintentionally also changes Trackpad settings, by intercepting scroll events at the system level via the Accessibility API and selectively inverting only wheel-based input. Simple single-app architecture: main app in UnnaturalScrollWheels/ contains AppDelegate.swift (lifecycle), ScrollInterceptor.swift (core scroll event interception), MenuBarItem.swift (UI), and Options.swift (preferences storage). Companion AutoLauncher/ is a lightweight helper app for login-time autostart. UI built with Storyboard (Base.lproj/Main.storyboard) and preferences stored via AppDelegate.
👥Who it's for
Mac users who prefer traditional scroll wheel behavior (scroll up = move up) but also use trackpads with natural scrolling (scroll up = scroll content up). Specifically targets users frustrated by macOS's linked Mouse/Trackpad scroll settings that lack per-device granularity.
🌱Maturity & risk
Production-ready and actively maintained. The project has a clean Homebrew integration, CI/CD workflows (ci.yml, release.yml), professional app distribution via DMG releases, and demonstrates solid engineering practices. Single maintainer (ther0n) with recent activity, though open issue visibility from the file list is unclear.
Low risk for a mature utility app, but single-maintainer dependency and reliance on macOS Accessibility API (which Apple has tightened) are considerations. The Objective-C/Swift mix (192 lines Objective-C vs 20k Swift) is minimal and well-contained. No external package manager dependencies evident; primary risk is macOS API breakage on future OS versions.
Active areas of work
Active maintenance evidenced by GitHub workflows (ci.yml for testing, release.yml for distribution) and Homebrew cask integration. Specific PR/issue data unavailable from file list, but the presence of .github/workflows suggests ongoing CI integration and automated releases.
🚀Get running
- Clone:
git clone https://github.com/ther0n/UnnaturalScrollWheels.git - Open Xcode project:
open UnnaturalScrollWheels.xcodeproj - Build: Cmd+B (builds against macOS SDK included in Xcode)
- Run: Cmd+R or manually launch the .app from DerivedData. Requires Accessibility permissions via System Preferences → Security & Privacy → Accessibility.
Daily commands: Open UnnaturalScrollWheels.xcodeproj in Xcode. Cmd+B to build. Cmd+R to run. App launches to menu bar; click icon to show PreferencesViewController window. Requires granting Accessibility permissions in System Preferences.
🗺️Map of the codebase
- UnnaturalScrollWheels/ScrollInterceptor.swift: Core engine that intercepts CGEvent scroll events, detects device type (wheel vs trackpad), and inverts delta values only for wheels
- UnnaturalScrollWheels/AppDelegate.swift: Application lifecycle, menu bar setup, and ScrollInterceptor initialization; critical for launch and event loop registration
- UnnaturalScrollWheels/PreferencesViewController.swift: Settings UI for toggling inversion, disabling mouse acceleration, and configuring per-app behavior
- UnnaturalScrollWheels/Options.swift: UserDefaults wrapper for persisting user preferences across app sessions
- UnnaturalScrollWheels/MenuBarItem.swift: Creates and manages the menu bar icon and dropdown menu for quick access
- AutoLauncher/AppDelegate.swift: Helper app that launches the main app at login without user interaction
- UnnaturalScrollWheels/Base.lproj/Main.storyboard: UI definition for PreferencesViewController window and controls
- .github/workflows/ci.yml: GitHub Actions CI pipeline for building and testing on PR/push
- .github/workflows/release.yml: Automated release workflow that builds DMG and triggers Homebrew distribution
🛠️How to make changes
For scroll behavior changes: edit ScrollInterceptor.swift (where scroll events are captured and delta values inverted). For UI/settings: modify PreferencesViewController.swift and Base.lproj/Main.storyboard. For autostart logic: edit AutoLauncher/AppDelegate.swift. For menu bar icon: MenuBarItem.swift. Options.swift handles UserDefaults persistence.
🪤Traps & gotchas
ScrollInterceptor must run in the main thread with proper CGEvent tap registration; failure to request Accessibility permissions before event interception causes silent failure. The app hides from Dock by default (LSUIElement key in Info.plist) — opening the app again reveals Preferences; this behavior is non-obvious. AutoLauncher's code signing and sandboxing must match the main app's entitlements or launch-at-login fails. Trackpad detection likely relies on IOKit or specific CGEvent flags that may differ across macOS versions.
💡Concepts to learn
- CGEvent tapping (Quartz Event Services) — UnnaturalScrollWheels intercepts mouse events at the system level via CGEvent before they reach applications; understanding event tap registration, event filtering, and re-injection is core to how the app works
- Accessibility API (AXUIElement) — The app requires Accessibility permissions in System Preferences; this is not a security theater but a requirement for CGEvent access, so understanding macOS trust model is essential
- Input device discrimination (wheel vs trackpad) — The app's core logic detects whether scroll input came from a physical wheel or trackpad to selectively invert only wheels; this likely relies on CGEventGetIntegerValueField flags that differ by device type
- Menu bar applications (NSStatusBar) — UnnaturalScrollWheels lives in the macOS menu bar rather than the Dock; understanding NSStatusBar, NSStatusItem, and LSUIElement plist key is necessary for menu bar app patterns
- Login-time app launching (LaunchAgent) — The AutoLauncher helper enables autostart at login via LaunchAgent plist; understanding the launch services mechanism is key to the autostart feature
- UserDefaults persistence & plist encoding — Options.swift wraps UserDefaults for cross-session preference storage; understanding macOS preferences architecture helps with extending settings
- Sandbox entitlements and code signing — The .entitlements files (AutoLauncher/AutoLauncher.entitlements) define what system capabilities the app can access; understanding entitlements is critical for distribution and Accessibility permission success
🔗Related repos
KeyboardCleanTool/KeyboardCleanTool— Another macOS utility that requires Accessibility permissions to intercept system events and modify input behaviorfwheel/MacMouseFix— Direct alternative that also inverts mouse scroll direction on macOS, useful for comparing implementation approachesHomebrew/homebrew-cask— The distribution channel for UnnaturalScrollWheels; understanding cask submission and maintenance is relevant for releasesapple/swift— The Swift language and runtime powering this app; relevant for understanding AppKit integration and macOS API stabilityrequests/requests— While not related to this project directly, demonstrates mature open-source maintenance practices similar to what UnnaturalScrollWheels exhibits
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add unit tests for ScrollInterceptor.swift event handling logic
ScrollInterceptor.swift appears to be the core logic for inverting scroll wheel events while preserving trackpad behavior. Currently there are no visible test files in the repo. Adding unit tests would catch regressions in scroll event detection and inversion logic, especially critical since this involves low-level event interception that could break across macOS versions.
- [ ] Create UnnaturalScrollWheels/ScrollInterceptor.swift unit test file
- [ ] Add tests for identifying physical scroll wheel events vs trackpad events
- [ ] Add tests for scroll direction inversion logic with various delta values
- [ ] Add tests for edge cases (momentum scrolling, rapid scrolling)
- [ ] Integrate tests into ci.yml workflow to run on PR submissions
Add macOS version compatibility matrix to README.md with tested versions
Since UnnaturalScrollWheels hooks into low-level macOS scroll events, it's highly version-dependent. The README mentions no specific macOS version requirements or tested compatibility. New contributors and users need to know which macOS versions are supported, which is critical for a system-level tool.
- [ ] Add a 'Compatibility' section to README.md below the 'Why' section
- [ ] Document minimum and tested macOS versions (e.g., macOS 10.13+, tested on Ventura)
- [ ] Document known issues or limitations per macOS version if any exist
- [ ] Reference the specific code in ScrollInterceptor.swift that handles macOS API compatibility
Extract magic numbers and hardcoded values from scroll event detection into Options.swift configuration
ScrollInterceptor.swift likely contains hardcoded delta thresholds or constants for distinguishing wheel vs trackpad events. These magic numbers should be configurable constants in Options.swift (which already handles user preferences) to allow tuning without code changes and enable user-customizable sensitivity for different hardware.
- [ ] Audit ScrollInterceptor.swift for hardcoded numeric thresholds or event type constants
- [ ] Move identified magic numbers to Options.swift as configurable properties
- [ ] Add getter/setter methods in Options.swift to persist these values to UserDefaults
- [ ] Add corresponding UI controls in PreferencesViewController.swift if appropriate
- [ ] Update any documentation referencing these values
🌿Good first issues
- Add unit tests for ScrollInterceptor event inversion logic — currently no test files visible in the repository; start with testing delta sign-flip behavior for wheel vs trackpad events
- Document the Accessibility permission flow in README.md with step-by-step screenshots for different macOS versions (Monterey, Ventura, Sonoma) — currently README only shows generic screenshots
- Implement per-app inversion profiles in PreferencesViewController — the Options.swift structure exists but UI is missing for users to whitelist/blacklist specific apps
⭐Top contributors
Click to expand
Top contributors
📝Recent commits
Click to expand
Recent commits
0b6bb3b— CI: Remove Homebrew tap step from release workflow (do4k)3990112— Remove spctl check from pre-notarization verification (it requires notarization to pass) (Dan Oak)460aeb2— Add --timestamp flag and code signing verification to fix notarization timeout (Dan Oak)431406c— Simplify notarization to use notarytool --wait instead of manual polling (Dan Oak)ffa4a07— Increase notarization polling timeout from 60 to 90 minutes (Dan Oak)872f3a4— Increment version to 1.4.0 (ther0n)e9bce51— Move Help and Sponsor buttons into their own stack view, in an attempt to align the buttons nicely (ther0n)e8e4512— Update strings to reflect changes in recent macOS versions (ther0n)4c84121— update CI to use main branch (do4k)626123a— Fix readme screenshots (do4k)
🔒Security observations
The UnnaturalScrollWheels application has a moderate security posture with several areas of concern related to privileged system access. As a macOS utility requiring Accessibility permissions to intercept input, the primary risks involve potential misuse of elevated privileges and secure storage of user preferences. The codebase should focus on: (1) strict validation that only scroll wheel events are processed, (2) proper file permission handling for stored preferences, (3) secure configuration of the auto-launcher component, (4) implementation of code signing validation, and (5) removal of user-specific development files from version control. The absence of external dependencies reduces supply chain risk, but the system-level access required necessitates careful attention to least-privilege principles and input validation.
- High · Accessibility Access Without Proper Validation —
UnnaturalScrollWheels/ScrollInterceptor.swift. The application requires Accessibility permissions to intercept scroll events (as evidenced by ScrollInterceptor.swift and the accessibility screenshots). macOS applications with accessibility access can potentially intercept all user input system-wide. The codebase should validate that only scroll wheel events are captured and other input is not logged or processed. Fix: Implement strict input filtering to ensure only scroll wheel events are processed. Add comprehensive logging and audit trails for accessibility API usage. Consider using more restricted APIs if available. - Medium · User Data Directory Permissions —
UnnaturalScrollWheels/Options.swift. The application stores preferences and options (Options.swift) likely in user-accessible directories. If proper file permissions are not set, configuration files containing user preferences could be readable/writable by other processes. Fix: Ensure all preference files are stored with restrictive permissions (0600). Validate file ownership and permissions before reading. Use macOS Security Framework APIs for secure storage when dealing with sensitive settings. - Medium · Auto-Launcher Helper Privilege Escalation Risk —
AutoLauncher/AppDelegate.swift, AutoLauncher/main.swift, AutoLauncher/AutoLauncher.entitlements. The AutoLauncher component (AutoLauncher/AppDelegate.swift, AutoLauncher/main.swift) runs privileged code at startup. If the launcher mechanism is not properly secured, it could be exploited for privilege escalation or malicious code execution. Fix: Review entitlements in AutoLauncher.entitlements for least privilege. Verify code signing and notarization. Implement integrity checks for the main application before auto-launching. Never load unsigned code in the launcher. - Medium · Missing Code Signing Validation —
UnnaturalScrollWheels/AppDelegate.swift. No evidence of code signing verification in the provided files. The application performs system-level input interception and should validate its own integrity and signature. Fix: Implement code signature validation at runtime. Ensure the application is properly code-signed and notarized by Apple. Add signature verification checks before sensitive operations. - Low · User Data Exposure in Xcodeproj —
.gitignore, UnnaturalScrollWheels.xcodeproj/xcuserdata/. The repository includes user data directory (UnnaturalScrollWheels.xcodeproj/xcuserdata/theron.xcuserdatad) which should typically be gitignored. This may contain sensitive development information. Fix: Add xcuserdata/ to .gitignore. Remove user-specific data from version control history using git filter-branch or BFG Repo-Cleaner. - Low · Missing Input Validation Documentation —
UnnaturalScrollWheels/PreferencesViewController.swift. No visible input validation or sanitization in the file structure for the PreferencesViewController. User preferences should be validated before application. Fix: Document and implement input validation for all user preference inputs. Validate types, ranges, and formats before storing or applying settings.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.