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) -> Bool { guard NSWorkspace.shared.frontmostApplication?.processIdentifier == info.pid, let focused = 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 isFront(info) { return info } NSRunningApplication(processIdentifier: info.pid)?.activate() AXUIElementPerformAction(w, kAXRaiseAction as CFString) await pause(150) let now = try window(id) if !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) } }