Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions Sources/OpenWisprLib/HotkeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
69 changes: 69 additions & 0 deletions Tests/OpenWisprTests/HotkeyManagerTests.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}