Compare commits

...
4 Commits
Author SHA1 Message Date
jsahnandClaude Opus 5.5 1af177f30d 틸트할 때 함께 들어오는 클릭 막기
틸트 비트는 macOS에 4~7번 버튼으로도 전달되어 앱이 휠 클릭처럼 처리하거나,
휠을 옆으로 밀 때 휠 버튼이 같이 눌리는 경우가 있었다. 이벤트 탭에서 4~7번
버튼과 틸트 직후 0.15초 안의 휠 클릭을 버리고, 버린 버튼의 드래그·떼기도
함께 버린다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 12:23:33 +09:00
jsahnandClaude Opus 5.5 d840bfd842 스크롤 이벤트 진단 로그 제거
휠 반전 문제를 찾으려고 넣었던 스크롤 종류 로그를 뺀다. 스크롤 경로에서
파일 쓰기가 없어진다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:46:20 +09:00
jsahnandClaude Opus 5.5 606b617d19 휠 반전 대체 이벤트를 줄 단위로 바꿔 원래 스크롤 속도 유지
픽셀 단위 대체 이벤트는 앱이 트랙패드처럼 처리해 한 칸당 이동 거리가 줄었다.
줄 단위로 만들고 원래 픽셀·소수점 이동량을 반전해 그대로 넣는다. 픽셀 이동량이
0인 휠 이벤트가 반전 없이 지나가던 문제도 고친다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:56:58 +09:00
jsahnandClaude Opus 5.5 70a1c17e03 마우스 휠 세로 스크롤 방향 반전 추가
스크롤 이벤트를 가로채 제스처 phase가 없는 휠 이벤트만 세로 방향을 뒤집은
새 이벤트로 바꿔 보낸다. 원래 이벤트의 delta 필드만 수정하면 앱에 반영되지
않아 대체 이벤트 방식을 사용한다. 트랙패드와 가로 스크롤은 그대로 둔다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:52:18 +09:00
2 changed files with 90 additions and 0 deletions
+4
View File
@@ -2,6 +2,10 @@
Razer Pro Click V2 (vendor 5426, product 209)의 휠 좌우 틸트를 각각 `Command+[` / `Command+]`로 보냅니다. 제공한 HID 보고서의 첫 바이트와 비트 마스크를 그대로 사용합니다.
마우스 휠의 세로 스크롤 방향도 반전합니다. 트랙패드와 Magic Mouse처럼 제스처로 스크롤하는 장치와 가로 스크롤은 바꾸지 않습니다.
틸트할 때 함께 들어오는 원래 버튼 이벤트(4~7번 버튼, 틸트 직후의 휠 클릭)는 막습니다. 측면 버튼도 4·5번 버튼이라 원래 동작 대신 뒤로/앞으로 가기로 동작합니다.
## 설치 / 업데이트
```sh
+86
View File
@@ -147,4 +147,90 @@ 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?
// Mouse buttons whose down event was swallowed, so the matching drag/up is swallowed too.
var suppressedButtons = Set<Int64>()
// CG button numbers (0-based) of the tilt bits 0x08/0x10/0x20/0x40, i.e. buttons 4-7.
let tiltButtonNumbers: ClosedRange<Int64> = 3...6
let middleButtonNumber: Int64 = 2
// A wheel click this close to a tilt is the wheel being pressed while it is pushed sideways.
let tiltClickWindow: TimeInterval = 0.15
func tiltActive() -> Bool {
if state.lastBits.values.contains(where: { $0 != 0 }) { return true }
let now = ProcessInfo.processInfo.systemUptime
return state.lastFired.values.contains { now - $0 < tiltClickWindow }
}
// Swallow the native button events behind a tilt: many apps treat buttons 4-7 like a
// middle click (e.g. open link in new tab), and tilting can also press the wheel button.
func shouldSuppress(_ type: CGEventType, _ event: CGEvent) -> Bool {
let button = event.getIntegerValueField(.mouseEventButtonNumber)
switch type {
case .otherMouseDown:
if tiltButtonNumbers.contains(button) || (button == middleButtonNumber && tiltActive()) {
suppressedButtons.insert(button)
return true
}
return false
case .otherMouseDragged:
return suppressedButtons.contains(button)
case .otherMouseUp:
return suppressedButtons.remove(button) != nil
default:
return false
}
}
// 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)
}
if shouldSuppress(type, event) { return nil }
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 lineY = event.getIntegerValueField(.scrollWheelEventDeltaAxis1)
let pointY = event.getIntegerValueField(.scrollWheelEventPointDeltaAxis1)
let fixedY = event.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1)
guard isWheel, lineY != 0 || pointY != 0 || fixedY != 0 else { return Unmanaged.passUnretained(event) }
let lineX = event.getIntegerValueField(.scrollWheelEventDeltaAxis2)
let pointX = event.getIntegerValueField(.scrollWheelEventPointDeltaAxis2)
let fixedX = event.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2)
// Apps read the hardware data attached to the original event, so editing its delta
// fields has no visible effect. Post a fresh line-based event (like a real wheel, so
// apps keep their per-line scroll distance) carrying the original deltas, Y flipped.
guard let replacement = CGEvent(scrollWheelEvent2Source: nil, units: .line, wheelCount: 2,
wheel1: Int32(clamping: -lineY), wheel2: Int32(clamping: lineX), wheel3: 0)
else { return Unmanaged.passUnretained(event) }
replacement.setIntegerValueField(.scrollWheelEventPointDeltaAxis1, value: -pointY)
replacement.setIntegerValueField(.scrollWheelEventPointDeltaAxis2, value: pointX)
replacement.setDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1, value: -fixedY)
replacement.setDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2, value: fixedX)
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: [CGEventType.scrollWheel, .otherMouseDown, .otherMouseDragged, .otherMouseUp]
.reduce(CGEventMask(0)) { $0 | CGEventMask(1 << $1.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()