diff --git a/Sources/OpenWisprLib/HotkeyManager.swift b/Sources/OpenWisprLib/HotkeyManager.swift index 1e31b3a..77c67ab 100644 --- a/Sources/OpenWisprLib/HotkeyManager.swift +++ b/Sources/OpenWisprLib/HotkeyManager.swift @@ -2,16 +2,36 @@ import AppKit import Foundation class HotkeyManager { + typealias GlobalMonitorInstaller = (NSEvent.EventTypeMask, @escaping (NSEvent) -> Void) -> Any? + typealias LocalMonitorInstaller = (NSEvent.EventTypeMask, @escaping (NSEvent) -> NSEvent?) -> Any? + private var globalMonitor: Any? + private var localMonitor: Any? private let keyCode: UInt16 private let requiredModifiers: UInt64 + private let addGlobalMonitor: GlobalMonitorInstaller + private let addLocalMonitor: LocalMonitorInstaller + private let removeMonitor: (Any) -> Void private var onKeyDown: (() -> Void)? private var onKeyUp: (() -> Void)? private var modifierPressed = false - init(keyCode: UInt16, modifiers: UInt64 = 0) { + init( + keyCode: UInt16, + modifiers: UInt64 = 0, + addGlobalMonitor: @escaping GlobalMonitorInstaller = { mask, handler in + NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) + }, + addLocalMonitor: @escaping LocalMonitorInstaller = { mask, handler in + NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler) + }, + removeMonitor: @escaping (Any) -> Void = { NSEvent.removeMonitor($0) } + ) { self.keyCode = keyCode self.requiredModifiers = modifiers + self.addGlobalMonitor = addGlobalMonitor + self.addLocalMonitor = addLocalMonitor + self.removeMonitor = removeMonitor } func start(onKeyDown: @escaping () -> Void, onKeyUp: @escaping () -> Void) { @@ -20,16 +40,24 @@ class HotkeyManager { let mask: NSEvent.EventTypeMask = [.keyDown, .keyUp, .flagsChanged] - globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: mask) { [weak self] event in + globalMonitor = addGlobalMonitor(mask) { [weak self] event in + self?.handleEvent(event) + } + localMonitor = addLocalMonitor(mask) { [weak self] event in self?.handleEvent(event) + return event } } func stop() { if let monitor = globalMonitor { - NSEvent.removeMonitor(monitor) + removeMonitor(monitor) + } + if let monitor = localMonitor { + removeMonitor(monitor) } globalMonitor = nil + localMonitor = nil } private func handleEvent(_ event: NSEvent) { diff --git a/Tests/OpenWisprTests/HotkeyManagerTests.swift b/Tests/OpenWisprTests/HotkeyManagerTests.swift new file mode 100644 index 0000000..42d3c92 --- /dev/null +++ b/Tests/OpenWisprTests/HotkeyManagerTests.swift @@ -0,0 +1,69 @@ +import AppKit +import XCTest +@testable import OpenWisprLib + +final class HotkeyManagerTests: XCTestCase { + func testMonitorsHotkeyEventsFromOtherAppsAndOpenWisprItself() throws { + let globalToken = NSObject() + let localToken = NSObject() + var globalHandler: ((NSEvent) -> Void)? + var localHandler: ((NSEvent) -> NSEvent?)? + var removedTokens: [AnyObject] = [] + var keyDownCount = 0 + var keyUpCount = 0 + + let manager = HotkeyManager( + keyCode: 49, + addGlobalMonitor: { mask, handler in + XCTAssertTrue(mask.contains(.keyDown)) + XCTAssertTrue(mask.contains(.keyUp)) + XCTAssertTrue(mask.contains(.flagsChanged)) + globalHandler = handler + return globalToken + }, + addLocalMonitor: { mask, handler in + XCTAssertTrue(mask.contains(.keyDown)) + XCTAssertTrue(mask.contains(.keyUp)) + XCTAssertTrue(mask.contains(.flagsChanged)) + localHandler = handler + return localToken + }, + removeMonitor: { removedTokens.append($0 as AnyObject) } + ) + + manager.start( + onKeyDown: { keyDownCount += 1 }, + onKeyUp: { keyUpCount += 1 } + ) + + let keyDown = try XCTUnwrap(makeKeyEvent(type: .keyDown, keyCode: 49)) + let keyUp = try XCTUnwrap(makeKeyEvent(type: .keyUp, keyCode: 49)) + globalHandler?(keyDown) + let returnedEvent = localHandler?(keyUp) + + XCTAssertEqual(keyDownCount, 1) + XCTAssertEqual(keyUpCount, 1) + XCTAssertTrue(returnedEvent === keyUp) + + manager.stop() + + XCTAssertEqual(removedTokens.count, 2) + XCTAssertTrue(removedTokens.contains { $0 === globalToken }) + XCTAssertTrue(removedTokens.contains { $0 === localToken }) + } + + private func makeKeyEvent(type: NSEvent.EventType, keyCode: UInt16) -> NSEvent? { + NSEvent.keyEvent( + with: type, + location: .zero, + modifierFlags: [], + timestamp: 0, + windowNumber: 0, + context: nil, + characters: " ", + charactersIgnoringModifiers: " ", + isARepeat: false, + keyCode: keyCode + ) + } +}