마우스 휠 세로 스크롤 방향 반전 추가

스크롤 이벤트를 가로채 제스처 phase가 없는 휠 이벤트만 세로 방향을 뒤집은
새 이벤트로 바꿔 보낸다. 원래 이벤트의 delta 필드만 수정하면 앱에 반영되지
않아 대체 이벤트 방식을 사용한다. 트랙패드와 가로 스크롤은 그대로 둔다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-09-28 16:52:18 +09:00
co-authored by Claude Opus 5.5
parent 178ef4b2bd
commit 70a1c17e03
2 changed files with 50 additions and 0 deletions
+2
View File
@@ -2,6 +2,8 @@
Razer Pro Click V2 (vendor 5426, product 209)의 휠 좌우 틸트를 각각 `Command+[` / `Command+]`로 보냅니다. 제공한 HID 보고서의 첫 바이트와 비트 마스크를 그대로 사용합니다.
마우스 휠의 세로 스크롤 방향도 반전합니다. 트랙패드와 Magic Mouse처럼 제스처로 스크롤하는 장치와 가로 스크롤은 바꾸지 않습니다.
## 설치 / 업데이트
```sh
+48
View File
@@ -147,4 +147,52 @@ let accessTimer: Timer? = accessGranted() ? nil : {
}
}
}()
// Invert vertical mouse-wheel scrolling. Trackpads and Magic Mouse send gesture phases;
// wheels do not, even when their high-resolution scrolling is marked continuous.
var scrollTap: CFMachPort?
var lastScrollKind = ""
// Marks the replacement events this app posts so the tap lets them through untouched.
let invertedScrollMarker: Int64 = 0x52544D31
let scrollCallback: CGEventTapCallBack = { proxy, type, event, _ in
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
if let tap = scrollTap { CGEvent.tapEnable(tap: tap, enable: true) }
return Unmanaged.passUnretained(event)
}
guard type == .scrollWheel,
event.getIntegerValueField(.eventSourceUserData) != invertedScrollMarker
else { return Unmanaged.passUnretained(event) }
let phase = event.getIntegerValueField(.scrollWheelEventScrollPhase)
let momentum = event.getIntegerValueField(.scrollWheelEventMomentumPhase)
let isWheel = phase == 0 && momentum == 0
let kind = "continuous=\(event.getIntegerValueField(.scrollWheelEventIsContinuous)) wheel=\(isWheel)"
if kind != lastScrollKind {
log("Scroll events: \(kind)")
lastScrollKind = kind
}
let pointY = event.getIntegerValueField(.scrollWheelEventPointDeltaAxis1)
guard isWheel, pointY != 0 else { return Unmanaged.passUnretained(event) }
// Apps read the hardware data attached to the original event, so editing its delta
// fields has no visible effect. Post a fresh event with the vertical delta flipped.
let pointX = event.getIntegerValueField(.scrollWheelEventPointDeltaAxis2)
guard let replacement = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2,
wheel1: Int32(clamping: -pointY), wheel2: Int32(clamping: pointX), wheel3: 0)
else { return Unmanaged.passUnretained(event) }
replacement.location = event.location
replacement.flags = event.flags
replacement.setIntegerValueField(.eventSourceUserData, value: invertedScrollMarker)
replacement.tapPostEvent(proxy)
return nil
}
if accessGranted() {
scrollTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap,
eventsOfInterest: CGEventMask(1 << CGEventType.scrollWheel.rawValue),
callback: scrollCallback, userInfo: nil)
if let tap = scrollTap {
CFRunLoopAddSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0), .commonModes)
log("Mouse wheel inversion running")
} else {
log("Mouse wheel inversion failed: could not create event tap")
}
}
CFRunLoopRun()