1+ import ActiveApplicationMonitor
12import AppKit
3+ import AXNotificationStream
24import DisplayLink
35import Environment
46import QuartzCore
57import SwiftUI
68import XPCShared
79
810/// Present a tiny dot next to mouse cursor if real-time suggestion is enabled.
11+ @MainActor
912final 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}
0 commit comments