Skip to content

Commit 61fbc73

Browse files
committed
Replace display link with AXObserver
1 parent 5a481e5 commit 61fbc73

File tree

3 files changed

+89
-86
lines changed

3 files changed

+89
-86
lines changed
Lines changed: 83 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import ActiveApplicationMonitor
12
import AppKit
3+
import AXNotificationStream
24
import DisplayLink
35
import Environment
46
import QuartzCore
57
import SwiftUI
68
import XPCShared
79

810
/// Present a tiny dot next to mouse cursor if real-time suggestion is enabled.
11+
@MainActor
912
final class RealtimeSuggestionIndicatorController {
1013
class IndicatorContentViewModel: ObservableObject {
1114
@Published var isPrefetching = false
1215
private var prefetchTask: Task<Void, Error>?
13-
16+
1417
@MainActor
1518
func prefetch() {
1619
prefetchTask?.cancel()
@@ -63,6 +66,9 @@ final class RealtimeSuggestionIndicatorController {
6366

6467
private let viewModel = IndicatorContentViewModel()
6568
private var userDefaultsObserver = UserDefaultsObserver()
69+
private var windowChangeObservationTask: Task<Void, Error>?
70+
private var activeApplicationMonitorTask: Task<Void, Error>?
71+
private var xcode: NSRunningApplication?
6672
var isObserving = false {
6773
didSet {
6874
Task {
@@ -71,8 +77,6 @@ final class RealtimeSuggestionIndicatorController {
7177
}
7278
}
7379

74-
private var displayLinkTask: Task<Void, Never>?
75-
7680
@MainActor
7781
lazy var window = {
7882
let it = NSWindow(
@@ -92,35 +96,34 @@ final class RealtimeSuggestionIndicatorController {
9296
return it
9397
}()
9498

95-
init() {
96-
Task {
97-
let sequence = NSWorkspace.shared.notificationCenter
98-
.notifications(named: NSWorkspace.didActivateApplicationNotification)
99-
for await notification in sequence {
100-
guard let app = notification
101-
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
102-
else { continue }
103-
guard app.bundleIdentifier == "com.apple.dt.Xcode" else { continue }
104-
await updateIndicatorVisibility()
105-
}
106-
}
107-
108-
Task {
109-
let sequence = NSWorkspace.shared.notificationCenter
110-
.notifications(named: NSWorkspace.didDeactivateApplicationNotification)
111-
for await notification in sequence {
112-
guard let app = notification
113-
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication
114-
else { continue }
115-
guard app.bundleIdentifier == "com.apple.dt.Xcode" else { continue }
116-
await updateIndicatorVisibility()
99+
nonisolated init() {
100+
Task { @MainActor in
101+
activeApplicationMonitorTask = Task { [weak self] in
102+
var previousApp: NSRunningApplication?
103+
for await app in ActiveApplicationMonitor.createStream() {
104+
guard let self else { return }
105+
try Task.checkCancellation()
106+
defer { previousApp = app }
107+
if let app = ActiveApplicationMonitor.activeXcode {
108+
if app != previousApp {
109+
windowChangeObservationTask?.cancel()
110+
windowChangeObservationTask = nil
111+
self.observeXcodeWindowChangeIfNeeded(app)
112+
}
113+
await self.updateIndicatorVisibility()
114+
self.updateIndicatorLocation()
115+
} else {
116+
await self.updateIndicatorVisibility()
117+
}
118+
}
117119
}
118120
}
119121

120-
Task {
122+
Task { @MainActor in
121123
userDefaultsObserver.onChange = { [weak self] in
122124
Task { [weak self] in
123125
await self?.updateIndicatorVisibility()
126+
self?.updateIndicatorLocation()
124127
}
125128
}
126129
UserDefaults.shared.addObserver(
@@ -132,74 +135,77 @@ final class RealtimeSuggestionIndicatorController {
132135
}
133136
}
134137

138+
private func observeXcodeWindowChangeIfNeeded(_ app: NSRunningApplication) {
139+
xcode = app
140+
guard windowChangeObservationTask == nil else { return }
141+
windowChangeObservationTask = Task {
142+
let notifications = AXNotificationStream(
143+
app: app,
144+
notificationNames:
145+
kAXMovedNotification,
146+
kAXResizedNotification,
147+
kAXMainWindowChangedNotification,
148+
kAXFocusedWindowChangedNotification,
149+
kAXFocusedUIElementChangedNotification,
150+
kAXSelectedTextChangedNotification
151+
)
152+
for await _ in notifications {
153+
try Task.checkCancellation()
154+
updateIndicatorLocation()
155+
}
156+
}
157+
}
158+
135159
private func updateIndicatorVisibility() async {
136160
let isVisible = await {
137161
let isOn = UserDefaults.shared.bool(forKey: SettingsKey.realtimeSuggestionToggle)
138162
let isXcodeActive = await Environment.isXcodeActive()
139-
return isOn && isXcodeActive && isObserving
163+
return isOn && isXcodeActive
140164
}()
141165

142-
await { @MainActor in
143-
guard window.isVisible != isVisible else { return }
144-
if isVisible {
145-
if displayLinkTask == nil {
146-
displayLinkTask = Task {
147-
for await _ in DisplayLink.createStream() {
148-
self.updateIndicatorLocation()
149-
}
150-
}
151-
}
152-
} else {
153-
displayLinkTask?.cancel()
154-
displayLinkTask = nil
155-
}
156-
window.setIsVisible(isVisible)
157-
}()
166+
guard window.isVisible != isVisible else { return }
167+
window.setIsVisible(isVisible)
158168
}
159169

160170
private func updateIndicatorLocation() {
161-
Task { @MainActor in
162-
if !window.isVisible {
163-
return
164-
}
171+
if !window.isVisible {
172+
return
173+
}
165174

166-
if let activeXcode = NSRunningApplication
167-
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
168-
.first(where: \.isActive)
175+
if let activeXcode = NSRunningApplication
176+
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
177+
.first(where: \.isActive)
178+
{
179+
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
180+
if let focusElement: AXUIElement = try? application
181+
.copyValue(key: kAXFocusedUIElementAttribute),
182+
let selectedRange: AXValue = try? focusElement
183+
.copyValue(key: kAXSelectedTextRangeAttribute),
184+
let rect: AXValue = try? focusElement.copyParameterizedValue(
185+
key: kAXBoundsForRangeParameterizedAttribute,
186+
parameters: selectedRange
187+
)
169188
{
170-
let application = AXUIElementCreateApplication(activeXcode.processIdentifier)
171-
if let focusElement: AXUIElement = try? application
172-
.copyValue(key: kAXFocusedUIElementAttribute),
173-
let selectedRange: AXValue = try? focusElement
174-
.copyValue(key: kAXSelectedTextRangeAttribute),
175-
let rect: AXValue = try? focusElement.copyParameterizedValue(
176-
key: kAXBoundsForRangeParameterizedAttribute,
177-
parameters: selectedRange
189+
var frame: CGRect = .zero
190+
let found = AXValueGetValue(rect, .cgRect, &frame)
191+
let screen = NSScreen.screens.first
192+
if found, let screen {
193+
frame.origin = .init(
194+
x: frame.maxX + 2,
195+
y: screen.frame.height - frame.minY - 4
178196
)
179-
{
180-
var frame: CGRect = .zero
181-
let found = AXValueGetValue(rect, .cgRect, &frame)
182-
let screen = NSScreen.screens.first
183-
if found, let screen {
184-
frame.origin = .init(
185-
x: frame.maxX + 2,
186-
y: screen.frame.height - frame.minY - 4
187-
)
188-
frame.size = .init(width: 10, height: 10)
189-
window.alphaValue = 1
190-
window.setFrame(frame, display: false, animate: true)
191-
return
192-
}
197+
frame.size = .init(width: 10, height: 10)
198+
window.alphaValue = 1
199+
window.setFrame(frame, display: false)
200+
return
193201
}
194202
}
195-
196-
window.alphaValue = 0
197203
}
204+
205+
window.alphaValue = 0
198206
}
199207

200208
func triggerPrefetchAnimation() {
201-
Task { @MainActor in
202-
viewModel.prefetch()
203-
}
209+
viewModel.prefetch()
204210
}
205211
}

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ public actor RealtimeSuggestionController {
4444
}
4545
}
4646
}
47-
if eventObserver.activateIfPossible() {
48-
realtimeSuggestionIndicatorController.isObserving = true
49-
}
47+
eventObserver.activateIfPossible()
5048
}
5149

5250
private func stop(by listener: AnyHashable) {
@@ -55,7 +53,6 @@ public actor RealtimeSuggestionController {
5553
task?.cancel()
5654
task = nil
5755
eventObserver.deactivate()
58-
realtimeSuggestionIndicatorController.isObserving = false
5956
}
6057

6158
func handleKeyboardEvent(event: CGEvent) async {
@@ -107,14 +104,14 @@ public actor RealtimeSuggestionController {
107104
.value(forKey: SettingsKey.realtimeSuggestionDebounce) as? Double
108105
?? 0.7
109106
) * 1_000_000_000))
110-
107+
111108
guard UserDefaults.shared.bool(forKey: SettingsKey.realtimeSuggestionToggle)
112109
else { return }
113-
110+
114111
if Task.isCancelled { return }
115-
112+
116113
os_log(.info, "Prefetch suggestions.")
117-
114+
118115
await realtimeSuggestionIndicatorController.triggerPrefetchAnimation()
119116
do {
120117
try await Environment.triggerAction("Prefetch Suggestions")

Core/Sources/Service/SuggestionCommandHandler/CommentBaseCommandHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ struct CommentBaseCommandHandler: SuggestionCommandHanlder {
173173
// If there is a suggestion available, call another command to present it.
174174
guard !suggestions.isEmpty else { return nil }
175175
try await Environment.triggerAction("Real-time Suggestions")
176-
GraphicalUserInterfaceController.shared.realtimeSuggestionIndicatorController
176+
await GraphicalUserInterfaceController.shared.realtimeSuggestionIndicatorController
177177
.triggerPrefetchAnimation()
178178

179179
return nil

0 commit comments

Comments
 (0)