Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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

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 thread
coderabbitai[bot] marked this conversation as resolved.
],
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 thread
pmanot marked this conversation as resolved.

// - 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 thread
pmanot marked this conversation as resolved.

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 thread
pmanot marked this conversation as resolved.

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