mirror of
https://github.com/wgtunnel/Nucleus.git
synced 2026-10-09 14:40:54 -07:00
176 lines
7.5 KiB
Swift
176 lines
7.5 KiB
Swift
// axtool — minimal AX exerciser for end-to-end testing.
|
|
// axtool <pid> dump [maxDepth] Print tree
|
|
// axtool <pid> find <role> <descSubstring> Print first match info + actions + value
|
|
// axtool <pid> press <role> <descSubstring> AXPress
|
|
// axtool <pid> increment <role> <descSubstring> AXIncrement
|
|
// axtool <pid> decrement <role> <descSubstring> AXDecrement
|
|
// axtool <pid> setvalue <role> <descSubstring> <newValue>
|
|
// axtool <pid> nav <role> <descSubstring> Print NavigableStaticText params
|
|
import Cocoa
|
|
import ApplicationServices
|
|
|
|
func attr(_ el: AXUIElement, _ name: String) -> CFTypeRef? {
|
|
var v: CFTypeRef?
|
|
return AXUIElementCopyAttributeValue(el, name as CFString, &v) == .success ? v : nil
|
|
}
|
|
|
|
func paramAttr(_ el: AXUIElement, _ name: String, _ param: AnyObject) -> CFTypeRef? {
|
|
var v: CFTypeRef?
|
|
return AXUIElementCopyParameterizedAttributeValue(el, name as CFString, param, &v) == .success ? v : nil
|
|
}
|
|
|
|
func walk(_ el: AXUIElement, _ visit: (AXUIElement) -> Bool) {
|
|
if !visit(el) { return }
|
|
if let kids = attr(el, kAXChildrenAttribute as String) as? [AXUIElement] {
|
|
for k in kids { walk(k, visit) }
|
|
}
|
|
// Application elements expose top-level windows via AXWindows, not AXChildren.
|
|
if let wins = attr(el, kAXWindowsAttribute as String) as? [AXUIElement] {
|
|
for w in wins { walk(w, visit) }
|
|
}
|
|
}
|
|
|
|
func appRoot(_ pid: Int32) -> AXUIElement { AXUIElementCreateApplication(pid) }
|
|
|
|
func find(_ root: AXUIElement, role: String, descSubstr: String) -> AXUIElement? {
|
|
var found: AXUIElement?
|
|
walk(root) { el in
|
|
if found != nil { return false }
|
|
let r = (attr(el, kAXRoleAttribute as String) as? String) ?? ""
|
|
if r != role { return true }
|
|
let d = (attr(el, kAXDescriptionAttribute as String) as? String) ?? ""
|
|
let t = (attr(el, kAXTitleAttribute as String) as? String) ?? ""
|
|
if descSubstr.isEmpty || d.contains(descSubstr) || t.contains(descSubstr) {
|
|
found = el
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
return found
|
|
}
|
|
|
|
func dump(_ el: AXUIElement, _ depth: Int, _ maxDepth: Int) {
|
|
if depth > maxDepth { return }
|
|
let role = (attr(el, kAXRoleAttribute as String) as? String) ?? "?"
|
|
let desc = (attr(el, kAXDescriptionAttribute as String) as? String) ?? ""
|
|
let title = (attr(el, kAXTitleAttribute as String) as? String) ?? ""
|
|
let value = attr(el, kAXValueAttribute as String).map { String(describing: $0) } ?? ""
|
|
let pad = String(repeating: " ", count: depth)
|
|
print("\(pad)\(role) desc=[\(desc)] title=[\(title)] val=[\(value)]")
|
|
if let kids = attr(el, kAXChildrenAttribute as String) as? [AXUIElement] {
|
|
for k in kids { dump(k, depth + 1, maxDepth) }
|
|
}
|
|
}
|
|
|
|
func info(_ el: AXUIElement) {
|
|
let role = (attr(el, kAXRoleAttribute as String) as? String) ?? "?"
|
|
let sub = (attr(el, kAXSubroleAttribute as String) as? String) ?? ""
|
|
let desc = (attr(el, kAXDescriptionAttribute as String) as? String) ?? ""
|
|
let title = (attr(el, kAXTitleAttribute as String) as? String) ?? ""
|
|
let value = attr(el, kAXValueAttribute as String).map { String(describing: $0) } ?? ""
|
|
print("role=\(role) subrole=[\(sub)] desc=[\(desc)] title=[\(title)] value=[\(value)]")
|
|
var actions: CFArray?
|
|
if AXUIElementCopyActionNames(el, &actions) == .success, let arr = actions as? [String] {
|
|
print("actions=\(arr.joined(separator: ","))")
|
|
}
|
|
if let mn = attr(el, kAXMinValueAttribute as String) {
|
|
print("min=\(mn) max=\(attr(el, kAXMaxValueAttribute as String) ?? "?" as CFTypeRef)")
|
|
}
|
|
// Decode position + size (each is an AXValue carrying a CGPoint / CGSize).
|
|
if let posVal = attr(el, kAXPositionAttribute as String) {
|
|
if CFGetTypeID(posVal) == AXValueGetTypeID() {
|
|
var p = CGPoint.zero
|
|
AXValueGetValue(posVal as! AXValue, .cgPoint, &p)
|
|
print("position=(\(p.x), \(p.y))")
|
|
}
|
|
}
|
|
if let szVal = attr(el, kAXSizeAttribute as String) {
|
|
if CFGetTypeID(szVal) == AXValueGetTypeID() {
|
|
var s = CGSize.zero
|
|
AXValueGetValue(szVal as! AXValue, .cgSize, &s)
|
|
print("size=(\(s.width), \(s.height))")
|
|
}
|
|
}
|
|
}
|
|
|
|
let argv = CommandLine.arguments
|
|
guard argv.count >= 3 else {
|
|
print("usage: axtool <pid> <cmd> [...]")
|
|
exit(2)
|
|
}
|
|
_ = argv.count
|
|
let pid = Int32(argv[1])!
|
|
let cmd = argv[2]
|
|
let app = appRoot(pid)
|
|
|
|
switch cmd {
|
|
case "dump":
|
|
let depth = argv.count >= 4 ? Int(argv[3])! : 10
|
|
if let wins = attr(app, kAXWindowsAttribute as String) as? [AXUIElement] {
|
|
for w in wins { dump(w, 0, depth) }
|
|
}
|
|
case "find":
|
|
if let el = find(app, role: argv[3], descSubstr: argv[4]) {
|
|
info(el)
|
|
} else { print("NOT FOUND"); exit(1) }
|
|
case "wininfo":
|
|
if let wins = attr(app, kAXWindowsAttribute as String) as? [AXUIElement] {
|
|
for (i, w) in wins.enumerated() {
|
|
print("Window \(i):")
|
|
info(w)
|
|
}
|
|
}
|
|
case "roles":
|
|
var seen: [String: Int] = [:]
|
|
walk(app) { el in
|
|
let r = (attr(el, kAXRoleAttribute as String) as? String) ?? "?"
|
|
seen[r, default: 0] += 1
|
|
return true
|
|
}
|
|
for (k, v) in seen.sorted(by: { $0.key < $1.key }) { print("\(k): \(v)") }
|
|
case "press", "increment", "decrement":
|
|
let action: String = ["press":"AXPress", "increment":"AXIncrement", "decrement":"AXDecrement"][cmd]!
|
|
if let el = find(app, role: argv[3], descSubstr: argv[4]) {
|
|
let err = AXUIElementPerformAction(el, action as CFString)
|
|
print("perform \(action) err=\(err.rawValue)")
|
|
} else { print("NOT FOUND"); exit(1) }
|
|
case "setvalue":
|
|
if let el = find(app, role: argv[3], descSubstr: argv[4]) {
|
|
let str = argv[5] as CFString
|
|
let err = AXUIElementSetAttributeValue(el, kAXValueAttribute as CFString, str)
|
|
print("setvalue err=\(err.rawValue)")
|
|
} else { print("NOT FOUND"); exit(1) }
|
|
case "setsel":
|
|
if let el = find(app, role: argv[3], descSubstr: argv[4]) {
|
|
var r = CFRange(location: Int(argv[5])!, length: Int(argv[6])!)
|
|
let v = AXValueCreate(.cfRange, &r)!
|
|
let err = AXUIElementSetAttributeValue(el, kAXSelectedTextRangeAttribute as CFString, v)
|
|
print("setsel err=\(err.rawValue)")
|
|
} else { print("NOT FOUND"); exit(1) }
|
|
case "nav":
|
|
if let el = find(app, role: argv[3], descSubstr: argv[4]) {
|
|
// Read NavigableStaticText params
|
|
let count = attr(el, kAXNumberOfCharactersAttribute as String)
|
|
print("numberOfChars=\(count ?? "nil" as CFTypeRef)")
|
|
if let n = (count as? Int) ?? (count as? NSNumber).map({ $0.intValue }) {
|
|
// line for index 0
|
|
let zero = NSNumber(value: 0)
|
|
let line = paramAttr(el, kAXLineForIndexParameterizedAttribute as String, zero)
|
|
print("lineForIndex(0)=\(line ?? "nil" as CFTypeRef)")
|
|
// range for line 0
|
|
let range = paramAttr(el, kAXRangeForLineParameterizedAttribute as String, zero)
|
|
print("rangeForLine(0)=\(range ?? "nil" as CFTypeRef)")
|
|
// string for full range
|
|
var rangeVal = CFRange(location: 0, length: n)
|
|
let axRange = AXValueCreate(.cfRange, &rangeVal)!
|
|
let str = paramAttr(el, kAXStringForRangeParameterizedAttribute as String, axRange)
|
|
print("stringForRange(0..\(n))=\(str ?? "nil" as CFTypeRef)")
|
|
}
|
|
let sel = attr(el, kAXSelectedTextRangeAttribute as String)
|
|
print("selectedTextRange=\(sel ?? "nil" as CFTypeRef)")
|
|
} else { print("NOT FOUND"); exit(1) }
|
|
default:
|
|
print("unknown cmd")
|
|
exit(2)
|
|
}
|