Skip to content

Commit c08e390

Browse files
committed
Merge branch 'feature/trigger-action-with-accessibility-api' into develop
2 parents 83fa467 + 26b8e3a commit c08e390

5 files changed

Lines changed: 98 additions & 24 deletions

File tree

Copilot for Xcode/DebugView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ final class DebugSettings: ObservableObject {
88
@AppStorage(\.preCacheOnFileOpen)
99
var preCacheOnFileOpen: Bool
1010
@AppStorage(\.useCustomScrollViewWorkaround) var useCustomScrollViewWorkaround
11+
@AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI
1112
init() {}
1213
}
1314

@@ -29,6 +30,10 @@ struct DebugSettingsView: View {
2930
Text("Use custom scroll view workaround for smooth scrolling")
3031
}
3132
.toggleStyle(.switch)
33+
Toggle(isOn: $settings.triggerActionWithAccessibilityAPI) {
34+
Text("Trigger action with AccessibilityAPI")
35+
}
36+
.toggleStyle(.switch)
3237
}
3338
}
3439
.buttonStyle(.copilot)

Core/Sources/AXExtension/AXUIElement.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ public extension AXUIElement {
1212
(try? copyValue(key: kAXValueAttribute)) ?? ""
1313
}
1414

15+
var title: String {
16+
(try? copyValue(key: kAXTitleAttribute)) ?? ""
17+
}
18+
19+
var role: String {
20+
(try? copyValue(key: kAXRoleAttribute)) ?? ""
21+
}
22+
1523
var doubleValue: Double {
1624
(try? copyValue(key: kAXValueAttribute)) ?? 0.0
1725
}
@@ -115,14 +123,34 @@ public extension AXUIElement {
115123
(try? copyValue(key: kAXChildrenAttribute)) ?? []
116124
}
117125

126+
var menuBar: AXUIElement? {
127+
try? copyValue(key: kAXMenuBarAttribute)
128+
}
129+
118130
var visibleChildren: [AXUIElement] {
119131
(try? copyValue(key: kAXVisibleChildrenAttribute)) ?? []
120132
}
121133

