166 lines
6.4 KiB
Swift
166 lines
6.4 KiB
Swift
import AppKit
|
|
import CoreGraphics
|
|
|
|
let keyCodes: [String: CGKeyCode] = [
|
|
"a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7, "c": 8, "v": 9, "b": 11, "q": 12,
|
|
"w": 13, "e": 14, "r": 15, "y": 16, "t": 17, "1": 18, "2": 19, "3": 20, "4": 21, "6": 22, "5": 23,
|
|
"=": 24, "9": 25, "7": 26, "-": 27, "8": 28, "0": 29, "]": 30, "o": 31, "u": 32, "[": 33, "i": 34,
|
|
"p": 35, "return": 36, "enter": 36, "l": 37, "j": 38, "'": 39, "k": 40, ";": 41, "\\": 42, ",": 43,
|
|
"/": 44, "n": 45, "m": 46, ".": 47, "tab": 48, "space": 49, "`": 50, "delete": 51, "backspace": 51,
|
|
"escape": 53, "esc": 53, "f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97, "f7": 98,
|
|
"f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111, "home": 115, "pageup": 116,
|
|
"forwarddelete": 117, "end": 119, "pagedown": 121, "left": 123, "right": 124, "down": 125, "up": 126,
|
|
]
|
|
|
|
let modifierFlags: [String: CGEventFlags] = [
|
|
"cmd": .maskCommand, "command": .maskCommand, "shift": .maskShift, "alt": .maskAlternate,
|
|
"opt": .maskAlternate, "option": .maskAlternate, "ctrl": .maskControl, "control": .maskControl,
|
|
"fn": .maskSecondaryFn,
|
|
]
|
|
|
|
func pause(_ ms: Int) async {
|
|
try? await Task.sleep(nanoseconds: UInt64(ms) * 1_000_000)
|
|
}
|
|
|
|
let shifted: [Character: String] = [
|
|
"!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9", ")": "0",
|
|
"_": "-", "+": "=", "{": "[", "}": "]", "|": "\\", ":": ";", "\"": "'", "<": ",", ">": ".", "?": "/", "~": "`",
|
|
]
|
|
|
|
let named: [Character: String] = [" ": "space", "\n": "return", "\t": "tab"]
|
|
|
|
func keystroke(_ ch: Character) -> (CGKeyCode, CGEventFlags)? {
|
|
if let name = named[ch], let c = keyCodes[name] {
|
|
return (c, [])
|
|
}
|
|
if let base = shifted[ch], let c = keyCodes[base] {
|
|
return (c, .maskShift)
|
|
}
|
|
if ch.isUppercase, let c = keyCodes[ch.lowercased()] {
|
|
return (c, .maskShift)
|
|
}
|
|
if let c = keyCodes[String(ch)] {
|
|
return (c, [])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lockInput() async throws -> Int32 {
|
|
let fd = open(NSTemporaryDirectory() + "byre-hands-input.lock", O_CREAT | O_RDWR, 0o600)
|
|
if fd < 0 {
|
|
throw ToolError("open the input lock: errno \(errno)")
|
|
}
|
|
let start = Date()
|
|
while flock(fd, LOCK_EX | LOCK_NB) != 0 {
|
|
if Date().timeIntervalSince(start) > 30 {
|
|
close(fd)
|
|
throw ToolError("another agent has been driving this Mac's apps for 30s, so nothing ran; try again")
|
|
}
|
|
await pause(100)
|
|
}
|
|
return fd
|
|
}
|
|
|
|
func post(_ e: CGEvent?) {
|
|
e?.post(tap: .cghidEventTap)
|
|
}
|
|
|
|
func isFront(_ info: WindowInfo) throws -> Bool {
|
|
guard NSWorkspace.shared.frontmostApplication?.processIdentifier == info.pid,
|
|
let focused = try attr(appElement(info.pid), kAXFocusedWindowAttribute)
|
|
else {
|
|
return false
|
|
}
|
|
var wid: CGWindowID = 0
|
|
return _AXUIElementGetWindow(focused as! AXUIElement, &wid) == .success && wid == info.id
|
|
}
|
|
|
|
func focusWindow(_ id: Int, _ w: AXUIElement) async throws -> WindowInfo {
|
|
let info = try window(id)
|
|
if try isFront(info) {
|
|
return info
|
|
}
|
|
NSRunningApplication(processIdentifier: info.pid)?.activate()
|
|
AXUIElementPerformAction(w, kAXRaiseAction as CFString)
|
|
await pause(150)
|
|
let now = try window(id)
|
|
if try !isFront(now) {
|
|
throw ToolError("could not bring window \(id) to the front (\(NSWorkspace.shared.frontmostApplication?.localizedName ?? "nothing") is frontmost), so no input was sent")
|
|
}
|
|
return now
|
|
}
|
|
|
|
func click(_ p: CGPoint, right: Bool, count: Int) async {
|
|
let (down, up): (CGEventType, CGEventType) = right ? (.rightMouseDown, .rightMouseUp) : (.leftMouseDown, .leftMouseUp)
|
|
let button: CGMouseButton = right ? .right : .left
|
|
post(CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: button))
|
|
await pause(30)
|
|
for i in 1...max(1, count) {
|
|
for t in [down, up] {
|
|
let e = CGEvent(mouseEventSource: nil, mouseType: t, mouseCursorPosition: p, mouseButton: button)
|
|
e?.setIntegerValueField(.mouseEventClickState, value: Int64(i))
|
|
post(e)
|
|
}
|
|
await pause(30)
|
|
}
|
|
}
|
|
|
|
func drag(_ from: CGPoint, _ to: CGPoint) async {
|
|
post(CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: from, mouseButton: .left))
|
|
post(CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: from, mouseButton: .left))
|
|
await pause(50)
|
|
for i in 1...20 {
|
|
let f = CGFloat(i) / 20
|
|
let p = CGPoint(x: from.x + (to.x - from.x) * f, y: from.y + (to.y - from.y) * f)
|
|
post(CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged, mouseCursorPosition: p, mouseButton: .left))
|
|
await pause(10)
|
|
}
|
|
post(CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: to, mouseButton: .left))
|
|
}
|
|
|
|
func scroll(_ p: CGPoint, dx: Int32, dy: Int32) {
|
|
post(CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: .left))
|
|
post(CGEvent(scrollWheelEvent2Source: nil, units: .line, wheelCount: 2, wheel1: dy, wheel2: dx, wheel3: 0))
|
|
}
|
|
|
|
func type(_ s: String) async {
|
|
for ch in s {
|
|
for down in [true, false] {
|
|
if let (code, flags) = keystroke(ch) {
|
|
let e = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down)
|
|
e?.flags = flags
|
|
post(e)
|
|
continue
|
|
}
|
|
let units = Array(String(ch).utf16)
|
|
let e = CGEvent(keyboardEventSource: nil, virtualKey: 0, keyDown: down)
|
|
e?.keyboardSetUnicodeString(stringLength: units.count, unicodeString: units)
|
|
post(e)
|
|
}
|
|
await pause(15)
|
|
}
|
|
}
|
|
|
|
func key(_ combo: String) throws {
|
|
var flags: CGEventFlags = []
|
|
var code: CGKeyCode?
|
|
for part in combo.lowercased().split(separator: "+").map(String.init) {
|
|
if let f = modifierFlags[part] {
|
|
flags.insert(f)
|
|
continue
|
|
}
|
|
guard let c = keyCodes[part] else {
|
|
throw ToolError("unknown key \(part.debugDescription) in \(combo.debugDescription)")
|
|
}
|
|
code = c
|
|
}
|
|
guard let code else {
|
|
throw ToolError("no key in \(combo.debugDescription)")
|
|
}
|
|
for down in [true, false] {
|
|
let e = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: down)
|
|
e?.flags = flags
|
|
post(e)
|
|
}
|
|
}
|