Skip to content

Instantly share code, notes, and snippets.

@stuartcarnie
Last active March 5, 2023 19:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stuartcarnie/d7f4128af4fa48970ea221119cfde45e to your computer and use it in GitHub Desktop.
Save stuartcarnie/d7f4128af4fa48970ea221119cfde45e to your computer and use it in GitHub Desktop.
Combine Publisher support for NSEvent.addLocalMonitorForEvents
enum MouseState {
case idle, moving
}
// only publish events when we're active and the main window
let pub = NSEvent.publisher(scope: .local, matching: .mouseMoved)
.filter { _ in NSApp.isActive && window.isMainWindow }
// whilst we're receiving events, send .moving
let a = pub.map { _ in MouseState.moving }
// after we see no events for 2 seconds, send .idle
let b = pub.debounce(for: 2, scheduler: RunLoop.main).map { _ in MouseState.idle }
// merge the streams and remove duplicates (.moving, .moving, ...)
let c = a.merge(with: b).removeDuplicates()
s = c.sink { s in
switch s {
case .idle:
// do something when the mouse is idle
case .moving:
// do something when the mouse started moving
}
}
@available(macOS 10.15, *)
extension NSEvent {
static func publisher(scope: Publisher.Scope, matching: EventTypeMask) -> Publisher {
return Publisher(scope: scope, matching: matching)
}
public struct Publisher: Combine.Publisher {
public enum Scope {
case local, global
}
public typealias Output = NSEvent
public typealias Failure = Never
let scope: Scope
let matching: EventTypeMask
init(scope: Scope, matching: EventTypeMask) {
self.scope = scope
self.matching = matching
}
public func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = Subscription(scope:scope, matching: matching, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
}
@available(macOS 10.15, *)
private extension NSEvent.Publisher {
final class Subscription<S: Subscriber> where S.Input == NSEvent, S.Failure == Never {
fileprivate let lock = NSLock()
fileprivate var demand = Subscribers.Demand.none
private var monitor: Any?
fileprivate let subscriberLock = NSRecursiveLock()
init(scope: Scope, matching: NSEvent.EventTypeMask, subscriber: S) {
switch scope {
case .local:
self.monitor = NSEvent.addLocalMonitorForEvents(matching: matching, handler: { [weak self] (event) -> NSEvent? in
self?.didReceive(event: event, subscriber: subscriber)
return event
})
case .global:
self.monitor = NSEvent.addGlobalMonitorForEvents(matching: matching, handler: { [weak self] in
self?.didReceive(event: $0, subscriber: subscriber)
})
}
}
deinit {
if let monitor = monitor {
NSEvent.removeMonitor(monitor)
}
}
func didReceive(event: NSEvent, subscriber: S) {
let val = { () -> Subscribers.Demand in
lock.lock()
defer { lock.unlock() }
let before = demand
if demand > 0 {
demand -= 1
}
return before
}()
guard val > 0 else { return }
let newDemand = subscriber.receive(event)
lock.lock()
demand += newDemand
lock.unlock()
}
}
}
@available(macOS 10.15, *)
extension NSEvent.Publisher.Subscription: Combine.Subscription {
func request(_ demand: Subscribers.Demand) {
lock.lock()
defer { lock.unlock() }
self.demand += demand
}
func cancel() {
lock.lock()
defer { lock.unlock() }
guard let monitor = monitor else { return }
self.monitor = nil
NSEvent.removeMonitor(monitor)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment