cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
import AppKit
import ApplicationServices
import CryptoKit
import Foundation

func fail(_ message: String, code: Int32 = 1) -> Never {
    fputs("\(message)\n", stderr)
    exit(code)
}

func text(_ value: CFTypeRef?) -> String? {
    if let string = value as? String { return string }
    if let number = value as? NSNumber { return number.stringValue }
    return nil
}

func elements(_ element: AXUIElement, _ name: String) -> [AXUIElement] {
    var value: CFTypeRef?
    let status = AXUIElementCopyAttributeValue(element, name as CFString, &value)
    do {
        return try checkedElements(status: status, value: value)
    } catch {
        fail("failed to enumerate \(name): \(error); inspect the window again")
    }
}

func bounds(_ element: AXUIElement,
            reader: (AXUIElement, String) -> CFTypeRef? = attribute) -> CGRect? {
    guard let positionValue = reader(element, kAXPositionAttribute),
          let sizeValue = reader(element, kAXSizeAttribute),
          CFGetTypeID(positionValue) == AXValueGetTypeID(),
          CFGetTypeID(sizeValue) == AXValueGetTypeID() else { return nil }
    var point = CGPoint.zero
    var dimensions = CGSize.zero
    guard AXValueGetValue(positionValue as! AXValue, .cgPoint, &point),
          AXValueGetValue(sizeValue as! AXValue, .cgSize, &dimensions) else { return nil }
    return CGRect(origin: point, size: dimensions)
}

func actionNames(_ element: AXUIElement) -> [String] {
    var names: CFArray?
    guard AXUIElementCopyActionNames(element, &names) == .success else { return [] }
    return (names as? [String] ?? []).sorted()
}

func valueIsSettable(_ element: AXUIElement) -> Bool {
    var settable: DarwinBoolean = false
    guard AXUIElementIsAttributeSettable(element, kAXValueAttribute as CFString, &settable) == .success else {
        return false
    }
    return settable.boolValue
}

func describeElement(_ element: AXUIElement,
                     reader: (AXUIElement, String) -> CFTypeRef? = attribute) -> [String: Any] {
    let role = text(reader(element, kAXRoleAttribute)) ?? "unknown"
    let subrole = text(reader(element, kAXSubroleAttribute))
    let title = text(reader(element, kAXTitleAttribute))
    let description = text(reader(element, kAXDescriptionAttribute))
    let name = [title, description].compactMap { $0 }.first { !$0.isEmpty } ?? ""
    let secure = role == "AXSecureTextField" || subrole == "AXSecureTextField"
    var node: [String: Any] = [
        "role": role, "name": name,
        "actions": secure ? [] : actionNames(element),
        "settable_value": !secure && valueIsSettable(element),
    ]
    if let subrole { node["subrole"] = subrole }
    if !secure, let value = text(reader(element, kAXValueAttribute)) { node["value"] = value }
    if let identifier = text(reader(element, kAXIdentifierAttribute)) { node["identifier"] = identifier }
    if let enabled = reader(element, kAXEnabledAttribute) as? Bool { node["enabled"] = enabled }
    if let frame = bounds(element, reader: reader) {
        node["bounds"] = ["x": frame.minX, "y": frame.minY, "width": frame.width, "height": frame.height]
    }
    return node
}

func fingerprint(_ node: [String: Any], parent: String) -> String {
    guard let data = try? JSONSerialization.data(withJSONObject: [parent, node], options: [.sortedKeys]) else {
        fail("failed to encode element identity")
    }
    return SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
}

func emit(_ result: [String: Any]) {
    guard let data = try? JSONSerialization.data(withJSONObject: result),
          let output = String(data: data, encoding: .utf8) else { fail("failed to encode AX result") }
    print(output)
}