/ internal / tools / axserver / Sources / Find.swift
Find.swift
 1  import ApplicationServices
 2  
 3  func findElements(pid: Int, query: String?, role: String?, identifier: String?) -> [FindResult] {
 4      let appRef = AXUIElementCreateApplication(Int32(pid))
 5      guard let windows = axValue(appRef, "AXWindows") as? [AXUIElement] else {
 6          return []
 7      }
 8  
 9      var results: [FindResult] = []
10      for (winIdx, window) in windows.enumerated() {
11          searchTree(window, path: "window[\(winIdx)]", query: query, role: role,
12                     identifier: identifier, results: &results, limit: 50)
13      }
14      return results
15  }
16  
17  private func searchTree(_ el: AXUIElement, path: String, query: String?, role: String?,
18                           identifier: String?, results: inout [FindResult], limit: Int) {
19      guard results.count < limit else { return }
20  
21      let elRole = axString(el, "AXRole") ?? ""
22      let title = axString(el, "AXTitle") ?? ""
23      let desc = axString(el, "AXDescription") ?? ""
24      let value: String
25      if let v = axValue(el, "AXValue") {
26          value = "\(v)"
27      } else {
28          value = ""
29      }
30      let ident = axString(el, "AXIdentifier")
31  
32      // Match by identifier (exact)
33      if let id = identifier, let actualIdent = ident, actualIdent == id {
34          var r = FindResult(path: path, role: elRole, title: title)
35          if !desc.isEmpty { r.desc = desc }
36          if !value.isEmpty { r.value = String(value.prefix(200)) }
37          results.append(r)
38          return
39      }
40  
41      // Match by role + query
42      let roleMatch = role == nil || elRole == role
43      var textMatch = query == nil
44      if let q = query?.lowercased() {
45          textMatch = title.lowercased().contains(q)
46                   || desc.lowercased().contains(q)
47                   || value.lowercased().contains(q)
48      }
49  
50      if roleMatch && textMatch {
51          var r = FindResult(path: path, role: elRole, title: title)
52          if !desc.isEmpty { r.desc = desc }
53          if !value.isEmpty { r.value = String(value.prefix(200)) }
54          results.append(r)
55      }
56  
57      // Recurse
58      guard let children = axChildren(el) else { return }
59      var childIndex: [String: Int] = [:]
60      for child in children {
61          guard let childRole = axString(child, "AXRole") else { continue }
62          let idx = childIndex[childRole, default: 0]
63          childIndex[childRole] = idx + 1
64          searchTree(child, path: "\(path)/\(childRole)[\(idx)]", query: query,
65                     role: role, identifier: identifier, results: &results, limit: limit)
66      }
67  }