Skip to content

Commit 3f33162

Browse files
committed
Simplify code with ActiveApplicationMonitor
1 parent 645a554 commit 3f33162

3 files changed

Lines changed: 41 additions & 101 deletions

File tree

Core/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public final class ActiveApplicationMonitor {
2626
}
2727

2828
public static var activeApplication: NSRunningApplication? { shared.activeApplication }
29+
30+
public static var activeXcode: NSRunningApplication? {
31+
if activeApplication?.bundleIdentifier == "com.apple.dt.Xcode" {
32+
return activeApplication
33+
}
34+
return nil
35+
}
2936

3037
public static func createStream() -> AsyncStream<NSRunningApplication?> {
3138
.init { continuation in

Core/Sources/Service/Environment.swift

Lines changed: 27 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ActiveApplicationMonitor
12
import AppKit
23
import CopilotService
34
import Foundation
@@ -17,31 +18,8 @@ struct FailedToFetchFileURLError: Error, LocalizedError {
1718
enum Environment {
1819
static var now = { Date() }
1920

20-
static var runningXcodes: () async -> [NSRunningApplication] = {
21-
var xcodes = [NSRunningApplication]()
22-
var retryCount = 0
23-
// Sometimes runningApplications returns 0 items.
24-
while xcodes.isEmpty, retryCount < 3 {
25-
xcodes = NSRunningApplication
26-
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
27-
try? await Task.sleep(nanoseconds: 1_000_000)
28-
retryCount += 1
29-
}
30-
return xcodes
31-
}
32-
3321
static var isXcodeActive: () async -> Bool = {
34-
var activeXcodes = [NSRunningApplication]()
35-
var retryCount = 0
36-
// Sometimes runningApplications returns 0 items.
37-
while activeXcodes.isEmpty, retryCount < 3 {
38-
activeXcodes = NSRunningApplication
39-
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
40-
.filter(\.isActive)
41-
try? await Task.sleep(nanoseconds: 1_000_000)
42-
retryCount += 1
43-
}
44-
return !activeXcodes.isEmpty
22+
ActiveApplicationMonitor.activeXcode != nil
4523
}
4624

4725
static var frontmostXcodeWindowIsEditor: () async -> Bool = {
@@ -93,58 +71,37 @@ enum Environment {
9371
}
9472

9573
static var fetchCurrentFileURL: () async throws -> URL = {
96-
var activeXcodes = [NSRunningApplication]()
97-
var retryCount = 0
98-
// Sometimes runningApplications returns 0 items.
99-
while activeXcodes.isEmpty, retryCount < 5 {
100-
activeXcodes = NSRunningApplication
101-
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
102-
.sorted { lhs, _ in
103-
if lhs.isActive { return true }
104-
return false
105-
}
106-
if retryCount > 0 { try await Task.sleep(nanoseconds: 10_000_000) }
107-
retryCount += 1
74+
guard let xcode = ActiveApplicationMonitor.activeXcode else {
75+
throw FailedToFetchFileURLError()
10876
}
10977

11078
// fetch file path of the frontmost window of Xcode through Accessability API.
111-
for xcode in activeXcodes {
112-
let application = AXUIElementCreateApplication(xcode.processIdentifier)
113-
do {
114-
let frontmostWindow = try application.copyValue(
115-
key: kAXFocusedWindowAttribute,
116-
ofType: AXUIElement.self
117-
)
118-
var path = try? frontmostWindow.copyValue(
119-
key: kAXDocumentAttribute,
120-
ofType: String?.self
121-
)
122-
if path == nil {
123-
for window in try application.copyValue(
124-
key: kAXWindowsAttribute,
125-
ofType: [AXUIElement].self
126-
) {
127-
path = try? window.copyValue(
128-
key: kAXDocumentAttribute,
129-
ofType: String?.self
130-
)
131-
if path != nil { break }
132-
}
133-
}
134-
if let path = path?.removingPercentEncoding {
135-
let url = URL(
136-
fileURLWithPath: path
137-
.replacingOccurrences(of: "file://", with: "")
138-
)
139-
return url
140-
}
141-
} catch {
142-
if let axError = error as? AXError, axError == .apiDisabled {
143-
throw NoAccessToAccessibilityAPIError()
79+
let application = AXUIElementCreateApplication(xcode.processIdentifier)
80+
do {
81+
let frontmostWindow: AXUIElement = try application
82+
.copyValue(key: kAXFocusedWindowAttribute)
83+
var path: String? = try? frontmostWindow.copyValue(key: kAXDocumentAttribute)
84+
if path == nil {
85+
for window in try application.copyValue(
86+
key: kAXWindowsAttribute,
87+
ofType: [AXUIElement].self
88+
) {
89+
path = try? window.copyValue(key: kAXDocumentAttribute)
90+
if path != nil { break }
14491
}
14592
}
93+
if let path = path?.removingPercentEncoding {
94+
let url = URL(
95+
fileURLWithPath: path
96+
.replacingOccurrences(of: "file://", with: "")
97+
)
98+
return url
99+
}
100+
} catch {
101+
if let axError = error as? AXError, axError == .apiDisabled {
102+
throw NoAccessToAccessibilityAPIError()
103+
}
146104
}
147-
148105
throw FailedToFetchFileURLError()
149106
}
150107

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ActiveApplicationMonitor
12
import AppKit
23
import CGEventObserver
34
import Foundation
@@ -6,8 +7,6 @@ import XPCShared
67

78
public actor RealtimeSuggestionController {
89
public static let shared = RealtimeSuggestionController()
9-
10-
private var listeners = Set<AnyHashable>()
1110
var eventObserver: CGEventObserverType = CGEventObserver(eventsOfInterest: [
1211
.keyUp,
1312
.keyDown,
@@ -22,41 +21,20 @@ public actor RealtimeSuggestionController {
2221
}
2322

2423
private init() {
25-
// Start the auto trigger if Xcode is running.
26-
Task {
27-
for xcode in await Environment.runningXcodes() {
28-
await start(by: xcode.processIdentifier)
29-
}
30-
let sequence = NSWorkspace.shared.notificationCenter
31-
.notifications(named: NSWorkspace.didActivateApplicationNotification)
32-
for await notification in sequence {
33-
try Task.checkCancellation()
34-
guard let app = notification
35-
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
36-
else { continue }
37-
guard app.bundleIdentifier == "com.apple.dt.Xcode" else { continue }
38-
await start(by: app.processIdentifier)
39-
}
40-
}
41-
42-
// Remove listener if Xcode is terminated.
4324
Task {
44-
let sequence = NSWorkspace.shared.notificationCenter
45-
.notifications(named: NSWorkspace.didTerminateApplicationNotification)
46-
for await notification in sequence {
25+
for await _ in ActiveApplicationMonitor.createStream() {
4726
try Task.checkCancellation()
48-
guard let app = notification
49-
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
50-
else { continue }
51-
guard app.bundleIdentifier == "com.apple.dt.Xcode" else { continue }
52-
await stop(by: app.processIdentifier)
27+
if ActiveApplicationMonitor.activeXcode != nil {
28+
await start(by: 1)
29+
} else {
30+
await stop(by: 1)
31+
}
5332
}
5433
}
5534
}
5635

5736
private func start(by listener: AnyHashable) {
5837
os_log(.info, "Add auto trigger listener: %@.", listener as CVarArg)
59-
listeners.insert(listener)
6038

6139
if task == nil {
6240
task = Task { [stream = eventObserver.stream] in
@@ -72,8 +50,6 @@ public actor RealtimeSuggestionController {
7250

7351
private func stop(by listener: AnyHashable) {
7452
os_log(.info, "Remove auto trigger listener: %@.", listener as CVarArg)
75-
listeners.remove(listener)
76-
guard listeners.isEmpty else { return }
7753
os_log(.info, "Auto trigger is stopped.")
7854
task?.cancel()
7955
task = nil

0 commit comments

Comments
 (0)