@@ -4,6 +4,8 @@ import AXExtension
44import AXNotificationStream
55import Combine
66import Foundation
7+ import Logger
8+ import Preferences
79import SuggestionModel
810
911public final class XcodeInspector : ObservableObject {
@@ -88,8 +90,6 @@ public final class XcodeInspector: ObservableObject {
8890 . first ( where: \. isActive)
8991 . map ( AppInstanceInspector . init ( runningApplication: ) )
9092
91- #warning("Test Me")
92-
9393 Task { // Did activate app
9494 if let activeXcode {
9595 await setActiveXcode ( activeXcode)
@@ -546,3 +546,132 @@ extension XcodeAppInstanceInspector {
546546 }
547547}
548548
549+ // MARK: - Triggering Command
550+
551+ public extension XcodeAppInstanceInspector {
552+ func triggerCopilotCommand( name: String ) async throws {
553+ let bundleName = Bundle . main
554+ . object ( forInfoDictionaryKey: " EXTENSION_BUNDLE_NAME " ) as! String
555+ try await triggerMenuItem ( path: [ " Editor " , bundleName, name] )
556+ }
557+ }
558+
559+ public extension AppInstanceInspector {
560+ func triggerMenuItem( path: [ String ] ) async throws {
561+ guard !path. isEmpty else { return }
562+
563+ struct CantRunCommand : Error , LocalizedError {
564+ let path : [ String ]
565+ var errorDescription : String ? {
566+ " Can't run command \( path. joined ( separator: " / " ) ) . "
567+ }
568+ }
569+
570+ if !runningApplication. isActive { runningApplication. activate ( ) }
571+
572+ if UserDefaults . shared. value ( for: \. triggerActionWithAccessibilityAPI) {
573+ let app = AXUIElementCreateApplication ( runningApplication. processIdentifier)
574+ guard let menuBar = app. menuBar else { throw CantRunCommand ( path: path) }
575+ var path = path
576+ var currentMenu = menuBar
577+ while !path. isEmpty {
578+ let item = path. removeFirst ( )
579+
580+ if path. isEmpty, let button = currentMenu. child ( title: item, role: " AXMenuItem " ) {
581+ let error = AXUIElementPerformAction ( button, kAXPressAction as CFString )
582+ if error != AXError . success {
583+ Logger . service. error ( """
584+ Trigger menu item \( path. joined ( separator: " / " ) ) failed: \
585+ \( error. localizedDescription)
586+ """ )
587+ throw error
588+ } else {
589+ return
590+ }
591+ } else if let menu = currentMenu. child ( title: item) {
592+ currentMenu = menu
593+ } else {
594+ throw CantRunCommand ( path: path)
595+ }
596+ }
597+ } else {
598+ guard path. count >= 2 else { throw CantRunCommand ( path: path) }
599+
600+ let clickTask = {
601+ var path = path
602+ let button = path. removeLast ( )
603+ let menuBarItem = path. removeFirst ( )
604+ let list = path
605+ . reversed ( )
606+ . map { " menu 1 of menu item \" \( $0) \" " }
607+ . joined ( separator: " of " )
608+ return """
609+ click menu item " \( button) " of \( list) \
610+ of menu bar item " \( menuBarItem) " \
611+ of menu bar 1
612+ """
613+ } ( )
614+ /// check if menu is open, if not, click the menu item.
615+ let appleScript = """
616+ tell application " System Events "
617+ set theprocs to every process whose unix id is \
618+ \( runningApplication. processIdentifier)
619+ repeat with proc in theprocs
620+ set the frontmost of proc to true
621+ tell proc
622+ repeat with theMenu in menus of menu bar 1
623+ set theValue to value of attribute " AXVisibleChildren " of theMenu
624+ if theValue is not {} then
625+ return
626+ end if
627+ end repeat
628+ \( clickTask)
629+ end tell
630+ end repeat
631+ end tell
632+ """
633+
634+ do {
635+ try await runAppleScript ( appleScript)
636+ } catch {
637+ Logger . service. error ( """
638+ Trigger menu item \( path. joined ( separator: " / " ) ) failed: \
639+ \( error. localizedDescription)
640+ """ )
641+ throw error
642+ }
643+ }
644+ }
645+ }
646+
647+ @discardableResult
648+ func runAppleScript( _ appleScript: String ) async throws -> String {
649+ let task = Process ( )
650+ task. launchPath = " /usr/bin/osascript "
651+ task. arguments = [ " -e " , appleScript]
652+ let outpipe = Pipe ( )
653+ task. standardOutput = outpipe
654+ task. standardError = Pipe ( )
655+
656+ return try await withUnsafeThrowingContinuation { continuation in
657+ do {
658+ task. terminationHandler = { _ in
659+ do {
660+ if let data = try outpipe. fileHandleForReading. readToEnd ( ) ,
661+ let content = String ( data: data, encoding: . utf8)
662+ {
663+ continuation. resume ( returning: content)
664+ return
665+ }
666+ continuation. resume ( returning: " " )
667+ } catch {
668+ continuation. resume ( throwing: error)
669+ }
670+ }
671+ try task. run ( )
672+ } catch {
673+ continuation. resume ( throwing: error)
674+ }
675+ }
676+ }
677+
0 commit comments