1+ import Cocoa
2+ import Foundation
3+ import ApplicationServices
4+
5+ protocol WindowInspector : AnyObject {
6+ var documentURL : URL ? { get }
7+ var workspaceURL : URL ? { get }
8+ var projectURL : URL ? { get }
9+ var isMainWorkWindow : Bool { get }
10+
11+ func refresh( )
12+ }
13+
14+ protocol EditorStrategy {
15+ var displayName : String { get }
16+ var bundleIdentifier : String { get }
17+
18+ func shouldMonitor( _ app: NSRunningApplication ) -> Bool
19+ func createWindowInspector( processId: pid_t , windowElement: AXUIElement ) -> WindowInspector ?
20+ }
21+
22+ struct AppInstance {
23+ let app : NSRunningApplication
24+ let strategy : EditorStrategy
25+ var windowInspector : WindowInspector ?
26+
27+ var displayName : String {
28+ strategy. displayName
29+ }
30+
31+ var processId : pid_t {
32+ app. processIdentifier
33+ }
34+ }
35+
36+ @MainActor
37+ class AppMonitor : ObservableObject {
38+ @Published var monitoredApps : [ String : AppInstance ] = [ : ]
39+ @Published var activeApp : AppInstance ?
40+ @Published var accessibilityPermissionGranted : Bool = false
41+
42+ private let strategies : [ String : EditorStrategy ]
43+ private let workspace = NSWorkspace . shared
44+
45+ init ( strategies: [ EditorStrategy ] ) {
46+ self . strategies = Dictionary ( uniqueKeysWithValues: strategies. map { ( $0. bundleIdentifier, $0) } )
47+
48+ checkAccessibilityPermission ( )
49+ setupMonitoring ( )
50+ findExistingApps ( )
51+ startPeriodicCheck ( )
52+ }
53+
54+ private func startPeriodicCheck( ) {
55+ Timer . scheduledTimer ( withTimeInterval: 2.0 , repeats: true ) { [ weak self] _ in
56+ Task { @MainActor in
57+ self ? . refreshActiveState ( )
58+ }
59+ }
60+ }
61+
62+ @MainActor
63+ public func refreshActiveState( ) {
64+ let frontmostApp = workspace. frontmostApplication
65+ let currentlyActivePID = frontmostApp? . processIdentifier ?? - 1
66+
67+ print ( " 🔄 Periodic check - Frontmost: \( frontmostApp? . localizedName ?? " None " ) (PID: \( currentlyActivePID) ) " )
68+
69+ let shouldBeActive = monitoredApps. values. first { instance in
70+ instance. processId == currentlyActivePID
71+ }
72+
73+ if shouldBeActive? . processId != activeApp? . processId {
74+ print ( " 🔄 State correction needed! " )
75+ print ( " - Should be active: \( shouldBeActive? . processId ?? - 1 ) " )
76+ print ( " - Currently tracked as active: \( activeApp? . processId ?? - 1 ) " )
77+
78+ activeApp = shouldBeActive
79+
80+ if let newActive = shouldBeActive, accessibilityPermissionGranted {
81+ monitorAppWindows ( for: newActive)
82+ }
83+ } else if let currentActive = shouldBeActive, accessibilityPermissionGranted {
84+ monitorAppWindows ( for: currentActive)
85+ }
86+ }
87+
88+ private func setupMonitoring( ) {
89+ NotificationCenter . default. addObserver (
90+ forName: NSWorkspace . didActivateApplicationNotification,
91+ object: nil ,
92+ queue: . main
93+ ) { [ weak self] notification in
94+ self ? . handleApplicationActivated ( notification)
95+ }
96+
97+ NotificationCenter . default. addObserver (
98+ forName: NSWorkspace . didTerminateApplicationNotification,
99+ object: nil ,
100+ queue: . main
101+ ) { [ weak self] notification in
102+ self ? . handleApplicationTerminated ( notification)
103+ }
104+ }
105+
106+ private func checkAccessibilityPermission( ) {
107+ accessibilityPermissionGranted = AXIsProcessTrusted ( )
108+ print ( " 🔐 Accessibility Permission: \( accessibilityPermissionGranted ? " ✅ Granted " : " ❌ Not Granted " ) " )
109+
110+ if !accessibilityPermissionGranted {
111+ print ( " 💡 Request accessibility permission... " )
112+ requestAccessibilityPermission ( )
113+ }
114+ }
115+
116+ private func requestAccessibilityPermission( ) {
117+ let options = [ kAXTrustedCheckOptionPrompt. takeUnretainedValue ( ) : true ]
118+ AXIsProcessTrustedWithOptions ( options as CFDictionary )
119+ }
120+
121+ private func findExistingApps( ) {
122+ let runningApps = workspace. runningApplications
123+
124+ for app in runningApps {
125+ if let strategy = findStrategy ( for: app) {
126+ let instance = AppInstance ( app: app, strategy: strategy, windowInspector: nil )
127+ monitoredApps [ makeKey ( for: app) ] = instance
128+ print ( " 📱 Found \( strategy. displayName) : PID \( app. processIdentifier) " )
129+ }
130+ }
131+
132+ let frontmostApp = workspace. frontmostApplication
133+ print ( " 🔍 Current frontmost app: \( frontmostApp? . localizedName ?? " Unknown " ) (PID: \( frontmostApp? . processIdentifier ?? - 1 ) ) " )
134+
135+ activeApp = monitoredApps. values. first { instance in
136+ instance. processId == frontmostApp? . processIdentifier
137+ }
138+
139+ if let active = activeApp {
140+ print ( " ✅ Active app: \( active. displayName) PID \( active. processId) " )
141+ }
142+
143+ if accessibilityPermissionGranted, let activeApp = activeApp {
144+ Task { @MainActor in
145+ monitorAppWindows ( for: activeApp)
146+ }
147+ }
148+ }
149+
150+ @MainActor
151+ private func monitorAppWindows( for appInstance: AppInstance ) {
152+ guard accessibilityPermissionGranted else {
153+ print ( " ❌ Cannot monitor windows - no accessibility permission " )
154+ return
155+ }
156+
157+ let axApp = AXUIElementCreateApplication ( appInstance. processId)
158+
159+ var focusedWindowElement : AnyObject ?
160+ let result = AXUIElementCopyAttributeValue ( axApp, kAXFocusedWindowAttribute as CFString , & focusedWindowElement)
161+
162+ guard result == . success, let windowElement = focusedWindowElement else {
163+ print ( " ❌ Could not get focused window from \( appInstance. displayName) " )
164+ return
165+ }
166+
167+ guard CFGetTypeID ( windowElement) == AXUIElementGetTypeID ( ) else {
168+ print ( " ❌ Focused window element is not an AXUIElement " )
169+ return
170+ }
171+
172+ let axWindowElement = windowElement as! AXUIElement
173+
174+ print ( " 🪟 Found focused window for \( appInstance. displayName) , creating inspector... " )
175+
176+ if let windowInspector = appInstance. strategy. createWindowInspector (
177+ processId: appInstance. processId,
178+ windowElement: axWindowElement
179+ ) {
180+ if windowInspector. isMainWorkWindow {
181+ print ( " ✅ Found main work window for \( appInstance. displayName) " )
182+ var updatedInstance = appInstance
183+ updatedInstance. windowInspector = windowInspector
184+ monitoredApps [ makeKey ( for: appInstance. app) ] = updatedInstance
185+
186+ if activeApp? . processId == appInstance. processId {
187+ activeApp = updatedInstance
188+ }
189+ } else {
190+ print ( " 📝 Found other window for \( appInstance. displayName) (not main work window) " )
191+ }
192+ }
193+ }
194+
195+ private func findStrategy( for app: NSRunningApplication ) -> EditorStrategy ? {
196+ return strategies. values. first { strategy in
197+ strategy. shouldMonitor ( app)
198+ }
199+ }
200+
201+ private func makeKey( for app: NSRunningApplication ) -> String {
202+ return " \( app. bundleIdentifier ?? " unknown " ) _ \( app. processIdentifier) "
203+ }
204+
205+ private func handleApplicationActivated( _ notification: Notification ) {
206+ guard let app = notification. userInfo ? [ NSWorkspace . applicationUserInfoKey] as? NSRunningApplication else { return }
207+
208+ print ( " 📱 App activated: \( app. localizedName ?? " Unknown " ) (PID: \( app. processIdentifier) ) " )
209+
210+ DispatchQueue . main. async { [ weak self] in
211+ guard let self = self else { return }
212+
213+ if let strategy = self . findStrategy ( for: app) {
214+ print ( " 🔄 \( strategy. displayName) activated: PID \( app. processIdentifier) " )
215+
216+ let key = self . makeKey ( for: app)
217+ let instance = AppInstance ( app: app, strategy: strategy, windowInspector: nil )
218+ self . monitoredApps [ key] = instance
219+ self . activeApp = instance
220+ } else {
221+ if let previousActive = self . activeApp {
222+ print ( " 📱 \( app. localizedName ?? " Unknown app " ) became active, \( previousActive. displayName) (PID: \( previousActive. processId) ) backgrounded " )
223+ self . activeApp = nil
224+ }
225+ }
226+ }
227+ }
228+
229+ private func handleApplicationTerminated( _ notification: Notification ) {
230+ guard let app = notification. userInfo ? [ NSWorkspace . applicationUserInfoKey] as? NSRunningApplication else { return }
231+
232+ if findStrategy ( for: app) != nil {
233+ let key = makeKey ( for: app)
234+ print ( " ❌ App terminated: \( app. localizedName ?? " Unknown " ) PID \( app. processIdentifier) " )
235+ monitoredApps. removeValue ( forKey: key)
236+
237+ if activeApp? . processId == app. processIdentifier {
238+ activeApp = monitoredApps. values. first { $0. app. isActive }
239+ }
240+ }
241+ }
242+ }
0 commit comments