From 70a1c17e034a260c96cb55c97b71f10eaa0564fe Mon Sep 17 00:00:00 2001 From: jsahn Date: Mon, 28 Sep 2026 16:52:18 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=ED=9C=A0=20?= =?UTF-8?q?=EC=84=B8=EB=A1=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=B0=A9?= =?UTF-8?q?=ED=96=A5=20=EB=B0=98=EC=A0=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스크롤 이벤트를 가로채 제스처 phase가 없는 휠 이벤트만 세로 방향을 뒤집은 새 이벤트로 바꿔 보낸다. 원래 이벤트의 delta 필드만 수정하면 앱에 반영되지 않아 대체 이벤트 방식을 사용한다. 트랙패드와 가로 스크롤은 그대로 둔다. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 ++ razer-tilt.swift | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/README.md b/README.md index 7983b7a..393a69d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Razer Pro Click V2 (vendor 5426, product 209)의 휠 좌우 틸트를 각각 `Command+[` / `Command+]`로 보냅니다. 제공한 HID 보고서의 첫 바이트와 비트 마스크를 그대로 사용합니다. +마우스 휠의 세로 스크롤 방향도 반전합니다. 트랙패드와 Magic Mouse처럼 제스처로 스크롤하는 장치와 가로 스크롤은 바꾸지 않습니다. + ## 설치 / 업데이트 ```sh diff --git a/razer-tilt.swift b/razer-tilt.swift index f822713..8744216 100644 --- a/razer-tilt.swift +++ b/razer-tilt.swift @@ -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()