Skip to content

Commit

Permalink
feat: add option to listen to mouse clicks
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Jan 4, 2021
1 parent f4839a3 commit d1d129f
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 16 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -196,7 +196,8 @@ execute the following command:
sendkeys mouse-position
```

Use the `--wait` option to capture multiple mouse positions in a session.
Use the `--watch` option to capture the location of mouse clicks, and combine it with `--output commands` to output
approximate mouse commands that can be used to _replay_ mouse actions.

## Prerequisites

Expand Down
15 changes: 15 additions & 0 deletions Sources/SendKeysLib/Bridge.swift
@@ -0,0 +1,15 @@
func bridge<T : AnyObject>(obj : T) -> UnsafeRawPointer {
return UnsafeRawPointer(Unmanaged.passUnretained(obj).toOpaque())
}

func bridge<T : AnyObject>(ptr : UnsafeRawPointer) -> T {
return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue()
}

func bridgeRetained<T : AnyObject>(obj : T) -> UnsafeRawPointer {
return UnsafeRawPointer(Unmanaged.passRetained(obj).toOpaque())
}

func bridgeTransfer<T : AnyObject>(ptr : UnsafeRawPointer) -> T {
return Unmanaged<T>.fromOpaque(ptr).takeRetainedValue()
}
98 changes: 98 additions & 0 deletions Sources/SendKeysLib/MouseEventProcessor.swift
@@ -0,0 +1,98 @@
import Foundation

enum MouseEventType {
case click
case drag
}

enum MouseButton: String, CustomStringConvertible {
case left
case right
case center
case other

var description: String {
get {
return self.rawValue
}
}
}

struct RawMouseEvent {
let eventType: CGEventType
let button: MouseButton
let point: CGPoint

init(eventType: CGEventType, button: MouseButton, point: CGPoint) {
self.eventType = eventType
self.button = button
self.point = point
}
}

struct MouseEvent {
let eventType: MouseEventType
let button: MouseButton
let startPoint: CGPoint
let endPoint: CGPoint

init(eventType: MouseEventType, button: MouseButton, startPoint: CGPoint, endPoint: CGPoint) {
self.eventType = eventType
self.button = button
self.startPoint = startPoint
self.endPoint = endPoint
}
}

class MouseEventProcessor {
var events: [RawMouseEvent] = []

func consumeEvent(type: CGEventType, event: CGEvent) -> MouseEvent? {
let button = getMouseButton(type: type, event: event)
let rawEvent = RawMouseEvent(eventType: type, button: button, point: event.location)

switch type {
case .leftMouseUp, .rightMouseUp, .otherMouseUp:
switch events.last?.eventType {
case .leftMouseDown, .rightMouseDown, .otherMouseDown:
return MouseEvent(eventType: .click, button: button, startPoint: events.first!.point, endPoint: event.location)
case .leftMouseDragged, .rightMouseDragged, .otherMouseDragged:
return MouseEvent(eventType: .drag, button: button, startPoint: events.first!.point, endPoint: event.location)
default:
events.append(rawEvent)
}
events = []
default:
events.append(rawEvent)
}

return nil
}

private func getMouseButton(type: CGEventType, event: CGEvent) -> MouseButton {
var button: MouseButton = .other

switch type {
case .leftMouseDown, .leftMouseUp, .leftMouseDragged:
button = .left
case .rightMouseDown, .rightMouseUp, .rightMouseDragged:
button = .right
case .otherMouseDown, .otherMouseUp, .otherMouseDragged:
let buttonNumber = event.getIntegerValueField(.mouseEventButtonNumber)
switch UInt32(buttonNumber) {
case CGMouseButton.left.rawValue:
button = .left
case CGMouseButton.right.rawValue:
button = .right
case CGMouseButton.center.rawValue:
button = .center
default:
button = .other
}
default:
button = .other
}

return button
}
}
120 changes: 105 additions & 15 deletions Sources/SendKeysLib/MousePosition.swift
@@ -1,30 +1,56 @@
import ArgumentParser
import Foundation

struct MousePosition: ParsableCommand {
enum OutputMode: String, Codable, ExpressibleByArgument {
case coordinates
case commands
}

class MousePosition: ParsableCommand {
public static let configuration = CommandConfiguration(
abstract: "Prints the current mouse position."
)

@Flag(help: "Wait for user input to diplay the mouse position. Useful for capturing multiple mouse positions.")
var wait = false

mutating func run() {
if wait {
listenForInput()

@Flag(name: .shortAndLong, help: "Watch and display the mouse positions as the mouse is clicked.")
var watch = false

@Option(name: NameSpecification([.customShort("o"), .customLong("output", withSingleDash: false) ]), help: "Displays results as either a series of coordinates or commands.")
var mode = OutputMode.coordinates

@Option(name: NameSpecification([.customShort("d"), .long ]), help: "Default duration to attach to commands.")
var includeDelay: Double = 0.5

static let eventProcessor = MouseEventProcessor()
static var numberFormatter = NumberFormatter()

required init() {
MousePosition.numberFormatter.usesSignificantDigits = true
MousePosition.numberFormatter.minimumSignificantDigits = 1
MousePosition.numberFormatter.maximumSignificantDigits = 4
}

func run() {
if watch {
watchMouseInput()
} else {
printMousePosition()
}
}

func printMousePosition() {
let location = MouseController().getLocation()!
print(String(format: "%.0f,%.0f", location.x, location.y))
}

func listenForInput() {
fputs("Waiting for user input... Escape or ctrl + d to stop, or any other key to capture mouse position.\n", stderr)


waitForCharInput { _ in
printMousePosition()
}
}

func waitForCharInput(callback: (_ char: UInt8) -> Void) {
let stdIn = FileHandle.standardInput
let originalTerm = enableRawMode(fileHandle: stdIn)

Expand All @@ -33,13 +59,77 @@ struct MousePosition: ParsableCommand {
if char == 4 /* EOF (Ctrl+D) */ || char == 27 /* escape */ {
break
}
printMousePosition()

callback(char)
}

restoreRawMode(fileHandle: stdIn, originalTerm: originalTerm)
}


func watchMouseInput() {
fputs("Waiting for mouse input... ctrl + c to stop.\n", stderr)

var eventMask = (1 << CGEventType.leftMouseDown.rawValue)
| (1 << CGEventType.leftMouseUp.rawValue)
| (1 << CGEventType.leftMouseDragged.rawValue)
eventMask = eventMask | (1 << CGEventType.rightMouseDown.rawValue)
| (1 << CGEventType.rightMouseUp.rawValue)
| (1 << CGEventType.rightMouseDragged.rawValue)
eventMask = eventMask | (1 << CGEventType.otherMouseDown.rawValue)
| (1 << CGEventType.otherMouseUp.rawValue)
| (1 << CGEventType.otherMouseDragged.rawValue)

let info = UnsafeMutableRawPointer(mutating: bridge(obj: self))

guard let eventTap = CGEvent.tapCreate(tap: .cghidEventTap, place: .tailAppendEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), callback: {
(proxy: CGEventTapProxy, eventType: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? in
let command: MousePosition = bridge(ptr: UnsafeRawPointer(refcon)!)
let duration = MousePosition.numberFormatter.string(from: NSNumber(value: command.includeDelay))!

if let mouseEvent = MousePosition.eventProcessor.consumeEvent(type: eventType, event: event) {
switch command.mode {
case .coordinates:
if mouseEvent.eventType == .click {
command.printAndFlush(String(format: "%.0f,%.0f", mouseEvent.endPoint.x, mouseEvent.endPoint.y))
}
case .commands:
switch mouseEvent.eventType {
case .click:
command.printAndFlush(String(format: "<m:%.0f,%.0f:%@><m:%@><\\>", mouseEvent.endPoint.x, mouseEvent.endPoint.y, duration, mouseEvent.button.rawValue))
case .drag:
command.printAndFlush(String(format: "<d:%.0f,%.0f,%.0f,%.0f:%@:%@><\\>", mouseEvent.startPoint.x, mouseEvent.startPoint.y, mouseEvent.endPoint.x, mouseEvent.endPoint.y, duration, mouseEvent.button.rawValue))
}
}
}

return Unmanaged.passRetained(event)
}, userInfo: info) else {
MousePosition.exit(withError: RuntimeError("Failed to create event tap."))
}

let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
CFRunLoopRun()
}

func printAndFlush(_ message: String) {
print(message)
fflush(stdout)
}

func eventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
switch mode {
case .coordinates:
printMousePosition()
case .commands:
printMousePosition()
}
print("Event \(type) \(type.rawValue)")

return Unmanaged.passRetained(event)
}

// see https://stackoverflow.com/a/24335355/669586
func initStruct<S>() -> S {
let struct_pointer = UnsafeMutablePointer<S>.allocate(capacity: 1)
Expand Down

0 comments on commit d1d129f

Please sign in to comment.