|
| 1 | +import QuartzCore |
| 2 | + |
| 3 | +enum ClickMonitorError: Error, CustomStringConvertible { |
| 4 | + case failedToCreateTap |
| 5 | + case failedToCreateSource |
| 6 | + |
| 7 | + var description: String { |
| 8 | + switch self { |
| 9 | + case .failedToCreateTap: |
| 10 | + "알 수 없는 오류가 발생했습니다. (tap)" |
| 11 | + case .failedToCreateSource: |
| 12 | + "알 수 없는 오류가 발생했습니다. (source)" |
| 13 | + } |
| 14 | + } |
| 15 | +} |
| 16 | + |
| 17 | +/** |
| 18 | + 마우스 클릭 모니터링 |
| 19 | + @see https://github.com/pqrs-org/Karabiner-Elements/blob/main/DEVELOPMENT.md |
| 20 | + @see https://github.com/pqrs-org/Karabiner-Elements/blob/main/src/share/monitor/event_tap_monitor.hpp |
| 21 | + */ |
| 22 | +class ClickMonitor { |
| 23 | + private var tap: CFMachPort? |
| 24 | + private var source: CFRunLoopSource? |
| 25 | + |
| 26 | + func start() throws { |
| 27 | + debug() |
| 28 | + |
| 29 | + if tap != nil || source != nil { |
| 30 | + warning("초기화된 tap 또는 source가 이미 있음") |
| 31 | + return |
| 32 | + } |
| 33 | + |
| 34 | + let tap = CGEvent.tapCreate( |
| 35 | + tap: .cghidEventTap, |
| 36 | + place: .headInsertEventTap, |
| 37 | + options: .listenOnly, |
| 38 | + eventsOfInterest: CGEventMask( |
| 39 | + 1 << CGEventType.leftMouseDown.rawValue |
| 40 | + | 1 << CGEventType.rightMouseDown.rawValue |
| 41 | + | 1 << CGEventType.otherMouseDown.rawValue), |
| 42 | + callback: { _, _, event, _ in |
| 43 | + // 사용자가 마우스 클릭하는 시점에 초기화 |
| 44 | + AppDelegate.shared().reset() |
| 45 | + return Unmanaged.passUnretained(event) |
| 46 | + }, |
| 47 | + userInfo: nil |
| 48 | + ) |
| 49 | + guard let tap else { |
| 50 | + throw ClickMonitorError.failedToCreateTap |
| 51 | + } |
| 52 | + |
| 53 | + let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0) |
| 54 | + guard let source else { |
| 55 | + throw ClickMonitorError.failedToCreateSource |
| 56 | + } |
| 57 | + |
| 58 | + CFRunLoopAddSource(CFRunLoopGetMain(), source, .defaultMode) |
| 59 | + CGEvent.tapEnable(tap: tap, enable: true) |
| 60 | + |
| 61 | + self.tap = tap |
| 62 | + self.source = source |
| 63 | + debug("ClickMonitor 시작 성공") |
| 64 | + } |
| 65 | + |
| 66 | + func stop() { |
| 67 | + debug() |
| 68 | + |
| 69 | + if let tap { |
| 70 | + CGEvent.tapEnable(tap: tap, enable: false) |
| 71 | + CFMachPortInvalidate(tap) |
| 72 | + } else { |
| 73 | + warning("초기화된 tap이 없음") |
| 74 | + } |
| 75 | + |
| 76 | + if let source { |
| 77 | + CFRunLoopSourceInvalidate(source) |
| 78 | + } else { |
| 79 | + warning("초기화된 source가 없음") |
| 80 | + } |
| 81 | + |
| 82 | + self.tap = nil |
| 83 | + self.source = nil |
| 84 | + debug("ClickMonitor 중단 성공") |
| 85 | + } |
| 86 | +} |
0 commit comments