122-
func child(identifier: String) -> AXUIElement? {
134+
func child(
135+
identifier: String? = nil,
136+
title: String? = nil,
137+
role: String? = nil
138+
) -> AXUIElement? {
123139
for child in children {
124-
if child.identifier == identifier { return child }
125-
if let target = child.child(identifier: identifier) { return target }
140+
let match = {
141+
if let identifier, child.identifier != identifier { return false }
142+
if let title, child.title != title { return false }
143+
if let role, child.role != role { return false }
144+
return true
145+
}()
146+
if match { return child }
147+
}
148+
for child in children {
149+
if let target = child.child(
150+
identifier: identifier,
151+
title: title,
152+
role: role
153+
) { return target }
126154
}
127155
return nil
128156
}

Core/Sources/Environment/Environment.swift

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AppKit
33
import AXExtension
44
import CopilotService
55
import Foundation
6+
import Logger
67

78
public struct NoAccessToAccessibilityAPIError: Error, LocalizedError {
89
public var errorDescription: String? {
@@ -120,27 +121,62 @@ public enum Environment {
120121
else { return }
121122
let bundleName = Bundle.main
122123
.object(forInfoDictionaryKey: "EXTENSION_BUNDLE_NAME") as! String
124+
125+
await Task.yield()
123126

124-
/// check if menu is open, if not, click the menu item.
125-
let appleScript = """
126-
tell application "System Events"
127-
set theprocs to every process whose unix id is \(activeXcode.processIdentifier)
128-
repeat with proc in theprocs
129-
set the frontmost of proc to true
130-
tell proc
131-
repeat with theMenu in menus of menu bar 1
132-
set theValue to value of attribute "AXVisibleChildren" of theMenu
133-
if theValue is not {} then
134-
return
135-
end if
136-
end repeat
137-
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
138-
end tell
139-
end repeat
140-
end tell
141-
"""
127+
if UserDefaults.shared.value(for: \.triggerActionWithAccessibilityAPI) {
128+
if !activeXcode.isActive { activeXcode.activate() }
129+
let app = AXUIElementCreateApplication(activeXcode.processIdentifier)
142130

143-
try await runAppleScript(appleScript)
131+
if let editorMenu = app.menuBar?.child(title: "Editor"),
132+
let commandMenu = editorMenu.child(title: bundleName)
133+
{
134+
if let button = commandMenu.child(title: name, role: "AXMenuItem") {
135+
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
136+
if error != AXError.success {
137+
Logger.service
138+
.error("Trigger action \(name) failed: \(error.localizedDescription)")
139+
throw error
140+
}
141+
}
142+
} else if let commandMenu = app.menuBar?.child(title: bundleName),
143+
let button = commandMenu.child(title: name, role: "AXMenuItem")
144+
{
145+
let error = AXUIElementPerformAction(button, kAXPressAction as CFString)
146+
if error != AXError.success {
147+
Logger.service
148+
.error("Trigger action \(name) failed: \(error.localizedDescription)")
149+
throw error
150+
}
151+
}
152+
} else {
153+
/// check if menu is open, if not, click the menu item.
154+
let appleScript = """
155+
tell application "System Events"
156+
set theprocs to every process whose unix id is \(activeXcode.processIdentifier)
157+
repeat with proc in theprocs
158+
set the frontmost of proc to true
159+
tell proc
160+
repeat with theMenu in menus of menu bar 1
161+
set theValue to value of attribute "AXVisibleChildren" of theMenu
162+
if theValue is not {} then
163+
return
164+
end if
165+
end repeat
166+
click menu item "\(name)" of menu 1 of menu item "\(bundleName)" of menu 1 of menu bar item "Editor" of menu bar 1
167+
end tell
168+
end repeat
169+
end tell
170+
"""
171+
172+
do {
173+
try await runAppleScript(appleScript)
174+
} catch {
175+
Logger.service
176+
.error("Trigger action \(name) failed: \(error.localizedDescription)")
177+
throw error
178+
}
179+
}
144180
}
145181

146182
public static var makeXcodeActive: () async throws -> Void = {

Core/Sources/Preferences/Keys.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ public enum FeatureFlags {
281281
public let defaultValue = true
282282
public let key = "FeatureFlag-UseCustomScrollViewWorkaround"
283283
}
284+
285+
public struct TriggerActionWithAccessibilityAPI: UserDefaultPreferenceKey {
286+
public let defaultValue = true
287+
public let key = "FeatureFlag-TriggerActionWithAccessibilityAPI"
288+
}
284289
}
285290

286291
public extension UserDefaultPreferenceKeys {
@@ -289,4 +294,5 @@ public extension UserDefaultPreferenceKeys {
289294
var runNodeWithInteractiveLoggedInShell: FeatureFlags
290295
.RunNodeWithInteractiveLoggedInShell { .init() }
291296
var useCustomScrollViewWorkaround: FeatureFlags.UseCustomScrollViewWorkaround { .init() }
297+
var triggerActionWithAccessibilityAPI: FeatureFlags.TriggerActionWithAccessibilityAPI { .init() }
292298
}

ExtensionService/AppDelegate.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ServiceManagement
88
import SwiftUI
99
import UpdateChecker
1010
import UserNotifications
11+
import Environment
1112

1213
let bundleIdentifierBase = Bundle.main
1314
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String
@@ -27,10 +28,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
2728

2829
func applicationDidFinishLaunching(_: Notification) {
2930
if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return }
30-
3131
_ = GraphicalUserInterfaceController.shared
3232
_ = RealtimeSuggestionController.shared
33-
UserDefaults.setupDefaultSettings()
3433
setupQuitOnUpdate()
3534
setupQuitOnUserTerminated()
3635
xpcListener = setupXPCListener()

0 commit comments

Comments
 (0)