Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ DerivedData/

# SwiftLint Remote Config Cache
.swiftlint/RemoteConfigCache
.claude
MEGAREADME.md
15 changes: 15 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Swift 5.11 release date tools version availability

💡 Result:

  • Swift 5.11 “release date” (stable): There was no general-availability Swift 5.11 release. The last Swift 5.x major release was Swift 5.10 (March 5, 2024), and the next major release was Swift 6 (September 17, 2024). [1][2]
  • Where “Swift 5.11” existed: “5.11” showed up as a development-snapshot/toolchain identifier during the period after Swift 5.10 and before Swift 6 (e.g., Swift Forums discussions referencing “swift-DEVELOPMENT-SNAPSHOT … (which should map to 5.11)”). [3]
  • Xcode / toolchain availability: Apple’s Xcode line moved from Swift 5.10-era toolchains to Swift 6.0 in Xcode 16 (i.e., not Swift 5.11). [2][4]
  • SwiftPM swift-tools-version: 5.11: If you’re asking about Swift Package Manager tools version 5.11, it’s not tied to a stable “Swift 5.11” compiler release, and using newer tools versions inside Xcode can be confusing because Xcode uses its own embedded SwiftPM, which may not match an installed custom toolchain. [3]

Sources:
[1] Swift.org — Swift 5.10 Released (Mar 5, 2024) (swift.org)
[2] Swift.org — Announcing Swift 6 (Sep 17, 2024) (swift.org)
[3] Swift Forums — discussion of 5.10/“5.11” development snapshots & Xcode SwiftPM behavior (Feb 2024) (forums.swift.org)
[4] Apple Developer — Xcode releases table showing Swift 6.0 with Xcode 16 (developer.apple.com)

Citations:


Remove swift-tools-version:5.11—this version does not exist.

Swift 5.11 was never released. Swift 5.10 (March 2024) was the final Swift 5.x release; the next version is Swift 6.0 (September 2024). "5.11" appeared only as a development snapshot identifier and is not available in any production Xcode or SwiftPM toolchain. Change to swift-tools-version:5.10 or swift-tools-version:6.0 depending on your minimum Xcode requirement.

🤖 Prompt for AI Agents
In `@Package.swift` at line 1, Replace the invalid toolchain declaration "//
swift-tools-version:5.11" in Package.swift with a supported version string —
e.g., "// swift-tools-version:5.10" or "// swift-tools-version:6.0" depending on
your minimum Xcode/toolchain requirement — so the Package.swift header uses a
real SwiftPM toolchain identifier.


import PackageDescription

