import AppKit import AXExtension import Foundation import Logger import Status public extension XcodeAppInstanceInspector { func triggerCopilotCommand(name: String, activateXcode: Bool = true) async throws { let bundleName = Bundle.main.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String let status = await getExtensionStatus(bundleName: bundleName) guard status == .granted else { let reason: String switch status { case .notGranted: reason = "No bundle found for \(bundleName)." case .disabled: reason = "\(bundleName) is found but disabled." default: reason = "" } throw CantRunCommand(path: "Editor/\(bundleName)/\(name)", reason: reason) } try await triggerMenuItem(path: ["Editor", bundleName, name], activateApp: activateXcode) } private func getExtensionStatus(bundleName: String) async -> ExtensionPermissionStatus { let app = AXUIElementCreateApplication(runningApplication.processIdentifier) guard let menuBar = app.menuBar, let editorMenu = menuBar.child(title: "Editor") else { return .notGranted } if let bundleMenuItem = editorMenu.child(title: bundleName, role: "AXMenuItem") { var enabled: CFTypeRef? let error = AXUIElementCopyAttributeValue(bundleMenuItem, kAXEnabledAttribute as CFString, &enabled) if error == .success, let isEnabled = enabled as? Bool { return isEnabled ? .granted : .disabled } return .disabled } return .notGranted } } public extension AppInstanceInspector { struct CantRunCommand: Error, LocalizedError { let path: String let reason: String public var errorDescription: String { "Can't run command \(path): \(reason)" } } @MainActor func triggerMenuItem(path: [String], activateApp: Bool) async throws { let sourcePath = path.joined(separator: "/") func cantRunCommand(_ reason: String) -> CantRunCommand { return CantRunCommand(path: sourcePath, reason: reason) } guard path.count >= 2 else { throw cantRunCommand("Path too short.") } if activateApp { if !runningApplication.activate() { Logger.service.error(""" Trigger menu item \(sourcePath): \ Xcode not activated. """) } } else { if !runningApplication.isActive { Logger.service.error(""" Trigger menu item \(sourcePath): \ Xcode not activated. """) } } await Task.yield() if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) { let app = AXUIElementCreateApplication(runningApplication.processIdentifier) guard let menuBar = app.menuBar else { Logger.service.error(""" Trigger menu item \(sourcePath) failed: \ Menu not found. """) throw cantRunCommand("Menu not found.") } var path = path var currentMenu = menuBar while !path.isEmpty { let item = path.removeFirst() if path.isEmpty, let button = currentMenu.child(title: item, role: "AXMenuItem") { let error = AXUIElementPerformAction(button, kAXPressAction as CFString) if error != AXError.success { Logger.service.error(""" Trigger menu item \(sourcePath) failed: \ \(error.localizedDescription) """) throw cantRunCommand(error.localizedDescription) } else { #if DEBUG Logger.service.info(""" Trigger menu item \(sourcePath) succeeded. """) #endif return } } else if let menu = currentMenu.child(title: item) { #if DEBUG Logger.service.info(""" Trigger menu item \(sourcePath): Move to \(item). """) #endif currentMenu = menu } else { Logger.service.error(""" Trigger menu item \(sourcePath) failed: \ \(item) is not found. """) throw cantRunCommand("\(item) is not found.") } } } else { let clickTask = { var path = path let button = path.removeLast() let menuBarItem = path.removeFirst() let list = path .reversed() .map { "menu 1 of menu item \"\($0)\"" } .joined(separator: " of ") return """ click menu item "\(button)" of \(list) \ of menu bar item "\(menuBarItem)" \ of menu bar 1 """ }() /// check if menu is open, if not, click the menu item. let appleScript = """ tell application "System Events" set theprocs to every process whose unix id is \ \(runningApplication.processIdentifier) repeat with proc in theprocs tell proc repeat with theMenu in menus of menu bar 1 set theValue to value of attribute "AXVisibleChildren" of theMenu if theValue is not {} then return end if end repeat \(clickTask) end tell end repeat end tell """ do { try await runAppleScript(appleScript) } catch { Logger.service.error(""" Trigger menu item \(path.joined(separator: "/")) failed: \ \(error.localizedDescription) """) throw cantRunCommand(error.localizedDescription) } } } } @discardableResult func runAppleScript(_ appleScript: String) async throws -> String { let task = Process() task.launchPath = "/usr/bin/osascript" task.arguments = ["-e", appleScript] let outpipe = Pipe() task.standardOutput = outpipe task.standardError = Pipe() return try await withUnsafeThrowingContinuation { continuation in do { task.terminationHandler = { _ in do { if let data = try outpipe.fileHandleForReading.readToEnd(), let content = String(data: data, encoding: .utf8) { continuation.resume(returning: content) return } continuation.resume(returning: "") } catch { continuation.resume(throwing: error) } } try task.run() } catch { continuation.resume(throwing: error) } } }