1+ import ApplicationServices
2+ import Foundation
3+
4+ /// Window inspector for individual Xcode windows - follows CopilotForXcode patterns
5+ class XcodeWindowInspector {
6+ let processIdentifier : pid_t
7+ let windowElement : AXUIElement
8+
9+ @Published var documentURL : URL ?
10+ @Published var workspaceURL : URL ?
11+ @Published var projectRootURL : URL ?
12+
13+ init ( processIdentifier: pid_t , windowElement: AXUIElement ) {
14+ self . processIdentifier = processIdentifier
15+ self . windowElement = windowElement
16+
17+ // Initial extraction
18+ refresh ( )
19+ }
20+
21+ /// Refresh all URLs from the window
22+ func refresh( ) {
23+ documentURL = Self . extractDocumentURL ( from: windowElement)
24+ workspaceURL = Self . extractWorkspaceURL ( from: windowElement)
25+ projectRootURL = Self . extractProjectURL ( workspaceURL: workspaceURL, documentURL: documentURL)
26+
27+ print ( " 📄 Window refresh: " )
28+ print ( " 📁 Document: \( documentURL? . lastPathComponent ?? " None " ) " )
29+ print ( " 📦 Workspace: \( workspaceURL? . lastPathComponent ?? " None " ) " )
30+ print ( " 🏗️ Project: \( projectRootURL? . lastPathComponent ?? " None " ) " )
31+ }
32+
33+ // MARK: - URL Extraction (following CopilotForXcode patterns)
34+
35+ /// Extract document URL from window element - mirrors CopilotForXcode's extractDocumentURL
36+ static func extractDocumentURL( from windowElement: AXUIElement ) -> URL ? {
37+ // Fetch file path of the frontmost window of Xcode through Accessibility API
38+ var documentValue : AnyObject ?
39+ let result = AXUIElementCopyAttributeValue ( windowElement, kAXDocumentAttribute as CFString , & documentValue)
40+
41+ guard result == . success, let path = documentValue as? String else {
42+ return nil
43+ }
44+
45+ // Remove percent encoding and clean up path
46+ guard let cleanPath = path. removingPercentEncoding else { return nil }
47+
48+ let url = URL ( fileURLWithPath: cleanPath. replacingOccurrences ( of: " file:// " , with: " " ) )
49+ return adjustFileURL ( url)
50+ }
51+
52+ /// Extract workspace URL from window children - mirrors CopilotForXcode's extractWorkspaceURL
53+ static func extractWorkspaceURL( from windowElement: AXUIElement ) -> URL ? {
54+ var children : AnyObject ?
55+ let result = AXUIElementCopyAttributeValue ( windowElement, kAXChildrenAttribute as CFString , & children)
56+
57+ guard result == . success, let childrenArray = children as? [ AXUIElement ] else {
58+ return nil
59+ }
60+
61+ // Look through children for path descriptions
62+ for child in childrenArray {
63+ var description : AnyObject ?
64+ let descResult = AXUIElementCopyAttributeValue ( child, kAXDescriptionAttribute as CFString , & description)
65+
66+ guard descResult == . success, let desc = description as? String else { continue }
67+
68+ // Check if this looks like a path (starts with "/" and is longer than 1 character)
69+ if desc. starts ( with: " / " ) , desc. count > 1 {
70+ let trimmedPath = desc. trimmingCharacters ( in: . newlines)
71+ return URL ( fileURLWithPath: trimmedPath)
72+ }
73+ }
74+
75+ return nil
76+ }
77+
78+ /// Extract project root URL - mirrors CopilotForXcode's extractProjectURL
79+ static func extractProjectURL( workspaceURL: URL ? , documentURL: URL ? ) -> URL ? {
80+ guard var currentURL = workspaceURL ?? documentURL else { return nil }
81+
82+ var firstDirectoryURL : URL ?
83+ var lastGitDirectoryURL : URL ?
84+
85+ while currentURL. pathComponents. count > 1 {
86+ defer { currentURL. deleteLastPathComponent ( ) }
87+
88+ guard FileManager . default. fileExists ( atPath: currentURL. path) else { continue }
89+
90+ var isDirectory : ObjCBool = false
91+ FileManager . default. fileExists ( atPath: currentURL. path, isDirectory: & isDirectory)
92+ guard isDirectory. boolValue else { continue }
93+
94+ // Skip Xcode-specific directories
95+ guard currentURL. pathExtension != " xcodeproj " ,
96+ currentURL. pathExtension != " xcworkspace " ,
97+ currentURL. pathExtension != " playground " else { continue }
98+
99+ if firstDirectoryURL == nil {
100+ firstDirectoryURL = currentURL
101+ }
102+
103+ // Check for .git directory
104+ let gitURL = currentURL. appendingPathComponent ( " .git " )
105+ var gitIsDirectory : ObjCBool = false
106+
107+ if FileManager . default. fileExists ( atPath: gitURL. path, isDirectory: & gitIsDirectory) {
108+ if gitIsDirectory. boolValue {
109+ lastGitDirectoryURL = currentURL
110+ } else if let gitContent = try ? String ( contentsOf: gitURL) {
111+ // Check for git worktree
112+ if !gitContent. hasPrefix ( " gitdir: ../ " ) && gitContent. contains ( " /.git/worktrees/ " ) {
113+ lastGitDirectoryURL = currentURL
114+ }
115+ }
116+ }
117+ }
118+
119+ return lastGitDirectoryURL ?? firstDirectoryURL ?? workspaceURL
120+ }
121+
122+ /// Adjust file URL for special cases - mirrors CopilotForXcode's adjustFileURL
123+ static func adjustFileURL( _ url: URL ) -> URL {
124+ if url. pathExtension == " playground " ,
125+ FileManager . default. fileExists ( atPath: url. path) {
126+ var isDirectory : ObjCBool = false
127+ FileManager . default. fileExists ( atPath: url. path, isDirectory: & isDirectory)
128+ if isDirectory. boolValue {
129+ return url. appendingPathComponent ( " Contents.swift " )
130+ }
131+ }
132+ return url
133+ }
134+
135+ /// Check if this is a workspace window
136+ var isWorkspaceWindow : Bool {
137+ var identifier : AnyObject ?
138+ let result = AXUIElementCopyAttributeValue ( windowElement, kAXIdentifierAttribute as CFString , & identifier)
139+
140+ if result == . success, let id = identifier as? String {
141+ return id == " Xcode.WorkspaceWindow "
142+ }
143+
144+ return false
145+ }
146+ }
0 commit comments