Expand All @@ -9,7 +9,10 @@ let package = Package(
.library(
name: "BetterSwiftAX",
targets: ["AccessibilityControl"]
),
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused dependency: swift-argument-parser is declared but not used by any target.

The package declares a dependency on swift-argument-parser, but none of the targets (CWindowControl, WindowControl, CAccessibilityControl, AccessibilityControl) include it in their dependencies array. Either remove this unused dependency or add it to the appropriate target.

🗑️ Option A: Remove unused dependency
-    dependencies: [
-        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
-    ],
+    dependencies: [],
➕ Option B: Add to a target that needs it
         .target(
             name: "AccessibilityControl",
-            dependencies: ["CAccessibilityControl", "WindowControl"]
+            dependencies: ["CAccessibilityControl", "WindowControl", .product(name: "ArgumentParser", package: "swift-argument-parser")]
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
dependencies: [],
🤖 Prompt for AI Agents
In `@Package.swift` around lines 14 - 15, The Package.swift currently lists
.package(url: "https://github.com/apple/swift-argument-parser.git", from:
"1.2.0") but no target references it; either remove that package entry from the
dependencies array or add the ArgumentParser product to the targets that
actually use it (e.g., add .product(name: "ArgumentParser", package:
"swift-argument-parser") to the dependencies array of the appropriate target(s)
such as "CWindowControl", "WindowControl", "CAccessibilityControl" or
"AccessibilityControl"); update only Package.swift so the declared package
matches the target usage.

],
targets: [
.target(
Expand All @@ -25,6 +28,6 @@ let package = Package(
.target(
name: "AccessibilityControl",
dependencies: ["CAccessibilityControl", "WindowControl"]
),
)
]
)
56 changes: 56 additions & 0 deletions Sources/AccessibilityControl/Accessibility+Role.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import ApplicationServices

extension Accessibility {
// https://developer.apple.com/documentation/applicationservices/carbon_accessibility/roles
public enum Role {
public static let application = kAXApplicationRole
public static let systemWide = kAXSystemWideRole
public static let window = kAXWindowRole
public static let sheet = kAXSheetRole
public static let drawer = kAXDrawerRole
public static let growArea = kAXGrowAreaRole
public static let image = kAXImageRole
public static let unknown = kAXUnknownRole
public static let button = kAXButtonRole
public static let radioButton = kAXRadioButtonRole
public static let checkBox = kAXCheckBoxRole
public static let popUpButton = kAXPopUpButtonRole
public static let menuButton = kAXMenuButtonRole
public static let tabGroup = kAXTabGroupRole
public static let table = kAXTableRole
public static let column = kAXColumnRole
public static let row = kAXRowRole
public static let outline = kAXOutlineRole
public static let browser = kAXBrowserRole
public static let scrollArea = kAXScrollAreaRole
public static let scrollBar = kAXScrollBarRole
public static let radioGroup = kAXRadioGroupRole
public static let list = kAXListRole
public static let group = kAXGroupRole
public static let valueIndicator = kAXValueIndicatorRole
public static let comboBox = kAXComboBoxRole
public static let slider = kAXSliderRole
public static let incrementor = kAXIncrementorRole
public static let busyIndicator = kAXBusyIndicatorRole
public static let progressIndicator = kAXProgressIndicatorRole
public static let relevanceIndicator = kAXRelevanceIndicatorRole
public static let toolbar = kAXToolbarRole
public static let disclosureTriangle = kAXDisclosureTriangleRole
public static let textField = kAXTextFieldRole
public static let textArea = kAXTextAreaRole
public static let staticText = kAXStaticTextRole
public static let menuBar = kAXMenuBarRole
public static let menuBarItem = kAXMenuBarItemRole
public static let menu = kAXMenuRole
public static let menuItem = kAXMenuItemRole
public static let splitGroup = kAXSplitGroupRole
public static let splitter = kAXSplitterRole
public static let colorWell = kAXColorWellRole
public static let timeField = kAXTimeFieldRole
public static let dateField = kAXDateFieldRole
public static let helpTag = kAXHelpTagRole
public static let matte = kAXMatteRole
public static let dockItem = kAXDockItemRole
public static let cell = kAXCellRole
}
}
34 changes: 34 additions & 0 deletions Sources/AccessibilityControl/Accessibility+Subrole.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import ApplicationServices

extension Accessibility {
public enum Subrole {
public static let `switch` = kAXSwitchSubrole
public static let closeButton = kAXCloseButtonSubrole
public static let minimizeButton = kAXMinimizeButtonSubrole
public static let zoomButton = kAXZoomButtonSubrole
public static let toolbarButton = kAXToolbarButtonSubrole
public static let secureTextField = kAXSecureTextFieldSubrole
public static let tableRow = kAXTableRowSubrole
public static let outlineRow = kAXOutlineRowSubrole
public static let unknown = kAXUnknownSubrole
public static let standardWindow = kAXStandardWindowSubrole
public static let dialog = kAXDialogSubrole
public static let systemDialog = kAXSystemDialogSubrole
public static let floatingWindow = kAXFloatingWindowSubrole
public static let systemFloatingWindow = kAXSystemFloatingWindowSubrole
public static let incrementArrow = kAXIncrementArrowSubrole
public static let decrementArrow = kAXDecrementArrowSubrole
public static let incrementPage = kAXIncrementPageSubrole
public static let decrementPage = kAXDecrementPageSubrole
public static let sortButton = kAXSortButtonSubrole
public static let searchField = kAXSearchFieldSubrole
public static let applicationDockItem = kAXApplicationDockItemSubrole
public static let documentDockItem = kAXDocumentDockItemSubrole
public static let folderDockItem = kAXFolderDockItemSubrole
public static let minimizedWindowDockItem = kAXMinimizedWindowDockItemSubrole
public static let urlDockItem = kAXURLDockItemSubrole
public static let dockExtraDockItem = kAXDockExtraDockItemSubrole
public static let trashDockItem = kAXTrashDockItemSubrole
public static let processSwitcherList = kAXProcessSwitcherListSubrole
}
}
4 changes: 3 additions & 1 deletion Sources/AccessibilityControl/Accessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ public enum Accessibility {
public typealias MutableAttributeName<T> = MutableAttribute<T>.Name
public typealias ParameterizedAttributeName<Parameter, Return> = ParameterizedAttribute<Parameter, Return>.Name

init() {}
init() {

}
}

public static func isTrusted(shouldPrompt: Bool = false) -> Bool {
Expand Down
92 changes: 92 additions & 0 deletions Sources/AccessibilityControl/Element+Utilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import CoreFoundation
import os.log

private let log = OSLog(subsystem: "com.betterswiftax", category: "accessibility")

public extension Accessibility.Element {
var isValid: Bool {
(try? pid()) != nil
}

var isFrameValid: Bool {
(try? self.frame()) != nil
}

var isInViewport: Bool {
(try? self.frame()) != CGRect.null
}
Comment on lines +15 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

isInViewport returns true when frame() throws.

try? self.frame() yields CGRect?. If frame() throws, the result is nil, and nil != CGRect.null evaluates to true — making the property incorrectly report the element as being in the viewport.

🐛 Proposed fix
     var isInViewport: Bool {
-        (try? self.frame()) != CGRect.null
+        guard let frame = try? self.frame() else { return false }
+        return frame != CGRect.null
     }
🤖 Prompt for AI Agents
In `@Sources/AccessibilityControl/Element`+Utilities.swift around lines 15 - 17,
The isInViewport computed property currently treats a thrown frame() as non-null
because (try? self.frame()) yields nil and nil != CGRect.null is true; change
isInViewport to safely unwrap the result of try? self.frame() and return false
if frame() throws or returns nil, otherwise compare the unwrapped CGRect to
CGRect.null (i.e., use a guard/if let on try? self.frame() and only then check
!= CGRect.null) so that exceptions result in false; reference: isInViewport
property and frame() method.


// - breadth-first, seems faster than dfs
// - default max complexity to 1,800; if i dump the complexity of the Messages app right now i get ~360. x10 that, should be plenty
// - we can't turn `AXUIElement`s into e.g. `ObjectIdentifier`s and use that to track a set of seen elements and avoid cycles because
// the objects aren't pooled; any given instance of `AXUIElement` in memory is "transient" and another may take its place
func recursiveChildren(maxTraversalComplexity: Int = 3_600) -> AnySequence<Accessibility.Element> {
// incremented for every element with children that we discover; not "depth" since it's a running tally
var traversalComplexity = 0

return AnySequence(sequence(state: [self] as [Accessibility.Element]) { queue -> Accessibility.Element? in
guard traversalComplexity < maxTraversalComplexity else {
os_log(.error, log: log, "HIT RECURSIVE TRAVERSAL COMPLEXITY LIMIT (%d > %d, queue count: %d), terminating early", traversalComplexity, maxTraversalComplexity, queue.count)
return nil
}

guard !queue.isEmpty else {
// queue is empty, we're done
return nil
}

let elt = queue.removeFirst()

if let children = try? elt.children() {
defer { traversalComplexity += 1 }
queue.append(contentsOf: children)
}
return elt
})
}

func recursiveSelectedChildren() -> AnySequence<Accessibility.Element> {
AnySequence(sequence(state: [self]) { queue -> Accessibility.Element? in
guard !queue.isEmpty else { return nil }
let elt = queue.removeFirst()
if let selectedChildren = try? elt.selectedChildren() {
queue.append(contentsOf: selectedChildren)
}
return elt
})
}
Comment on lines +48 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

recursiveSelectedChildren has no cycle or depth protection.

Unlike recursiveChildren which has maxTraversalComplexity, this method has no safeguard. If selectedChildren relationships form a cycle, this will loop indefinitely.

🤖 Prompt for AI Agents
In `@Sources/AccessibilityControl/Element`+Utilities.swift around lines 48 - 57,
The recursiveSelectedChildren() sequence lacks cycle/depth protection and can
loop indefinitely if selectedChildren() forms cycles; update
recursiveSelectedChildren() (referencing the recursiveSelectedChildren function
and elt.selectedChildren()) to mirror recursiveChildren's safeguards by either
tracking visited elements (a Set of Accessibility.Element identifiers) and
skipping already-seen nodes, or by enforcing the existing maxTraversalComplexity
limit used by recursiveChildren and terminating the sequence when that threshold
is exceeded; ensure you preserve the AnySequence/sequence structure while
breaking traversal when a cycle is detected or the maxTraversalComplexity is
reached.


func recursivelyFindChild(withID id: String) -> Accessibility.Element? {
recursiveChildren().lazy.first {
(try? $0.identifier()) == id
}
}

func setFrame(_ frame: CGRect) throws {
DispatchQueue.concurrentPerform(iterations: 2) { i in
switch i {
case 0:
try? self.position(assign: frame.origin)
case 1:
try? self.size(assign: frame.size)
default:
break
}
}
}
Comment on lines +65 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

setFrame is declared throws but can never throw.

Both position(assign:) and size(assign:) are called with try?, silently discarding any errors. Callers will incorrectly assume failures are propagated. Either remove throws or propagate the errors.

Option A: Remove `throws` (matches current behavior)
-    func setFrame(_ frame: CGRect) throws {
+    func setFrame(_ frame: CGRect) {
Option B: Propagate errors sequentially
     func setFrame(_ frame: CGRect) throws {
-        DispatchQueue.concurrentPerform(iterations: 2) { i in
-            switch i {
-            case 0:
-                try? self.position(assign: frame.origin)
-            case 1:
-                try? self.size(assign: frame.size)
-            default:
-                break
-            }
-        }
+        try self.position(assign: frame.origin)
+        try self.size(assign: frame.size)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func setFrame(_ frame: CGRect) throws {
DispatchQueue.concurrentPerform(iterations: 2) { i in
switch i {
case 0:
try? self.position(assign: frame.origin)
case 1:
try? self.size(assign: frame.size)
default:
break
}
}
}
func setFrame(_ frame: CGRect) {
DispatchQueue.concurrentPerform(iterations: 2) { i in
switch i {
case 0:
try? self.position(assign: frame.origin)
case 1:
try? self.size(assign: frame.size)
default:
break
}
}
}
Suggested change
func setFrame(_ frame: CGRect) throws {
DispatchQueue.concurrentPerform(iterations: 2) { i in
switch i {
case 0:
try? self.position(assign: frame.origin)
case 1:
try? self.size(assign: frame.size)
default:
break
}
}
}
func setFrame(_ frame: CGRect) throws {
try self.position(assign: frame.origin)
try self.size(assign: frame.size)
}
🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 66-66: Magic numbers should be replaced by named constants

(no_magic_numbers)

🤖 Prompt for AI Agents
In `@Sources/AccessibilityControl/Element`+Utilities.swift around lines 65 - 76,
The setFrame(_:) function is declared throws but uses try? inside
DispatchQueue.concurrentPerform so no errors are ever propagated; either remove
throws from setFrame or change the implementation to propagate errors: replace
the concurrentPerform block with sequential calls that use try (e.g., try
position(assign:) followed by try size(assign:)) or capture and rethrow the
first error from both calls, ensuring position(assign:) and size(assign:) errors
are not swallowed; adjust the function signature accordingly if you remove
throwing behavior.


func closeWindow() throws {
guard let closeButton = try? self.windowCloseButton() else {
throw AccessibilityError(.failure)
}
try closeButton.press()
}
}

public extension Accessibility.Element {
func firstChild(withRole role: KeyPath<Accessibility.Role.Type, String>) -> Accessibility.Element? {
try? self.children().first { child in
(try? child.role()) == Accessibility.Role.self[keyPath: role]
}
}
}
Loading