Files
byre/hands/Sources/byre-hands/Input.swift
T

166 lines
6.4 KiB
Swift
Raw Normal View History

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)
}
}