├── .gitignore ├── AXSwift.podspec ├── AXSwift.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── AXSwift.xcscheme ├── AXSwiftExample ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist └── main.swift ├── AXSwiftObserverExample ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist └── main.swift ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── AXSwift.h ├── AXSwift.swift ├── Application.swift ├── Constants.swift ├── Error.swift ├── Info.plist ├── Observer.swift ├── SystemWideElement.swift └── UIElement.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## Build generated 4 | build/ 5 | .build/ 6 | DerivedData 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | 19 | ## Other 20 | *.xccheckout 21 | *.moved-aside 22 | *.xcuserstate 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | # CocoaPods 30 | Pods/ 31 | 32 | # Carthage 33 | # 34 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 35 | # Carthage/Checkouts 36 | 37 | Carthage/Build 38 | 39 | 40 | # OS X 41 | 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | ## Icon must end with two \r 47 | Icon 48 | 49 | 50 | ## Thumbnails 51 | ._* 52 | -------------------------------------------------------------------------------- /AXSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AXSwift' 3 | s.version = '0.3.2' 4 | s.summary = 'Swift wrapper for Mac accessibility APIs' 5 | 6 | s.description = <<-DESC 7 | AXSwift is a Swift wrapper for OS X's C-based accessibility client APIs. Working with these APIs 8 | is error-prone and a huge pain, so AXSwift makes everything easier: 9 | 10 | - Modern API that's 100% Swift 11 | - Explicit error handling 12 | - Complete coverage of the underlying C API 13 | - Better documentation than Apple's, which is pretty poor 14 | 15 | This framework is intended as a basic wrapper and doesn't keep any state or do any "magic". 16 | That's up to you! 17 | DESC 18 | 19 | s.homepage = 'https://github.com/tmandry/AXSwift' 20 | s.license = { :type => 'MIT', :file => 'LICENSE' } 21 | s.author = { 'Tyler Mandry' => 'tmandry@gmail.com' } 22 | s.source = { :git => 'https://github.com/tmandry/AXSwift.git', :tag => s.version.to_s } 23 | s.social_media_url = 'https://twitter.com/tmandry' 24 | 25 | s.osx.deployment_target = '10.10' 26 | s.swift_version = '5.0' 27 | 28 | s.source_files = 'Sources/*.{swift,h}' 29 | 30 | s.public_header_files = 'Sources/*.h' 31 | s.frameworks = 'Cocoa' 32 | end 33 | -------------------------------------------------------------------------------- /AXSwift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2F86CE9B1BD49958007E8C29 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F86CE9A1BD49958007E8C29 /* AppDelegate.swift */; }; 11 | 2F86CE9D1BD49958007E8C29 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F86CE9C1BD49958007E8C29 /* Assets.xcassets */; }; 12 | 2F86CEA61BD4999F007E8C29 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F86CEA51BD4999F007E8C29 /* main.swift */; }; 13 | 2F86CEB01BD5621F007E8C29 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F86CEAF1BD5621F007E8C29 /* AppDelegate.swift */; }; 14 | 2F86CEB21BD5621F007E8C29 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F86CEB11BD5621F007E8C29 /* Assets.xcassets */; }; 15 | 2F86CEBB1BD56250007E8C29 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F86CEBA1BD56250007E8C29 /* main.swift */; }; 16 | 2FF560E41FF0C039001AF272 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560DB1FF0C039001AF272 /* Application.swift */; }; 17 | 2FF560E51FF0C039001AF272 /* AXSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FF560DC1FF0C039001AF272 /* AXSwift.h */; settings = {ATTRIBUTES = (Private, ); }; }; 18 | 2FF560E61FF0C039001AF272 /* AXSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560DD1FF0C039001AF272 /* AXSwift.swift */; }; 19 | 2FF560E71FF0C039001AF272 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560DE1FF0C039001AF272 /* Constants.swift */; }; 20 | 2FF560E81FF0C039001AF272 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560DF1FF0C039001AF272 /* Error.swift */; }; 21 | 2FF560EA1FF0C039001AF272 /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560E11FF0C039001AF272 /* Observer.swift */; }; 22 | 2FF560EB1FF0C039001AF272 /* SystemWideElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560E21FF0C039001AF272 /* SystemWideElement.swift */; }; 23 | 2FF560EC1FF0C039001AF272 /* UIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF560E31FF0C039001AF272 /* UIElement.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | 2F86CEFD1BD70887007E8C29 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 2F32C67B1BD42D5000AD4419 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 2F32C6831BD42D5000AD4419; 32 | remoteInfo = AXSwift; 33 | }; 34 | 2FF560D81FF0BFE3001AF272 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 2F32C67B1BD42D5000AD4419 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = 2F32C6831BD42D5000AD4419; 39 | remoteInfo = AXSwift; 40 | }; 41 | /* End PBXContainerItemProxy section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 2F86CE9A1BD49958007E8C29 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45 | 2F86CE9C1BD49958007E8C29 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | 2F86CEA11BD49958007E8C29 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | 2F86CEA51BD4999F007E8C29 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 48 | 2F86CEAF1BD5621F007E8C29 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | 2F86CEB11BD5621F007E8C29 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50 | 2F86CEB61BD5621F007E8C29 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 2F86CEBA1BD56250007E8C29 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 52 | 2FF560D51FF0BF79001AF272 /* AXSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AXSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 2FF560D61FF0BF79001AF272 /* AXSwiftExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AXSwiftExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 2FF560D71FF0BF79001AF272 /* AXSwiftObserverExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AXSwiftObserverExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 2FF560DB1FF0C039001AF272 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 56 | 2FF560DC1FF0C039001AF272 /* AXSwift.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AXSwift.h; sourceTree = ""; }; 57 | 2FF560DD1FF0C039001AF272 /* AXSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXSwift.swift; sourceTree = ""; }; 58 | 2FF560DE1FF0C039001AF272 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 59 | 2FF560DF1FF0C039001AF272 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 60 | 2FF560E01FF0C039001AF272 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 2FF560E11FF0C039001AF272 /* Observer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = ""; }; 62 | 2FF560E21FF0C039001AF272 /* SystemWideElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemWideElement.swift; sourceTree = ""; }; 63 | 2FF560E31FF0C039001AF272 /* UIElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIElement.swift; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 2F32C6801BD42D5000AD4419 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | 2F86CE951BD49958007E8C29 /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | 2F86CEAA1BD5621F007E8C29 /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 2F32C67A1BD42D5000AD4419 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 2FF560DA1FF0C039001AF272 /* Sources */, 95 | 2F86CE991BD49958007E8C29 /* AXSwiftExample */, 96 | 2F86CEAE1BD5621F007E8C29 /* AXSwiftObserverExample */, 97 | 2FF560D51FF0BF79001AF272 /* AXSwift.framework */, 98 | 2FF560D61FF0BF79001AF272 /* AXSwiftExample.app */, 99 | 2FF560D71FF0BF79001AF272 /* AXSwiftObserverExample.app */, 100 | ); 101 | indentWidth = 4; 102 | sourceTree = ""; 103 | tabWidth = 4; 104 | usesTabs = 0; 105 | }; 106 | 2F86CE991BD49958007E8C29 /* AXSwiftExample */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 2F86CE9A1BD49958007E8C29 /* AppDelegate.swift */, 110 | 2F86CEA51BD4999F007E8C29 /* main.swift */, 111 | 2F86CE9C1BD49958007E8C29 /* Assets.xcassets */, 112 | 2F86CEA11BD49958007E8C29 /* Info.plist */, 113 | ); 114 | path = AXSwiftExample; 115 | sourceTree = ""; 116 | }; 117 | 2F86CEAE1BD5621F007E8C29 /* AXSwiftObserverExample */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 2F86CEAF1BD5621F007E8C29 /* AppDelegate.swift */, 121 | 2F86CEBA1BD56250007E8C29 /* main.swift */, 122 | 2F86CEB11BD5621F007E8C29 /* Assets.xcassets */, 123 | 2F86CEB61BD5621F007E8C29 /* Info.plist */, 124 | ); 125 | path = AXSwiftObserverExample; 126 | sourceTree = ""; 127 | }; 128 | 2FF560DA1FF0C039001AF272 /* Sources */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 2FF560DB1FF0C039001AF272 /* Application.swift */, 132 | 2FF560DC1FF0C039001AF272 /* AXSwift.h */, 133 | 2FF560DD1FF0C039001AF272 /* AXSwift.swift */, 134 | 2FF560DE1FF0C039001AF272 /* Constants.swift */, 135 | 2FF560DF1FF0C039001AF272 /* Error.swift */, 136 | 2FF560E01FF0C039001AF272 /* Info.plist */, 137 | 2FF560E11FF0C039001AF272 /* Observer.swift */, 138 | 2FF560E21FF0C039001AF272 /* SystemWideElement.swift */, 139 | 2FF560E31FF0C039001AF272 /* UIElement.swift */, 140 | ); 141 | path = Sources; 142 | sourceTree = ""; 143 | }; 144 | /* End PBXGroup section */ 145 | 146 | /* Begin PBXHeadersBuildPhase section */ 147 | 2F32C6811BD42D5000AD4419 /* Headers */ = { 148 | isa = PBXHeadersBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 2FF560E51FF0C039001AF272 /* AXSwift.h in Headers */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXHeadersBuildPhase section */ 156 | 157 | /* Begin PBXNativeTarget section */ 158 | 2F32C6831BD42D5000AD4419 /* AXSwift */ = { 159 | isa = PBXNativeTarget; 160 | buildConfigurationList = 2F32C68C1BD42D5000AD4419 /* Build configuration list for PBXNativeTarget "AXSwift" */; 161 | buildPhases = ( 162 | 2F32C67F1BD42D5000AD4419 /* Sources */, 163 | 2F32C6801BD42D5000AD4419 /* Frameworks */, 164 | 2F32C6811BD42D5000AD4419 /* Headers */, 165 | 2F32C6821BD42D5000AD4419 /* Resources */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | ); 171 | name = AXSwift; 172 | productName = AXSwift; 173 | productReference = 2FF560D51FF0BF79001AF272 /* AXSwift.framework */; 174 | productType = "com.apple.product-type.framework"; 175 | }; 176 | 2F86CE971BD49958007E8C29 /* AXSwiftExample */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = 2F86CEA41BD49958007E8C29 /* Build configuration list for PBXNativeTarget "AXSwiftExample" */; 179 | buildPhases = ( 180 | 2F86CE941BD49958007E8C29 /* Sources */, 181 | 2F86CE951BD49958007E8C29 /* Frameworks */, 182 | 2F86CE961BD49958007E8C29 /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | 2FF560D91FF0BFE3001AF272 /* PBXTargetDependency */, 188 | ); 189 | name = AXSwiftExample; 190 | productName = AXSwiftExample; 191 | productReference = 2FF560D61FF0BF79001AF272 /* AXSwiftExample.app */; 192 | productType = "com.apple.product-type.application"; 193 | }; 194 | 2F86CEAC1BD5621F007E8C29 /* AXSwiftObserverExample */ = { 195 | isa = PBXNativeTarget; 196 | buildConfigurationList = 2F86CEB71BD5621F007E8C29 /* Build configuration list for PBXNativeTarget "AXSwiftObserverExample" */; 197 | buildPhases = ( 198 | 2F86CEA91BD5621F007E8C29 /* Sources */, 199 | 2F86CEAA1BD5621F007E8C29 /* Frameworks */, 200 | 2F86CEAB1BD5621F007E8C29 /* Resources */, 201 | ); 202 | buildRules = ( 203 | ); 204 | dependencies = ( 205 | 2F86CEFE1BD70887007E8C29 /* PBXTargetDependency */, 206 | ); 207 | name = AXSwiftObserverExample; 208 | productName = AXSwiftObserverExample; 209 | productReference = 2FF560D71FF0BF79001AF272 /* AXSwiftObserverExample.app */; 210 | productType = "com.apple.product-type.application"; 211 | }; 212 | /* End PBXNativeTarget section */ 213 | 214 | /* Begin PBXProject section */ 215 | 2F32C67B1BD42D5000AD4419 /* Project object */ = { 216 | isa = PBXProject; 217 | attributes = { 218 | LastSwiftUpdateCheck = 0710; 219 | LastUpgradeCheck = 1220; 220 | ORGANIZATIONNAME = "Tyler Mandry"; 221 | TargetAttributes = { 222 | 2F32C6831BD42D5000AD4419 = { 223 | CreatedOnToolsVersion = 7.0; 224 | LastSwiftMigration = 0900; 225 | }; 226 | 2F86CE971BD49958007E8C29 = { 227 | CreatedOnToolsVersion = 7.1; 228 | LastSwiftMigration = 0830; 229 | }; 230 | 2F86CEAC1BD5621F007E8C29 = { 231 | CreatedOnToolsVersion = 7.1; 232 | LastSwiftMigration = 0900; 233 | }; 234 | }; 235 | }; 236 | buildConfigurationList = 2F32C67E1BD42D5000AD4419 /* Build configuration list for PBXProject "AXSwift" */; 237 | compatibilityVersion = "Xcode 3.2"; 238 | developmentRegion = English; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | English, 242 | en, 243 | Base, 244 | ); 245 | mainGroup = 2F32C67A1BD42D5000AD4419; 246 | productRefGroup = 2F32C67A1BD42D5000AD4419; 247 | projectDirPath = ""; 248 | projectRoot = ""; 249 | targets = ( 250 | 2F32C6831BD42D5000AD4419 /* AXSwift */, 251 | 2F86CE971BD49958007E8C29 /* AXSwiftExample */, 252 | 2F86CEAC1BD5621F007E8C29 /* AXSwiftObserverExample */, 253 | ); 254 | }; 255 | /* End PBXProject section */ 256 | 257 | /* Begin PBXResourcesBuildPhase section */ 258 | 2F32C6821BD42D5000AD4419 /* Resources */ = { 259 | isa = PBXResourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | 2F86CE961BD49958007E8C29 /* Resources */ = { 266 | isa = PBXResourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | 2F86CE9D1BD49958007E8C29 /* Assets.xcassets in Resources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | 2F86CEAB1BD5621F007E8C29 /* Resources */ = { 274 | isa = PBXResourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 2F86CEB21BD5621F007E8C29 /* Assets.xcassets in Resources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | /* End PBXResourcesBuildPhase section */ 282 | 283 | /* Begin PBXSourcesBuildPhase section */ 284 | 2F32C67F1BD42D5000AD4419 /* Sources */ = { 285 | isa = PBXSourcesBuildPhase; 286 | buildActionMask = 2147483647; 287 | files = ( 288 | 2FF560EB1FF0C039001AF272 /* SystemWideElement.swift in Sources */, 289 | 2FF560EC1FF0C039001AF272 /* UIElement.swift in Sources */, 290 | 2FF560E81FF0C039001AF272 /* Error.swift in Sources */, 291 | 2FF560EA1FF0C039001AF272 /* Observer.swift in Sources */, 292 | 2FF560E61FF0C039001AF272 /* AXSwift.swift in Sources */, 293 | 2FF560E41FF0C039001AF272 /* Application.swift in Sources */, 294 | 2FF560E71FF0C039001AF272 /* Constants.swift in Sources */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | 2F86CE941BD49958007E8C29 /* Sources */ = { 299 | isa = PBXSourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | 2F86CEA61BD4999F007E8C29 /* main.swift in Sources */, 303 | 2F86CE9B1BD49958007E8C29 /* AppDelegate.swift in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | 2F86CEA91BD5621F007E8C29 /* Sources */ = { 308 | isa = PBXSourcesBuildPhase; 309 | buildActionMask = 2147483647; 310 | files = ( 311 | 2F86CEBB1BD56250007E8C29 /* main.swift in Sources */, 312 | 2F86CEB01BD5621F007E8C29 /* AppDelegate.swift in Sources */, 313 | ); 314 | runOnlyForDeploymentPostprocessing = 0; 315 | }; 316 | /* End PBXSourcesBuildPhase section */ 317 | 318 | /* Begin PBXTargetDependency section */ 319 | 2F86CEFE1BD70887007E8C29 /* PBXTargetDependency */ = { 320 | isa = PBXTargetDependency; 321 | target = 2F32C6831BD42D5000AD4419 /* AXSwift */; 322 | targetProxy = 2F86CEFD1BD70887007E8C29 /* PBXContainerItemProxy */; 323 | }; 324 | 2FF560D91FF0BFE3001AF272 /* PBXTargetDependency */ = { 325 | isa = PBXTargetDependency; 326 | target = 2F32C6831BD42D5000AD4419 /* AXSwift */; 327 | targetProxy = 2FF560D81FF0BFE3001AF272 /* PBXContainerItemProxy */; 328 | }; 329 | /* End PBXTargetDependency section */ 330 | 331 | /* Begin XCBuildConfiguration section */ 332 | 2F32C68A1BD42D5000AD4419 /* Debug */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ALWAYS_SEARCH_USER_PATHS = NO; 336 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 338 | CLANG_CXX_LIBRARY = "libc++"; 339 | CLANG_ENABLE_MODULES = YES; 340 | CLANG_ENABLE_OBJC_ARC = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | CURRENT_PROJECT_VERSION = 1; 363 | DEBUG_INFORMATION_FORMAT = dwarf; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | ENABLE_TESTABILITY = YES; 366 | GCC_C_LANGUAGE_STANDARD = gnu99; 367 | GCC_DYNAMIC_NO_PIC = NO; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_OPTIMIZATION_LEVEL = 0; 370 | GCC_PREPROCESSOR_DEFINITIONS = ( 371 | "DEBUG=1", 372 | "$(inherited)", 373 | ); 374 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 375 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 376 | GCC_WARN_UNDECLARED_SELECTOR = YES; 377 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 378 | GCC_WARN_UNUSED_FUNCTION = YES; 379 | GCC_WARN_UNUSED_VARIABLE = YES; 380 | MACOSX_DEPLOYMENT_TARGET = 10.10; 381 | MTL_ENABLE_DEBUG_INFO = YES; 382 | ONLY_ACTIVE_ARCH = YES; 383 | SDKROOT = macosx; 384 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 385 | VERSIONING_SYSTEM = "apple-generic"; 386 | VERSION_INFO_PREFIX = ""; 387 | }; 388 | name = Debug; 389 | }; 390 | 2F32C68B1BD42D5000AD4419 /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | buildSettings = { 393 | ALWAYS_SEARCH_USER_PATHS = NO; 394 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 395 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 396 | CLANG_CXX_LIBRARY = "libc++"; 397 | CLANG_ENABLE_MODULES = YES; 398 | CLANG_ENABLE_OBJC_ARC = YES; 399 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 400 | CLANG_WARN_BOOL_CONVERSION = YES; 401 | CLANG_WARN_COMMA = YES; 402 | CLANG_WARN_CONSTANT_CONVERSION = YES; 403 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 404 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 405 | CLANG_WARN_EMPTY_BODY = YES; 406 | CLANG_WARN_ENUM_CONVERSION = YES; 407 | CLANG_WARN_INFINITE_RECURSION = YES; 408 | CLANG_WARN_INT_CONVERSION = YES; 409 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 410 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 411 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 412 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 413 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 414 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 415 | CLANG_WARN_STRICT_PROTOTYPES = YES; 416 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 417 | CLANG_WARN_UNREACHABLE_CODE = YES; 418 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 419 | COPY_PHASE_STRIP = NO; 420 | CURRENT_PROJECT_VERSION = 1; 421 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 422 | ENABLE_NS_ASSERTIONS = NO; 423 | ENABLE_STRICT_OBJC_MSGSEND = YES; 424 | GCC_C_LANGUAGE_STANDARD = gnu99; 425 | GCC_NO_COMMON_BLOCKS = YES; 426 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 427 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 428 | GCC_WARN_UNDECLARED_SELECTOR = YES; 429 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 430 | GCC_WARN_UNUSED_FUNCTION = YES; 431 | GCC_WARN_UNUSED_VARIABLE = YES; 432 | MACOSX_DEPLOYMENT_TARGET = 10.10; 433 | MTL_ENABLE_DEBUG_INFO = NO; 434 | SDKROOT = macosx; 435 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 436 | VERSIONING_SYSTEM = "apple-generic"; 437 | VERSION_INFO_PREFIX = ""; 438 | }; 439 | name = Release; 440 | }; 441 | 2F32C68D1BD42D5000AD4419 /* Debug */ = { 442 | isa = XCBuildConfiguration; 443 | buildSettings = { 444 | CLANG_ENABLE_MODULES = YES; 445 | COMBINE_HIDPI_IMAGES = YES; 446 | DEFINES_MODULE = YES; 447 | DYLIB_COMPATIBILITY_VERSION = 1; 448 | DYLIB_CURRENT_VERSION = 1; 449 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 450 | FRAMEWORK_VERSION = A; 451 | INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; 452 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 453 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 454 | PRODUCT_BUNDLE_IDENTIFIER = com.tylermandry.AXSwift; 455 | PRODUCT_NAME = "$(TARGET_NAME)"; 456 | SKIP_INSTALL = YES; 457 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 458 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 459 | SWIFT_VERSION = 5.0; 460 | }; 461 | name = Debug; 462 | }; 463 | 2F32C68E1BD42D5000AD4419 /* Release */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | CLANG_ENABLE_MODULES = YES; 467 | COMBINE_HIDPI_IMAGES = YES; 468 | DEFINES_MODULE = YES; 469 | DYLIB_COMPATIBILITY_VERSION = 1; 470 | DYLIB_CURRENT_VERSION = 1; 471 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 472 | FRAMEWORK_VERSION = A; 473 | INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; 474 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 475 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 476 | PRODUCT_BUNDLE_IDENTIFIER = com.tylermandry.AXSwift; 477 | PRODUCT_NAME = "$(TARGET_NAME)"; 478 | SKIP_INSTALL = YES; 479 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 480 | SWIFT_VERSION = 5.0; 481 | }; 482 | name = Release; 483 | }; 484 | 2F86CEA21BD49958007E8C29 /* Debug */ = { 485 | isa = XCBuildConfiguration; 486 | buildSettings = { 487 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 488 | CODE_SIGN_IDENTITY = "-"; 489 | COMBINE_HIDPI_IMAGES = YES; 490 | INFOPLIST_FILE = AXSwiftExample/Info.plist; 491 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 492 | PRODUCT_BUNDLE_IDENTIFIER = com.tylermandry.AXSwiftExample; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | SWIFT_VERSION = 4.0; 495 | }; 496 | name = Debug; 497 | }; 498 | 2F86CEA31BD49958007E8C29 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 502 | CODE_SIGN_IDENTITY = "-"; 503 | COMBINE_HIDPI_IMAGES = YES; 504 | INFOPLIST_FILE = AXSwiftExample/Info.plist; 505 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 506 | PRODUCT_BUNDLE_IDENTIFIER = com.tylermandry.AXSwiftExample; 507 | PRODUCT_NAME = "$(TARGET_NAME)"; 508 | SWIFT_VERSION = 4.0; 509 | }; 510 | name = Release; 511 | }; 512 | 2F86CEB81BD5621F007E8C29 /* Debug */ = { 513 | isa = XCBuildConfiguration; 514 | buildSettings = { 515 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 516 | CODE_SIGN_IDENTITY = "-"; 517 | COMBINE_HIDPI_IMAGES = YES; 518 | INFOPLIST_FILE = AXSwiftObserverExample/Info.plist; 519 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 520 | PRODUCT_BUNDLE_IDENTIFIER = com.tylermandry.AXSwiftObserverExample; 521 | PRODUCT_NAME = "$(TARGET_NAME)"; 522 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 523 | SWIFT_VERSION = 4.0; 524 | }; 525 | name = Debug; 526 | }; 527 | 2F86CEB91BD5621F007E8C29 /* Release */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 531 | CODE_SIGN_IDENTITY = "-"; 532 | COMBINE_HIDPI_IMAGES = YES; 533 | INFOPLIST_FILE = AXSwiftObserverExample/Info.plist; 534 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 535 | PRODUCT_BUNDLE_IDENTIFIER = com.tylermandry.AXSwiftObserverExample; 536 | PRODUCT_NAME = "$(TARGET_NAME)"; 537 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 538 | SWIFT_VERSION = 4.0; 539 | }; 540 | name = Release; 541 | }; 542 | /* End XCBuildConfiguration section */ 543 | 544 | /* Begin XCConfigurationList section */ 545 | 2F32C67E1BD42D5000AD4419 /* Build configuration list for PBXProject "AXSwift" */ = { 546 | isa = XCConfigurationList; 547 | buildConfigurations = ( 548 | 2F32C68A1BD42D5000AD4419 /* Debug */, 549 | 2F32C68B1BD42D5000AD4419 /* Release */, 550 | ); 551 | defaultConfigurationIsVisible = 0; 552 | defaultConfigurationName = Release; 553 | }; 554 | 2F32C68C1BD42D5000AD4419 /* Build configuration list for PBXNativeTarget "AXSwift" */ = { 555 | isa = XCConfigurationList; 556 | buildConfigurations = ( 557 | 2F32C68D1BD42D5000AD4419 /* Debug */, 558 | 2F32C68E1BD42D5000AD4419 /* Release */, 559 | ); 560 | defaultConfigurationIsVisible = 0; 561 | defaultConfigurationName = Release; 562 | }; 563 | 2F86CEA41BD49958007E8C29 /* Build configuration list for PBXNativeTarget "AXSwiftExample" */ = { 564 | isa = XCConfigurationList; 565 | buildConfigurations = ( 566 | 2F86CEA21BD49958007E8C29 /* Debug */, 567 | 2F86CEA31BD49958007E8C29 /* Release */, 568 | ); 569 | defaultConfigurationIsVisible = 0; 570 | defaultConfigurationName = Release; 571 | }; 572 | 2F86CEB71BD5621F007E8C29 /* Build configuration list for PBXNativeTarget "AXSwiftObserverExample" */ = { 573 | isa = XCConfigurationList; 574 | buildConfigurations = ( 575 | 2F86CEB81BD5621F007E8C29 /* Debug */, 576 | 2F86CEB91BD5621F007E8C29 /* Release */, 577 | ); 578 | defaultConfigurationIsVisible = 0; 579 | defaultConfigurationName = Release; 580 | }; 581 | /* End XCConfigurationList section */ 582 | }; 583 | rootObject = 2F32C67B1BD42D5000AD4419 /* Project object */; 584 | } 585 | -------------------------------------------------------------------------------- /AXSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AXSwift.xcodeproj/xcshareddata/xcschemes/AXSwift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /AXSwiftExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import AXSwift 3 | 4 | class ApplicationDelegate: NSObject, NSApplicationDelegate { 5 | func applicationDidFinishLaunching(_ aNotification: Notification) { 6 | // Check that we have permission 7 | guard UIElement.isProcessTrusted(withPrompt: true) else { 8 | NSLog("No accessibility API permission, exiting") 9 | NSRunningApplication.current.terminate() 10 | return 11 | } 12 | 13 | // Get Active Application 14 | if let application = NSWorkspace.shared.frontmostApplication { 15 | NSLog("localizedName: \(String(describing: application.localizedName)), processIdentifier: \(application.processIdentifier)") 16 | let uiApp = Application(application)! 17 | NSLog("windows: \(String(describing: try! uiApp.windows()))") 18 | NSLog("attributes: \(try! uiApp.attributes())") 19 | NSLog("at 0,0: \(String(describing: try! uiApp.elementAtPosition(0, 0)))") 20 | if let bundleIdentifier = application.bundleIdentifier { 21 | NSLog("bundleIdentifier: \(bundleIdentifier)") 22 | let windows = try! Application.allForBundleID(bundleIdentifier).first!.windows() 23 | NSLog("windows: \(String(describing: windows))") 24 | } 25 | } 26 | 27 | // Get Application by bundleIdentifier 28 | let app = Application.allForBundleID("com.apple.finder").first! 29 | NSLog("finder: \(app)") 30 | NSLog("role: \(try! app.role()!)") 31 | NSLog("windows: \(try! app.windows()!)") 32 | NSLog("attributes: \(try! app.attributes())") 33 | if let title: String = try! app.attribute(.title) { 34 | NSLog("title: \(title)") 35 | } 36 | NSLog("multi: \(try! app.getMultipleAttributes(["AXRole", "asdf", "AXTitle"]))") 37 | NSLog("multi: \(try! app.getMultipleAttributes(.role, .title))") 38 | 39 | // Try to set an unsettable attribute 40 | if let window = try! app.windows()?.first { 41 | do { 42 | try window.setAttribute(.title, value: "my title") 43 | let newTitle: String? = try! window.attribute(.title) 44 | NSLog("title set; result = \(newTitle ?? "")") 45 | } catch { 46 | NSLog("error caught trying to set title of window: \(error)") 47 | } 48 | } 49 | 50 | NSLog("system wide:") 51 | NSLog("role: \(try! systemWideElement.role()!)") 52 | // NSLog("windows: \(try! sys.windows())") 53 | NSLog("attributes: \(try! systemWideElement.attributes())") 54 | 55 | NSRunningApplication.current.terminate() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /AXSwiftExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /AXSwiftExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2015 Tyler Mandry. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /AXSwiftExample/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let applicationDelegate = ApplicationDelegate() 4 | let application = NSApplication.shared 5 | application.setActivationPolicy(NSApplication.ActivationPolicy.accessory) 6 | application.delegate = applicationDelegate 7 | application.run() 8 | -------------------------------------------------------------------------------- /AXSwiftObserverExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import AXSwift 3 | 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | 6 | var observer: Observer! 7 | 8 | func applicationDidFinishLaunching(_ aNotification: Notification) { 9 | let app = Application.allForBundleID("com.apple.finder").first! 10 | 11 | do { 12 | try startWatcher(app) 13 | } catch let error { 14 | NSLog("Error: Could not watch app [\(app)]: \(error)") 15 | abort() 16 | } 17 | } 18 | 19 | func startWatcher(_ app: Application) throws { 20 | var updated = false 21 | observer = app.createObserver { (observer: Observer, element: UIElement, event: AXNotification, info: [String: AnyObject]?) in 22 | var elementDesc: String! 23 | if let role = try? element.role()!, role == .window { 24 | elementDesc = "\(element) \"\(try! (element.attribute(.title) as String?)!)\"" 25 | } else { 26 | elementDesc = "\(element)" 27 | } 28 | print("\(event) on \(String(describing: elementDesc)); info: \(info ?? [:])") 29 | 30 | // Watch events on new windows 31 | if event == .windowCreated { 32 | do { 33 | try observer.addNotification(.uiElementDestroyed, forElement: element) 34 | try observer.addNotification(.moved, forElement: element) 35 | } catch let error { 36 | NSLog("Error: Could not watch [\(element)]: \(error)") 37 | } 38 | } 39 | 40 | // Group simultaneous events together with --- lines 41 | if !updated { 42 | updated = true 43 | // Set this code to run after the current run loop, which is dispatching all notifications. 44 | DispatchQueue.main.async { 45 | print("---") 46 | updated = false 47 | } 48 | } 49 | } 50 | 51 | try observer.addNotification(.windowCreated, forElement: app) 52 | try observer.addNotification(.mainWindowChanged, forElement: app) 53 | } 54 | 55 | func applicationWillTerminate(_ aNotification: Notification) { 56 | // Insert code here to tear down your application 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /AXSwiftObserverExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /AXSwiftObserverExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2015 Tyler Mandry. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /AXSwiftObserverExample/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let applicationDelegate = AppDelegate() 4 | let application = NSApplication.shared 5 | application.setActivationPolicy(NSApplication.ActivationPolicy.accessory) 6 | application.delegate = applicationDelegate 7 | application.run() 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tyler Mandry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AXSwift", 7 | products: [ 8 | .library( 9 | name: "AXSwift", 10 | targets: ["AXSwift"]), 11 | ], 12 | targets: [ 13 | .target( 14 | name: "AXSwift", 15 | path: "Sources"), 16 | .target(name: "AXSwiftExample", 17 | dependencies: ["AXSwift"], 18 | path: "AXSwiftExample"), 19 | .target(name: "AXSwiftObserverExample", 20 | dependencies: ["AXSwift"], 21 | path: "AXSwiftObserverExample"), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AXSwift 2 | 3 | [![Version](https://cocoapod-badges.herokuapp.com/v/AXSwift/badge.svg)](https://cocoapods.org/pods/AXSwift) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | 6 | AXSwift is a Swift wrapper for macOS's C-based accessibility client APIs. Working with these APIs is 7 | error-prone and a huge pain, so AXSwift makes everything easier: 8 | 9 | - Modern API that's 100% Swift 10 | - Explicit error handling 11 | - Complete coverage of the underlying C API 12 | - Better documentation than Apple's, which is pretty poor 13 | 14 | This framework is intended as a basic wrapper, and doesn't keep any state or do any "magic". 15 | That's up to you! 16 | 17 | ## Using AXSwift 18 | 19 | ### SPM 20 | In your Package.swift: 21 | ``` 22 | .package(url: "https://github.com/tmandry/AXSwift", from: "0.3.0"), 23 | ``` 24 | 25 | ### Carthage 26 | In your Cartfile: 27 | ``` 28 | github "tmandry/AXSwift" ~> 0.3 29 | ``` 30 | 31 | ### CocoaPods 32 | In your Podfile: 33 | ``` 34 | pod 'AXSwift', '~> 0.3' 35 | ``` 36 | 37 | See the source of [AXSwiftExample](https://github.com/tmandry/AXSwift/blob/master/AXSwiftExample/AppDelegate.swift) 38 | and [AXSwiftObserverExample](https://github.com/tmandry/AXSwift/blob/master/AXSwiftObserverExample/AppDelegate.swift) 39 | for an example of the API. 40 | 41 | ## Related Projects 42 | 43 | [Swindler](https://github.com/tmandry/Swindler), a framework for building macOS window managers in Swift, 44 | is built on top of AXSwift. 45 | -------------------------------------------------------------------------------- /Sources/AXSwift.h: -------------------------------------------------------------------------------- 1 | // 2 | // AXSwift.h 3 | // AXSwift 4 | // 5 | // Created by Tyler Mandry on 10/18/15. 6 | // Copyright © 2015 Tyler Mandry. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AXSwift. 12 | FOUNDATION_EXPORT double AXSwiftVersionNumber; 13 | 14 | //! Project version string for AXSwift. 15 | FOUNDATION_EXPORT const unsigned char AXSwiftVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like 18 | // #import 19 | -------------------------------------------------------------------------------- /Sources/AXSwift.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @discardableResult 4 | public func checkIsProcessTrusted(prompt: Bool = false) -> Bool { 5 | let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String 6 | let opts = [promptKey: prompt] as CFDictionary 7 | return AXIsProcessTrustedWithOptions(opts) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Application.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | /// A `UIElement` for an application. 4 | public final class Application: UIElement { 5 | // Creates a UIElement for the given process ID. 6 | // Does NOT check if the given process actually exists, just checks for a valid ID. 7 | convenience init?(forKnownProcessID processID: pid_t) { 8 | let appElement = AXUIElementCreateApplication(processID) 9 | self.init(appElement) 10 | 11 | if processID < 0 { 12 | return nil 13 | } 14 | } 15 | 16 | /// Creates an `Application` from a `NSRunningApplication` instance. 17 | /// - returns: The `Application`, or `nil` if the given application is not running. 18 | public convenience init?(_ app: NSRunningApplication) { 19 | if app.isTerminated { 20 | return nil 21 | } 22 | self.init(forKnownProcessID: app.processIdentifier) 23 | } 24 | 25 | /// Create an `Application` from the process ID of a running application. 26 | /// - returns: The `Application`, or `nil` if the PID is invalid or the given application 27 | /// is not running. 28 | public convenience init?(forProcessID processID: pid_t) { 29 | guard let app = NSRunningApplication(processIdentifier: processID) else { 30 | return nil 31 | } 32 | self.init(app) 33 | } 34 | 35 | /// Creates an `Application` for every running application with a UI. 36 | /// - returns: An array of `Application`s. 37 | public class func all() -> [Application] { 38 | let runningApps = NSWorkspace.shared.runningApplications 39 | return runningApps 40 | .filter({ $0.activationPolicy != .prohibited }) 41 | .compactMap({ Application($0) }) 42 | } 43 | 44 | /// Creates an `Application` for every running instance of the given `bundleID`. 45 | /// - returns: A (potentially empty) array of `Application`s. 46 | public class func allForBundleID(_ bundleID: String) -> [Application] { 47 | let runningApps = NSWorkspace.shared.runningApplications 48 | return runningApps 49 | .filter({ $0.bundleIdentifier == bundleID }) 50 | .compactMap({ Application($0) }) 51 | } 52 | 53 | /// Creates an `Observer` on this application, if it is still alive. 54 | public func createObserver(_ callback: @escaping Observer.Callback) -> Observer? { 55 | do { 56 | return try Observer(processID: try pid(), callback: callback) 57 | } catch AXError.invalidUIElement { 58 | return nil 59 | } catch let error { 60 | fatalError("Caught unexpected error creating observer: \(error)") 61 | } 62 | } 63 | 64 | /// Creates an `Observer` on this application, if it is still alive. 65 | public func createObserver(_ callback: @escaping Observer.CallbackWithInfo) -> Observer? { 66 | do { 67 | return try Observer(processID: try pid(), callback: callback) 68 | } catch AXError.invalidUIElement { 69 | return nil 70 | } catch let error { 71 | fatalError("Caught unexpected error creating observer: \(error)") 72 | } 73 | } 74 | 75 | /// Returns a list of the application's visible windows. 76 | /// - returns: An array of `UIElement`s, one for every visible window. Or `nil` if the list 77 | /// cannot be retrieved. 78 | public func windows() throws -> [UIElement]? { 79 | let axWindows: [AXUIElement]? = try attribute("AXWindows") 80 | return axWindows?.map({ UIElement($0) }) 81 | } 82 | 83 | /// Returns the element at the specified top-down coordinates, or nil if there is none. 84 | public override func elementAtPosition(_ x: Float, _ y: Float) throws -> UIElement? { 85 | return try super.elementAtPosition(x, y) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Constants.swift: -------------------------------------------------------------------------------- 1 | /// All possible notifications you can subscribe to with `Observer`. 2 | /// - seeAlso: [Notificatons](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/c/data/NSAccessibilityAnnouncementRequestedNotification) 3 | public enum AXNotification: String { 4 | // Focus notifications 5 | case mainWindowChanged = "AXMainWindowChanged" 6 | case focusedWindowChanged = "AXFocusedWindowChanged" 7 | case focusedUIElementChanged = "AXFocusedUIElementChanged" 8 | case focusedTabChanged = "AXFocusedTabChanged" 9 | 10 | // Application notifications 11 | case applicationActivated = "AXApplicationActivated" 12 | case applicationDeactivated = "AXApplicationDeactivated" 13 | case applicationHidden = "AXApplicationHidden" 14 | case applicationShown = "AXApplicationShown" 15 | 16 | // Window notifications 17 | case windowCreated = "AXWindowCreated" 18 | case windowMoved = "AXWindowMoved" 19 | case windowResized = "AXWindowResized" 20 | case windowMiniaturized = "AXWindowMiniaturized" 21 | case windowDeminiaturized = "AXWindowDeminiaturized" 22 | 23 | // Drawer & sheet notifications 24 | case drawerCreated = "AXDrawerCreated" 25 | case sheetCreated = "AXSheetCreated" 26 | 27 | // Element notifications 28 | case uiElementDestroyed = "AXUIElementDestroyed" 29 | case valueChanged = "AXValueChanged" 30 | case titleChanged = "AXTitleChanged" 31 | case resized = "AXResized" 32 | case moved = "AXMoved" 33 | case created = "AXCreated" 34 | 35 | // Used when UI changes require the attention of assistive application. Pass along a user info 36 | // dictionary with the key NSAccessibilityUIElementsKey and an array of elements that have been 37 | // added or changed as a result of this layout change. 38 | case layoutChanged = "AXLayoutChanged" 39 | 40 | // Misc notifications 41 | case helpTagCreated = "AXHelpTagCreated" 42 | case selectedTextChanged = "AXSelectedTextChanged" 43 | case rowCountChanged = "AXRowCountChanged" 44 | case selectedChildrenChanged = "AXSelectedChildrenChanged" 45 | case selectedRowsChanged = "AXSelectedRowsChanged" 46 | case selectedColumnsChanged = "AXSelectedColumnsChanged" 47 | case loadComplete = "AXLoadComplete" 48 | 49 | case rowExpanded = "AXRowExpanded" 50 | case rowCollapsed = "AXRowCollapsed" 51 | 52 | // Cell-table notifications 53 | case selectedCellsChanged = "AXSelectedCellsChanged" 54 | 55 | // Layout area notifications 56 | case unitsChanged = "AXUnitsChanged" 57 | case selectedChildrenMoved = "AXSelectedChildrenMoved" 58 | 59 | // This notification allows an application to request that an announcement be made to the user 60 | // by an assistive application such as VoiceOver. The notification requires a user info 61 | // dictionary with the key NSAccessibilityAnnouncementKey and the announcement as a localized 62 | // string. In addition, the key NSAccessibilityAnnouncementPriorityKey should also be used to 63 | // help an assistive application determine the importance of this announcement. This 64 | // notification should be posted for the application element. 65 | case announcementRequested = "AXAnnouncementRequested" 66 | } 67 | 68 | /// All UIElement roles. 69 | /// - seeAlso: [Roles](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/doc/constant_group/Roles) 70 | public enum Role: String { 71 | case unknown = "AXUnknown" 72 | case button = "AXButton" 73 | case radioButton = "AXRadioButton" 74 | case checkBox = "AXCheckBox" 75 | case slider = "AXSlider" 76 | case tabGroup = "AXTabGroup" 77 | case textField = "AXTextField" 78 | case staticText = "AXStaticText" 79 | case textArea = "AXTextArea" 80 | case scrollArea = "AXScrollArea" 81 | case popUpButton = "AXPopUpButton" 82 | case menuButton = "AXMenuButton" 83 | case table = "AXTable" 84 | case application = "AXApplication" 85 | case group = "AXGroup" 86 | case radioGroup = "AXRadioGroup" 87 | case list = "AXList" 88 | case scrollBar = "AXScrollBar" 89 | case valueIndicator = "AXValueIndicator" 90 | case image = "AXImage" 91 | case menuBar = "AXMenuBar" 92 | case menu = "AXMenu" 93 | case menuItem = "AXMenuItem" 94 | case menuBarItem = "AXMenuBarItem" 95 | case column = "AXColumn" 96 | case row = "AXRow" 97 | case toolbar = "AXToolbar" 98 | case busyIndicator = "AXBusyIndicator" 99 | case progressIndicator = "AXProgressIndicator" 100 | case window = "AXWindow" 101 | case drawer = "AXDrawer" 102 | case systemWide = "AXSystemWide" 103 | case outline = "AXOutline" 104 | case incrementor = "AXIncrementor" 105 | case browser = "AXBrowser" 106 | case comboBox = "AXComboBox" 107 | case splitGroup = "AXSplitGroup" 108 | case splitter = "AXSplitter" 109 | case colorWell = "AXColorWell" 110 | case growArea = "AXGrowArea" 111 | case sheet = "AXSheet" 112 | case helpTag = "AXHelpTag" 113 | case matte = "AXMatte" 114 | case ruler = "AXRuler" 115 | case rulerMarker = "AXRulerMarker" 116 | case link = "AXLink" 117 | case disclosureTriangle = "AXDisclosureTriangle" 118 | case grid = "AXGrid" 119 | case relevanceIndicator = "AXRelevanceIndicator" 120 | case levelIndicator = "AXLevelIndicator" 121 | case cell = "AXCell" 122 | case popover = "AXPopover" 123 | case layoutArea = "AXLayoutArea" 124 | case layoutItem = "AXLayoutItem" 125 | case handle = "AXHandle" 126 | } 127 | 128 | /// All UIElement subroles. 129 | /// - seeAlso: [Subroles](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/doc/constant_group/Subroles) 130 | public enum Subrole: String { 131 | case unknown = "AXUnknown" 132 | case closeButton = "AXCloseButton" 133 | case zoomButton = "AXZoomButton" 134 | case minimizeButton = "AXMinimizeButton" 135 | case toolbarButton = "AXToolbarButton" 136 | case tableRow = "AXTableRow" 137 | case outlineRow = "AXOutlineRow" 138 | case secureTextField = "AXSecureTextField" 139 | case standardWindow = "AXStandardWindow" 140 | case dialog = "AXDialog" 141 | case systemDialog = "AXSystemDialog" 142 | case floatingWindow = "AXFloatingWindow" 143 | case systemFloatingWindow = "AXSystemFloatingWindow" 144 | case incrementArrow = "AXIncrementArrow" 145 | case decrementArrow = "AXDecrementArrow" 146 | case incrementPage = "AXIncrementPage" 147 | case decrementPage = "AXDecrementPage" 148 | case searchField = "AXSearchField" 149 | case textAttachment = "AXTextAttachment" 150 | case textLink = "AXTextLink" 151 | case timeline = "AXTimeline" 152 | case sortButton = "AXSortButton" 153 | case ratingIndicator = "AXRatingIndicator" 154 | case contentList = "AXContentList" 155 | case definitionList = "AXDefinitionList" 156 | case fullScreenButton = "AXFullScreenButton" 157 | case toggle = "AXToggle" 158 | case switchSubrole = "AXSwitch" 159 | case descriptionList = "AXDescriptionList" 160 | } 161 | 162 | /// Orientations returned by the orientation property. 163 | /// - seeAlso: [NSAccessibilityOrientation](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/c/tdef/NSAccessibilityOrientation) 164 | public enum Orientation: Int { 165 | case unknown = 0 166 | case vertical = 1 167 | case horizontal = 2 168 | } 169 | 170 | public enum Attribute: String { 171 | // Standard attributes 172 | case role = "AXRole" //(NSString *) - type, non-localized (e.g. radioButton) 173 | case roleDescription = "AXRoleDescription" //(NSString *) - user readable role (e.g. "radio button") 174 | case subrole = "AXSubrole" //(NSString *) - type, non-localized (e.g. closeButton) 175 | case help = "AXHelp" //(NSString *) - instance description (e.g. a tool tip) 176 | case value = "AXValue" //(id) - element's value 177 | case minValue = "AXMinValue" //(id) - element's min value 178 | case maxValue = "AXMaxValue" //(id) - element's max value 179 | case enabled = "AXEnabled" //(NSNumber *) - (boolValue) responds to user? 180 | case focused = "AXFocused" //(NSNumber *) - (boolValue) has keyboard focus? 181 | case parent = "AXParent" //(id) - element containing you 182 | case children = "AXChildren" //(NSArray *) - elements you contain 183 | case window = "AXWindow" //(id) - UIElement for the containing window 184 | case topLevelUIElement = "AXTopLevelUIElement" //(id) - UIElement for the containing top level element 185 | case selectedChildren = "AXSelectedChildren" //(NSArray *) - child elements which are selected 186 | case visibleChildren = "AXVisibleChildren" //(NSArray *) - child elements which are visible 187 | case position = "AXPosition" //(NSValue *) - (pointValue) position in screen coords 188 | case size = "AXSize" //(NSValue *) - (sizeValue) size 189 | case frame = "AXFrame" //(NSValue *) - (rectValue) frame 190 | case contents = "AXContents" //(NSArray *) - main elements 191 | case title = "AXTitle" //(NSString *) - visible text (e.g. of a push button) 192 | case description = "AXDescription" //(NSString *) - instance description 193 | case shownMenu = "AXShownMenu" //(id) - menu being displayed 194 | case valueDescription = "AXValueDescription" //(NSString *) - text description of value 195 | 196 | case sharedFocusElements = "AXSharedFocusElements" //(NSArray *) - elements that share focus 197 | 198 | // Misc attributes 199 | case previousContents = "AXPreviousContents" //(NSArray *) - main elements 200 | case nextContents = "AXNextContents" //(NSArray *) - main elements 201 | case header = "AXHeader" //(id) - UIElement for header. 202 | case edited = "AXEdited" //(NSNumber *) - (boolValue) is it dirty? 203 | case tabs = "AXTabs" //(NSArray *) - UIElements for tabs 204 | case horizontalScrollBar = "AXHorizontalScrollBar" //(id) - UIElement for the horizontal scroller 205 | case verticalScrollBar = "AXVerticalScrollBar" //(id) - UIElement for the vertical scroller 206 | case overflowButton = "AXOverflowButton" //(id) - UIElement for overflow 207 | case incrementButton = "AXIncrementButton" //(id) - UIElement for increment 208 | case decrementButton = "AXDecrementButton" //(id) - UIElement for decrement 209 | case filename = "AXFilename" //(NSString *) - filename 210 | case expanded = "AXExpanded" //(NSNumber *) - (boolValue) is expanded? 211 | case selected = "AXSelected" //(NSNumber *) - (boolValue) is selected? 212 | case splitters = "AXSplitters" //(NSArray *) - UIElements for splitters 213 | case document = "AXDocument" //(NSString *) - url as string - for open document 214 | case activationPoint = "AXActivationPoint" //(NSValue *) - (pointValue) 215 | 216 | case url = "AXURL" //(NSURL *) - url 217 | case index = "AXIndex" //(NSNumber *) - (intValue) 218 | 219 | case rowCount = "AXRowCount" //(NSNumber *) - (intValue) number of rows 220 | 221 | case columnCount = "AXColumnCount" //(NSNumber *) - (intValue) number of columns 222 | 223 | case orderedByRow = "AXOrderedByRow" //(NSNumber *) - (boolValue) is ordered by row? 224 | 225 | case warningValue = "AXWarningValue" //(id) - warning value of a level indicator, typically a number 226 | 227 | case criticalValue = "AXCriticalValue" //(id) - critical value of a level indicator, typically a number 228 | 229 | case placeholderValue = "AXPlaceholderValue" //(NSString *) - placeholder value of a control such as a text field 230 | 231 | case containsProtectedContent = "AXContainsProtectedContent" // (NSNumber *) - (boolValue) contains protected content? 232 | case alternateUIVisible = "AXAlternateUIVisible" //(NSNumber *) - (boolValue) 233 | 234 | // Linkage attributes 235 | case titleUIElement = "AXTitleUIElement" //(id) - UIElement for the title 236 | case servesAsTitleForUIElements = "AXServesAsTitleForUIElements" //(NSArray *) - UIElements this titles 237 | case linkedUIElements = "AXLinkedUIElements" //(NSArray *) - corresponding UIElements 238 | 239 | // Text-specific attributes 240 | case selectedText = "AXSelectedText" //(NSString *) - selected text 241 | case selectedTextRange = "AXSelectedTextRange" //(NSValue *) - (rangeValue) range of selected text 242 | case numberOfCharacters = "AXNumberOfCharacters" //(NSNumber *) - number of characters 243 | case visibleCharacterRange = "AXVisibleCharacterRange" //(NSValue *) - (rangeValue) range of visible text 244 | case sharedTextUIElements = "AXSharedTextUIElements" //(NSArray *) - text views sharing text 245 | case sharedCharacterRange = "AXSharedCharacterRange" //(NSValue *) - (rangeValue) part of shared text in this view 246 | case insertionPointLineNumber = "AXInsertionPointLineNumber" //(NSNumber *) - line# containing caret 247 | case selectedTextRanges = "AXSelectedTextRanges" //(NSArray *) - array of NSValue (rangeValue) ranges of selected text 248 | /// - note: private/undocumented attribute 249 | case textInputMarkedRange = "AXTextInputMarkedRange" 250 | 251 | // Parameterized text-specific attributes 252 | case lineForIndexParameterized = "AXLineForIndexParameterized" //(NSNumber *) - line# for char index; param:(NSNumber *) 253 | case rangeForLineParameterized = "AXRangeForLineParameterized" //(NSValue *) - (rangeValue) range of line; param:(NSNumber *) 254 | case stringForRangeParameterized = "AXStringForRangeParameterized" //(NSString *) - substring; param:(NSValue * - rangeValue) 255 | case rangeForPositionParameterized = "AXRangeForPositionParameterized" //(NSValue *) - (rangeValue) composed char range; param:(NSValue * - pointValue) 256 | case rangeForIndexParameterized = "AXRangeForIndexParameterized" //(NSValue *) - (rangeValue) composed char range; param:(NSNumber *) 257 | case boundsForRangeParameterized = "AXBoundsForRangeParameterized" //(NSValue *) - (rectValue) bounds of text; param:(NSValue * - rangeValue) 258 | case rtfForRangeParameterized = "AXRTFForRangeParameterized" //(NSData *) - rtf for text; param:(NSValue * - rangeValue) 259 | case styleRangeForIndexParameterized = "AXStyleRangeForIndexParameterized" //(NSValue *) - (rangeValue) extent of style run; param:(NSNumber *) 260 | case attributedStringForRangeParameterized = "AXAttributedStringForRangeParameterized" //(NSAttributedString *) - does _not_ use attributes from Appkit/AttributedString.h 261 | 262 | // Text attributed string attributes and constants 263 | case fontText = "AXFontText" //(NSDictionary *) - NSAccessibilityFontXXXKey's 264 | case foregroundColorText = "AXForegroundColorText" //CGColorRef 265 | case backgroundColorText = "AXBackgroundColorText" //CGColorRef 266 | case underlineColorText = "AXUnderlineColorText" //CGColorRef 267 | case strikethroughColorText = "AXStrikethroughColorText" //CGColorRef 268 | case underlineText = "AXUnderlineText" //(NSNumber *) - underline style 269 | case superscriptText = "AXSuperscriptText" //(NSNumber *) - superscript>0, subscript<0 270 | case strikethroughText = "AXStrikethroughText" //(NSNumber *) - (boolValue) 271 | case shadowText = "AXShadowText" //(NSNumber *) - (boolValue) 272 | case attachmentText = "AXAttachmentText" //id - corresponding element 273 | case linkText = "AXLinkText" //id - corresponding element 274 | case autocorrectedText = "AXAutocorrectedText" //(NSNumber *) - (boolValue) 275 | 276 | // Textual list attributes and constants. Examples: unordered or ordered lists in a document. 277 | case listItemPrefixText = "AXListItemPrefixText" // NSAttributedString, the prepended string of the list item. If the string is a common unicode character (e.g. a bullet •), return that unicode character. For lists with images before the text, return a reasonable label of the image. 278 | case listItemIndexText = "AXListItemIndexText" // NSNumber, integerValue of the line index. Each list item increments the index, even for unordered lists. The first item should have index 0. 279 | case listItemLevelText = "AXListItemLevelText" // NSNumber, integerValue of the indent level. Each sublist increments the level. The first item should have level 0. 280 | 281 | // MisspelledText attributes 282 | case misspelledText = "AXMisspelledText" //(NSNumber *) - (boolValue) 283 | case markedMisspelledText = "AXMarkedMisspelledText" //(NSNumber *) - (boolValue) 284 | 285 | // Window-specific attributes 286 | case main = "AXMain" //(NSNumber *) - (boolValue) is it the main window? 287 | case minimized = "AXMinimized" //(NSNumber *) - (boolValue) is window minimized? 288 | case closeButton = "AXCloseButton" //(id) - UIElement for close box (or nil) 289 | case zoomButton = "AXZoomButton" //(id) - UIElement for zoom box (or nil) 290 | case minimizeButton = "AXMinimizeButton" //(id) - UIElement for miniaturize box (or nil) 291 | case toolbarButton = "AXToolbarButton" //(id) - UIElement for toolbar box (or nil) 292 | case proxy = "AXProxy" //(id) - UIElement for title's icon (or nil) 293 | case growArea = "AXGrowArea" //(id) - UIElement for grow box (or nil) 294 | case modal = "AXModal" //(NSNumber *) - (boolValue) is the window modal 295 | case defaultButton = "AXDefaultButton" //(id) - UIElement for default button 296 | case cancelButton = "AXCancelButton" //(id) - UIElement for cancel button 297 | case fullScreenButton = "AXFullScreenButton" //(id) - UIElement for full screen button (or nil) 298 | /// - note: private/undocumented attribute 299 | case fullScreen = "AXFullScreen" //(NSNumber *) - (boolValue) is the window fullscreen 300 | 301 | // Application-specific attributes 302 | case menuBar = "AXMenuBar" //(id) - UIElement for the menu bar 303 | case windows = "AXWindows" //(NSArray *) - UIElements for the windows 304 | case frontmost = "AXFrontmost" //(NSNumber *) - (boolValue) is the app active? 305 | case hidden = "AXHidden" //(NSNumber *) - (boolValue) is the app hidden? 306 | case mainWindow = "AXMainWindow" //(id) - UIElement for the main window. 307 | case focusedWindow = "AXFocusedWindow" //(id) - UIElement for the key window. 308 | case focusedUIElement = "AXFocusedUIElement" //(id) - Currently focused UIElement. 309 | case extrasMenuBar = "AXExtrasMenuBar" //(id) - UIElement for the application extras menu bar. 310 | /// - note: private/undocumented attribute 311 | case enhancedUserInterface = "AXEnhancedUserInterface" //(NSNumber *) - (boolValue) is the enhanced user interface active? 312 | 313 | case orientation = "AXOrientation" //(NSString *) - NSAccessibilityXXXOrientationValue 314 | 315 | case columnTitles = "AXColumnTitles" //(NSArray *) - UIElements for titles 316 | 317 | case searchButton = "AXSearchButton" //(id) - UIElement for search field search btn 318 | case searchMenu = "AXSearchMenu" //(id) - UIElement for search field menu 319 | case clearButton = "AXClearButton" //(id) - UIElement for search field clear btn 320 | 321 | // Table/outline view attributes 322 | case rows = "AXRows" //(NSArray *) - UIElements for rows 323 | case visibleRows = "AXVisibleRows" //(NSArray *) - UIElements for visible rows 324 | case selectedRows = "AXSelectedRows" //(NSArray *) - UIElements for selected rows 325 | case columns = "AXColumns" //(NSArray *) - UIElements for columns 326 | case visibleColumns = "AXVisibleColumns" //(NSArray *) - UIElements for visible columns 327 | case selectedColumns = "AXSelectedColumns" //(NSArray *) - UIElements for selected columns 328 | case sortDirection = "AXSortDirection" //(NSString *) - see sort direction values below 329 | 330 | // Cell-based table attributes 331 | case selectedCells = "AXSelectedCells" //(NSArray *) - UIElements for selected cells 332 | case visibleCells = "AXVisibleCells" //(NSArray *) - UIElements for visible cells 333 | case rowHeaderUIElements = "AXRowHeaderUIElements" //(NSArray *) - UIElements for row headers 334 | case columnHeaderUIElements = "AXColumnHeaderUIElements" //(NSArray *) - UIElements for column headers 335 | 336 | // Cell-based table parameterized attributes. The parameter for this attribute is an NSArray containing two NSNumbers, the first NSNumber specifies the column index, the second NSNumber specifies the row index. 337 | case cellForColumnAndRowParameterized = "AXCellForColumnAndRowParameterized" // (id) - UIElement for cell at specified row and column 338 | 339 | // Cell attributes. The index range contains both the starting index, and the index span in a table. 340 | case rowIndexRange = "AXRowIndexRange" //(NSValue *) - (rangeValue) location and row span 341 | case columnIndexRange = "AXColumnIndexRange" //(NSValue *) - (rangeValue) location and column span 342 | 343 | // Layout area attributes 344 | case horizontalUnits = "AXHorizontalUnits" //(NSString *) - see ruler unit values below 345 | case verticalUnits = "AXVerticalUnits" //(NSString *) - see ruler unit values below 346 | case horizontalUnitDescription = "AXHorizontalUnitDescription" //(NSString *) 347 | case verticalUnitDescription = "AXVerticalUnitDescription" //(NSString *) 348 | 349 | // Layout area parameterized attributes 350 | case layoutPointForScreenPointParameterized = "AXLayoutPointForScreenPointParameterized" //(NSValue *) - (pointValue); param:(NSValue * - pointValue) 351 | case layoutSizeForScreenSizeParameterized = "AXLayoutSizeForScreenSizeParameterized" //(NSValue *) - (sizeValue); param:(NSValue * - sizeValue) 352 | case screenPointForLayoutPointParameterized = "AXScreenPointForLayoutPointParameterized" //(NSValue *) - (pointValue); param:(NSValue * - pointValue) 353 | case screenSizeForLayoutSizeParameterized = "AXScreenSizeForLayoutSizeParameterized" //(NSValue *) - (sizeValue); param:(NSValue * - sizeValue) 354 | 355 | // Layout item attributes 356 | case handles = "AXHandles" //(NSArray *) - UIElements for handles 357 | 358 | // Outline attributes 359 | case disclosing = "AXDisclosing" //(NSNumber *) - (boolValue) is disclosing rows? 360 | case disclosedRows = "AXDisclosedRows" //(NSArray *) - UIElements for disclosed rows 361 | case disclosedByRow = "AXDisclosedByRow" //(id) - UIElement for disclosing row 362 | case disclosureLevel = "AXDisclosureLevel" //(NSNumber *) - indentation level 363 | 364 | // Slider attributes 365 | case allowedValues = "AXAllowedValues" //(NSArray *) - array of allowed values 366 | case labelUIElements = "AXLabelUIElements" //(NSArray *) - array of label UIElements 367 | case labelValue = "AXLabelValue" //(NSNumber *) - value of a label UIElement 368 | 369 | // Matte attributes 370 | // Attributes no longer supported 371 | case matteHole = "AXMatteHole" //(NSValue *) - (rect value) bounds of matte hole in screen coords 372 | case matteContentUIElement = "AXMatteContentUIElement" //(id) - UIElement clipped by the matte 373 | 374 | // Ruler view attributes 375 | case markerUIElements = "AXMarkerUIElements" //(NSArray *) 376 | case markerValues = "AXMarkerValues" // 377 | case markerGroupUIElement = "AXMarkerGroupUIElement" //(id) 378 | case units = "AXUnits" //(NSString *) - see ruler unit values below 379 | case unitDescription = "AXUnitDescription" //(NSString *) 380 | case markerType = "AXMarkerType" //(NSString *) - see ruler marker type values below 381 | case markerTypeDescription = "AXMarkerTypeDescription" //(NSString *) 382 | 383 | // UI element identification attributes 384 | case identifier = "AXIdentifier" //(NSString *) 385 | 386 | // System-wide attributes 387 | case focusedApplication = "AXFocusedApplication" 388 | 389 | // Unknown attributes 390 | case functionRowTopLevelElements = "AXFunctionRowTopLevelElements" 391 | case childrenInNavigationOrder = "AXChildrenInNavigationOrder" 392 | } 393 | 394 | /// All actions a `UIElement` can support. 395 | /// - seeAlso: [Actions](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSAccessibility_Protocol/#//apple_ref/doc/constant_group/Actions) 396 | public enum Action: String { 397 | case press = "AXPress" 398 | case increment = "AXIncrement" 399 | case decrement = "AXDecrement" 400 | case confirm = "AXConfirm" 401 | case pick = "AXPick" 402 | case cancel = "AXCancel" 403 | case raise = "AXRaise" 404 | case showMenu = "AXShowMenu" 405 | case delete = "AXDelete" 406 | case showAlternateUI = "AXShowAlternateUI" 407 | case showDefaultUI = "AXShowDefaultUI" 408 | } 409 | -------------------------------------------------------------------------------- /Sources/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | extension AXError: Swift.Error {} 5 | 6 | // For some reason values don't get described in this enum, so we have to do it manually. 7 | extension AXError: CustomStringConvertible { 8 | fileprivate var valueAsString: String { 9 | switch self { 10 | case .success: 11 | return "Success" 12 | case .failure: 13 | return "Failure" 14 | case .illegalArgument: 15 | return "IllegalArgument" 16 | case .invalidUIElement: 17 | return "InvalidUIElement" 18 | case .invalidUIElementObserver: 19 | return "InvalidUIElementObserver" 20 | case .cannotComplete: 21 | return "CannotComplete" 22 | case .attributeUnsupported: 23 | return "AttributeUnsupported" 24 | case .actionUnsupported: 25 | return "ActionUnsupported" 26 | case .notificationUnsupported: 27 | return "NotificationUnsupported" 28 | case .notImplemented: 29 | return "NotImplemented" 30 | case .notificationAlreadyRegistered: 31 | return "NotificationAlreadyRegistered" 32 | case .notificationNotRegistered: 33 | return "NotificationNotRegistered" 34 | case .apiDisabled: 35 | return "APIDisabled" 36 | case .noValue: 37 | return "NoValue" 38 | case .parameterizedAttributeUnsupported: 39 | return "ParameterizedAttributeUnsupported" 40 | case .notEnoughPrecision: 41 | return "NotEnoughPrecision" 42 | @unknown default: 43 | return "" 44 | } 45 | } 46 | 47 | public var description: String { 48 | return "AXError.\(valueAsString)" 49 | } 50 | } 51 | 52 | /// All possible errors that could be returned from UIElement or one of its subclasses. 53 | /// 54 | /// These are just the errors that can be returned from the underlying API. 55 | /// 56 | /// - seeAlso: [AXUIElement.h Reference](https://developer.apple.com/library/mac/documentation/ApplicationServices/Reference/AXUIElement_header_reference/) 57 | /// - seeAlso: `UIElement` for a list of errors that you should handle 58 | //public typealias Error = AXError 59 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 Tyler Mandry. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/Observer.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | import Darwin 4 | 5 | /// Observers watch for events on an application's UI elements. 6 | /// 7 | /// Events are received as part of the application's default run loop. 8 | /// 9 | /// - seeAlso: `UIElement` for a list of exceptions that can be thrown. 10 | public final class Observer { 11 | public typealias Callback = (_ observer: Observer, 12 | _ element: UIElement, 13 | _ notification: AXNotification) -> Void 14 | public typealias CallbackWithInfo = (_ observer: Observer, 15 | _ element: UIElement, 16 | _ notification: AXNotification, 17 | _ info: [String: AnyObject]?) -> Void 18 | 19 | let pid: pid_t 20 | let axObserver: AXObserver! 21 | let callback: Callback? 22 | let callbackWithInfo: CallbackWithInfo? 23 | 24 | public fileprivate(set) lazy var application: Application = 25 | Application(forKnownProcessID: self.pid)! 26 | 27 | /// Creates and starts an observer on the given `processID`. 28 | public init(processID: pid_t, callback: @escaping Callback) throws { 29 | var axObserver: AXObserver? 30 | let error = AXObserverCreate(processID, internalCallback, &axObserver) 31 | 32 | pid = processID 33 | self.axObserver = axObserver 34 | self.callback = callback 35 | callbackWithInfo = nil 36 | 37 | guard error == .success else { 38 | throw error 39 | } 40 | assert(axObserver != nil) 41 | 42 | start() 43 | } 44 | 45 | /// Creates and starts an observer on the given `processID`. 46 | /// 47 | /// Use this initializer if you want the extra user info provided with notifications. 48 | /// - seeAlso: [UserInfo Keys for Posting Accessibility Notifications](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/doc/constant_group/UserInfo_Keys_for_Posting_Accessibility_Notifications) 49 | public init(processID: pid_t, callback: @escaping CallbackWithInfo) throws { 50 | var axObserver: AXObserver? 51 | let error = AXObserverCreateWithInfoCallback(processID, internalInfoCallback, &axObserver) 52 | 53 | pid = processID 54 | self.axObserver = axObserver 55 | self.callback = nil 56 | callbackWithInfo = callback 57 | 58 | guard error == .success else { 59 | throw error 60 | } 61 | assert(axObserver != nil) 62 | 63 | start() 64 | } 65 | 66 | deinit { 67 | stop() 68 | } 69 | 70 | /// Starts watching for events. You don't need to call this method unless you use `stop()`. 71 | /// 72 | /// If the observer has already been started, this method does nothing. 73 | public func start() { 74 | CFRunLoopAddSource( 75 | RunLoop.current.getCFRunLoop(), 76 | AXObserverGetRunLoopSource(axObserver), 77 | CFRunLoopMode.defaultMode) 78 | } 79 | 80 | /// Stops sending events to your callback until the next call to `start`. 81 | /// 82 | /// If the observer has already been started, this method does nothing. 83 | /// 84 | /// - important: Events will still be queued in the target process until the Observer is started 85 | /// again or destroyed. If you don't want them, create a new Observer. 86 | public func stop() { 87 | CFRunLoopRemoveSource( 88 | RunLoop.current.getCFRunLoop(), 89 | AXObserverGetRunLoopSource(axObserver), 90 | CFRunLoopMode.defaultMode) 91 | } 92 | 93 | /// Adds a notification for the observer to watch. 94 | /// 95 | /// - parameter notification: The name of the notification to watch for. 96 | /// - parameter forElement: The element to watch for the notification on. Must belong to the 97 | /// application this observer was created on. 98 | /// - seeAlso: [Notificatons](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/c/data/NSAccessibilityAnnouncementRequestedNotification) 99 | /// - note: The underlying API returns an error if the notification is already added, but that 100 | /// error is not passed on for consistency with `start()` and `stop()`. 101 | /// - throws: `Error.NotificationUnsupported`: The element does not support notifications (note 102 | /// that the system-wide element does not support notifications). 103 | public func addNotification(_ notification: AXNotification, 104 | forElement element: UIElement) throws { 105 | let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) 106 | let error = AXObserverAddNotification( 107 | axObserver, element.element, notification.rawValue as CFString, selfPtr 108 | ) 109 | guard error == .success || error == .notificationAlreadyRegistered else { 110 | throw error 111 | } 112 | } 113 | 114 | /// Removes a notification from the observer. 115 | /// 116 | /// - parameter notification: The name of the notification to stop watching. 117 | /// - parameter forElement: The element to stop watching the notification on. 118 | /// - note: The underlying API returns an error if the notification is not present, but that 119 | /// error is not passed on for consistency with `start()` and `stop()`. 120 | /// - throws: `Error.NotificationUnsupported`: The element does not support notifications (note 121 | /// that the system-wide element does not support notifications). 122 | public func removeNotification(_ notification: AXNotification, 123 | forElement element: UIElement) throws { 124 | let error = AXObserverRemoveNotification( 125 | axObserver, element.element, notification.rawValue as CFString 126 | ) 127 | guard error == .success || error == .notificationNotRegistered else { 128 | throw error 129 | } 130 | } 131 | } 132 | 133 | private func internalCallback(_ axObserver: AXObserver, 134 | axElement: AXUIElement, 135 | notification: CFString, 136 | userData: UnsafeMutableRawPointer?) { 137 | guard let userData = userData else { fatalError("userData should be an AXSwift.Observer") } 138 | 139 | let observer = Unmanaged.fromOpaque(userData).takeUnretainedValue() 140 | let element = UIElement(axElement) 141 | guard let notif = AXNotification(rawValue: notification as String) else { 142 | NSLog("Unknown AX notification %s received", notification as String) 143 | return 144 | } 145 | observer.callback!(observer, element, notif) 146 | } 147 | 148 | private func internalInfoCallback(_ axObserver: AXObserver, 149 | axElement: AXUIElement, 150 | notification: CFString, 151 | cfInfo: CFDictionary, 152 | userData: UnsafeMutableRawPointer?) { 153 | guard let userData = userData else { fatalError("userData should be an AXSwift.Observer") } 154 | 155 | let observer = Unmanaged.fromOpaque(userData).takeUnretainedValue() 156 | let element = UIElement(axElement) 157 | let info = cfInfo as NSDictionary? as! [String: AnyObject]? 158 | guard let notif = AXNotification(rawValue: notification as String) else { 159 | NSLog("Unknown AX notification %s received", notification as String) 160 | return 161 | } 162 | observer.callbackWithInfo!(observer, element, notif, info) 163 | } 164 | -------------------------------------------------------------------------------- /Sources/SystemWideElement.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | /// A singleton for the system-wide element. 5 | public var systemWideElement = SystemWideElement() 6 | 7 | /// A `UIElement` for the system-wide accessibility element, which can be used to retrieve global, 8 | /// application-inspecific parameters like the currently focused element. 9 | open class SystemWideElement: UIElement { 10 | fileprivate convenience init() { 11 | self.init(AXUIElementCreateSystemWide()) 12 | } 13 | 14 | /// Returns the element at the specified top-down coordinates, or nil if there is none. 15 | open override func elementAtPosition(_ x: Float, _ y: Float) throws -> UIElement? { 16 | return try super.elementAtPosition(x, y) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/UIElement.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Foundation 3 | 4 | /// Holds and interacts with any accessibility element. 5 | /// 6 | /// This class wraps every operation that operates on AXUIElements. 7 | /// 8 | /// - seeAlso: [OS X Accessibility Model](https://developer.apple.com/library/mac/documentation/Accessibility/Conceptual/AccessibilityMacOSX/OSXAXmodel.html) 9 | /// 10 | /// Note that every operation involves IPC and is tied to the event loop of the target process. This 11 | /// means that operations are synchronous and can hang until they time out. The default timeout is 12 | /// 6 seconds, but it can be changed using `setMessagingTimeout` and `setGlobalMessagingTimeout`. 13 | /// 14 | /// Every attribute- or action-related function has an enum version and a String version. This is 15 | /// because certain processes might report attributes or actions not documented in the standard API. 16 | /// These will be ignored by enum functions (and you can't specify them). Most users will want to 17 | /// use the enum-based versions, but if you want to be exhaustive or use non-standard attributes and 18 | /// actions, you can use the String versions. 19 | /// 20 | /// ### Error handling 21 | /// 22 | /// Unless otherwise specified, during reads, "missing data/attribute" errors are handled by 23 | /// returning optionals as nil. During writes, missing attribute errors are thrown. 24 | /// 25 | /// Other failures are all thrown, including if messaging fails or the underlying AXUIElement 26 | /// becomes invalid. 27 | /// 28 | /// #### Possible Errors 29 | /// - `Error.APIDisabled`: The accessibility API is disabled. Your application must request and 30 | /// receive special permission from the user to be able to use these APIs. 31 | /// - `Error.InvalidUIElement`: The UI element has become invalid, perhaps because it was destroyed. 32 | /// - `Error.CannotComplete`: There is a problem with messaging, perhaps because the application is 33 | /// being unresponsive. This error will be thrown when a message times 34 | /// out. 35 | /// - `Error.NotImplemented`: The process does not fully support the accessibility API. 36 | /// - Anything included in the docs of the method you are calling. 37 | /// 38 | /// Any undocumented errors thrown are bugs and should be reported. 39 | /// 40 | /// - seeAlso: [AXUIElement.h reference](https://developer.apple.com/library/mac/documentation/ApplicationServices/Reference/AXUIElement_header_reference/) 41 | open class UIElement { 42 | public let element: AXUIElement 43 | 44 | /// Create a UIElement from a raw AXUIElement object. 45 | /// 46 | /// The state and role of the AXUIElement is not checked. 47 | public required init(_ nativeElement: AXUIElement) { 48 | // Since we are dealing with low-level C APIs, it never hurts to double check types. 49 | assert(CFGetTypeID(nativeElement) == AXUIElementGetTypeID(), 50 | "nativeElement is not an AXUIElement") 51 | 52 | element = nativeElement 53 | } 54 | 55 | /// Checks if the current process is a trusted accessibility client. If false, all APIs will 56 | /// throw errors. 57 | /// 58 | /// - parameter withPrompt: Whether to show the user a prompt if the process is untrusted. This 59 | /// happens asynchronously and does not affect the return value. 60 | open class func isProcessTrusted(withPrompt showPrompt: Bool = false) -> Bool { 61 | let options = [ 62 | kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: showPrompt as CFBoolean 63 | ] 64 | return AXIsProcessTrustedWithOptions(options as CFDictionary) 65 | } 66 | 67 | /// Timeout in seconds for all UIElement messages. Use this to control how long a method call 68 | /// can delay execution. The default is `0` which means to use the system default. 69 | open class var globalMessagingTimeout: Float { 70 | get { return systemWideElement.messagingTimeout } 71 | set { systemWideElement.messagingTimeout = newValue } 72 | } 73 | 74 | // MARK: - Attributes 75 | 76 | /// Returns the list of all attributes. 77 | /// 78 | /// Does not include parameterized attributes. 79 | open func attributes() throws -> [Attribute] { 80 | let attrs = try attributesAsStrings() 81 | for attr in attrs where Attribute(rawValue: attr) == nil { 82 | print("Unrecognized attribute: \(attr)") 83 | } 84 | return attrs.compactMap({ Attribute(rawValue: $0) }) 85 | } 86 | 87 | // This version is named differently so the caller doesn't have to specify the return type when 88 | // using the enum version. 89 | open func attributesAsStrings() throws -> [String] { 90 | var names: CFArray? 91 | let error = AXUIElementCopyAttributeNames(element, &names) 92 | 93 | if error == .noValue || error == .attributeUnsupported { 94 | return [] 95 | } 96 | 97 | guard error == .success else { 98 | throw error 99 | } 100 | 101 | // We must first convert the CFArray to a native array, then downcast to an array of 102 | // strings. 103 | return names! as [AnyObject] as! [String] 104 | } 105 | 106 | /// Returns whether `attribute` is supported by this element. 107 | /// 108 | /// The `attribute` method returns nil for unsupported attributes and empty attributes alike, 109 | /// which is more convenient than dealing with exceptions (which are used for more serious 110 | /// errors). However, if you'd like to specifically test an attribute is actually supported, you 111 | /// can use this method. 112 | open func attributeIsSupported(_ attribute: Attribute) throws -> Bool { 113 | return try attributeIsSupported(attribute.rawValue) 114 | } 115 | 116 | open func attributeIsSupported(_ attribute: String) throws -> Bool { 117 | // Ask to copy 0 values, since we are only interested in the return code. 118 | var value: CFArray? 119 | let error = AXUIElementCopyAttributeValues(element, attribute as CFString, 0, 0, &value) 120 | 121 | if error == .attributeUnsupported { 122 | return false 123 | } 124 | 125 | if error == .noValue { 126 | return true 127 | } 128 | 129 | guard error == .success else { 130 | throw error 131 | } 132 | 133 | return true 134 | } 135 | 136 | /// Returns whether `attribute` is writeable. 137 | open func attributeIsSettable(_ attribute: Attribute) throws -> Bool { 138 | return try attributeIsSettable(attribute.rawValue) 139 | } 140 | 141 | open func attributeIsSettable(_ attribute: String) throws -> Bool { 142 | var settable: DarwinBoolean = false 143 | let error = AXUIElementIsAttributeSettable(element, attribute as CFString, &settable) 144 | 145 | if error == .noValue || error == .attributeUnsupported { 146 | return false 147 | } 148 | 149 | guard error == .success else { 150 | throw error 151 | } 152 | 153 | return settable.boolValue 154 | } 155 | 156 | /// Returns the value of `attribute`, if it exists. 157 | /// 158 | /// - parameter attribute: The name of a (non-parameterized) attribute. 159 | /// 160 | /// - returns: An optional containing the value of `attribute` as the desired type, or nil. 161 | /// If `attribute` is an array, all values are returned. 162 | /// 163 | /// - warning: This method force-casts the attribute to the desired type, which will abort if 164 | /// the cast fails. If you want to check the return type, ask for Any. 165 | open func attribute(_ attribute: Attribute) throws -> T? { 166 | return try self.attribute(attribute.rawValue) 167 | } 168 | 169 | open func attribute(_ attribute: String) throws -> T? { 170 | var value: AnyObject? 171 | let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value) 172 | 173 | if error == .noValue || error == .attributeUnsupported { 174 | return nil 175 | } 176 | 177 | guard error == .success else { 178 | throw error 179 | } 180 | 181 | guard let unpackedValue = (unpackAXValue(value!) as? T) else { 182 | throw AXError.illegalArgument 183 | } 184 | 185 | return unpackedValue 186 | } 187 | 188 | /// Sets the value of `attribute` to `value`. 189 | /// 190 | /// - warning: Unlike read-only methods, this method throws if the attribute doesn't exist. 191 | /// 192 | /// - throws: 193 | /// - `Error.AttributeUnsupported`: `attribute` isn't supported. 194 | /// - `Error.IllegalArgument`: `value` is an illegal value. 195 | /// - `Error.Failure`: A temporary failure occurred. 196 | open func setAttribute(_ attribute: Attribute, value: Any) throws { 197 | try setAttribute(attribute.rawValue, value: value) 198 | } 199 | 200 | open func setAttribute(_ attribute: String, value: Any) throws { 201 | let error = AXUIElementSetAttributeValue(element, attribute as CFString, packAXValue(value)) 202 | 203 | guard error == .success else { 204 | throw error 205 | } 206 | } 207 | 208 | /// Gets multiple attributes of the element at once. 209 | /// 210 | /// - parameter attributes: An array of attribute names. Nonexistent attributes are ignored. 211 | /// 212 | /// - returns: A dictionary mapping provided parameter names to their values. Parameters which 213 | /// don't exist or have no value will be absent. 214 | /// 215 | /// - throws: If there are any errors other than .NoValue or .AttributeUnsupported, it will 216 | /// throw the first one it encounters. 217 | /// 218 | /// - note: Presumably you would use this API for performance, though it's not explicitly 219 | /// documented by Apple that there is actually a difference. 220 | open func getMultipleAttributes(_ names: Attribute...) throws -> [Attribute: Any] { 221 | return try getMultipleAttributes(names) 222 | } 223 | 224 | open func getMultipleAttributes(_ attributes: [Attribute]) throws -> [Attribute: Any] { 225 | let values = try fetchMultiAttrValues(attributes.map({ $0.rawValue })) 226 | return try packMultiAttrValues(attributes, values: values) 227 | } 228 | 229 | open func getMultipleAttributes(_ attributes: [String]) throws -> [String: Any] { 230 | let values = try fetchMultiAttrValues(attributes) 231 | return try packMultiAttrValues(attributes, values: values) 232 | } 233 | 234 | // Helper: Gets list of values 235 | fileprivate func fetchMultiAttrValues(_ attributes: [String]) throws -> [AnyObject] { 236 | var valuesCF: CFArray? 237 | let error = AXUIElementCopyMultipleAttributeValues( 238 | element, 239 | attributes as CFArray, 240 | // keep going on errors (particularly NoValue) 241 | AXCopyMultipleAttributeOptions(rawValue: 0), 242 | &valuesCF) 243 | 244 | guard error == .success else { 245 | throw error 246 | } 247 | 248 | return valuesCF! as [AnyObject] 249 | } 250 | 251 | // Helper: Packs names, values into dictionary 252 | fileprivate func packMultiAttrValues(_ attributes: [Attr], 253 | values: [AnyObject]) throws -> [Attr: Any] { 254 | var result = [Attr: Any]() 255 | for (index, attribute) in attributes.enumerated() { 256 | if try checkMultiAttrValue(values[index]) { 257 | result[attribute] = unpackAXValue(values[index]) 258 | } 259 | } 260 | return result 261 | } 262 | 263 | // Helper: Checks if value is present and not an error (throws on nontrivial errors). 264 | fileprivate func checkMultiAttrValue(_ value: AnyObject) throws -> Bool { 265 | // Check for null 266 | if value is NSNull { 267 | return false 268 | } 269 | 270 | // Check for error 271 | if CFGetTypeID(value) == AXValueGetTypeID() && 272 | AXValueGetType(value as! AXValue).rawValue == kAXValueAXErrorType { 273 | var error: AXError = AXError.success 274 | AXValueGetValue(value as! AXValue, AXValueType(rawValue: kAXValueAXErrorType)!, &error) 275 | 276 | assert(error != .success) 277 | if error == .noValue || error == .attributeUnsupported { 278 | return false 279 | } else { 280 | throw error 281 | } 282 | } 283 | 284 | return true 285 | } 286 | 287 | // MARK: Array attributes 288 | 289 | /// Returns all the values of the attribute as an array of the given type. 290 | /// 291 | /// - parameter attribute: The name of the array attribute. 292 | /// 293 | /// - throws: `Error.IllegalArgument` if the attribute isn't an array. 294 | open func arrayAttribute(_ attribute: Attribute) throws -> [T]? { 295 | return try arrayAttribute(attribute.rawValue) 296 | } 297 | 298 | open func arrayAttribute(_ attribute: String) throws -> [T]? { 299 | guard let value: Any = try self.attribute(attribute) else { 300 | return nil 301 | } 302 | guard let array = value as? [AnyObject] else { 303 | // For consistency with the other array attribute APIs, throw if it's not an array. 304 | throw AXError.illegalArgument 305 | } 306 | return array.map({ unpackAXValue($0) as! T }) 307 | } 308 | 309 | /// Returns a subset of values from an array attribute. 310 | /// 311 | /// - parameter attribute: The name of the array attribute. 312 | /// - parameter startAtIndex: The index of the array to start taking values from. 313 | /// - parameter maxValues: The maximum number of values you want. 314 | /// 315 | /// - returns: An array of up to `maxValues` values starting at `startAtIndex`. 316 | /// - The array is empty if `startAtIndex` is out of range. 317 | /// - `nil` if the attribute doesn't exist or has no value. 318 | /// 319 | /// - throws: `Error.IllegalArgument` if the attribute isn't an array. 320 | open func valuesForAttribute 321 | (_ attribute: Attribute, startAtIndex index: Int, maxValues: Int) throws -> [T]? { 322 | return try valuesForAttribute(attribute.rawValue, startAtIndex: index, maxValues: maxValues) 323 | } 324 | 325 | open func valuesForAttribute 326 | (_ attribute: String, startAtIndex index: Int, maxValues: Int) throws -> [T]? { 327 | var values: CFArray? 328 | let error = AXUIElementCopyAttributeValues( 329 | element, attribute as CFString, index, maxValues, &values 330 | ) 331 | 332 | if error == .noValue || error == .attributeUnsupported { 333 | return nil 334 | } 335 | 336 | guard error == .success else { 337 | throw error 338 | } 339 | 340 | let array = values! as [AnyObject] 341 | return array.map({ unpackAXValue($0) as! T }) 342 | } 343 | 344 | /// Returns the number of values an array attribute has. 345 | /// - returns: The number of values, or `nil` if `attribute` isn't an array (or doesn't exist). 346 | open func valueCountForAttribute(_ attribute: Attribute) throws -> Int? { 347 | return try valueCountForAttribute(attribute.rawValue) 348 | } 349 | 350 | open func valueCountForAttribute(_ attribute: String) throws -> Int? { 351 | var count: Int = 0 352 | let error = AXUIElementGetAttributeValueCount(element, attribute as CFString, &count) 353 | 354 | if error == .attributeUnsupported || error == .illegalArgument { 355 | return nil 356 | } 357 | 358 | guard error == .success else { 359 | throw error 360 | } 361 | 362 | return count 363 | } 364 | 365 | // MARK: Parameterized attributes 366 | 367 | /// Returns a list of all parameterized attributes of the element. 368 | /// 369 | /// Parameterized attributes are attributes that require parameters to retrieve. For example, 370 | /// the cell contents of a spreadsheet might require the row and column of the cell you want. 371 | open func parameterizedAttributes() throws -> [Attribute] { 372 | return try parameterizedAttributesAsStrings().compactMap({ Attribute(rawValue: $0) }) 373 | } 374 | 375 | open func parameterizedAttributesAsStrings() throws -> [String] { 376 | var names: CFArray? 377 | let error = AXUIElementCopyParameterizedAttributeNames(element, &names) 378 | 379 | if error == .noValue || error == .attributeUnsupported { 380 | return [] 381 | } 382 | 383 | guard error == .success else { 384 | throw error 385 | } 386 | 387 | // We must first convert the CFArray to a native array, then downcast to an array of 388 | // strings. 389 | return names! as [AnyObject] as! [String] 390 | } 391 | 392 | /// Returns the value of the parameterized attribute `attribute` with parameter `param`. 393 | /// 394 | /// The expected type of `param` depends on the attribute. See the 395 | /// [NSAccessibility Informal Protocol Reference](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSAccessibility_Protocol/) 396 | /// for more info. 397 | open func parameterizedAttribute(_ attribute: Attribute, param: U) throws -> T? { 398 | return try parameterizedAttribute(attribute.rawValue, param: param) 399 | } 400 | 401 | open func parameterizedAttribute(_ attribute: String, param: U) throws -> T? { 402 | var value: AnyObject? 403 | let error = AXUIElementCopyParameterizedAttributeValue( 404 | element, attribute as CFString, param as AnyObject, &value 405 | ) 406 | 407 | if error == .noValue || error == .attributeUnsupported { 408 | return nil 409 | } 410 | 411 | guard error == .success else { 412 | throw error 413 | } 414 | 415 | return (unpackAXValue(value!) as! T) 416 | } 417 | 418 | // MARK: Attribute helpers 419 | 420 | // Checks if the value is an AXValue and if so, unwraps it. 421 | // If the value is an AXUIElement, wraps it in UIElement. 422 | fileprivate func unpackAXValue(_ value: AnyObject) -> Any { 423 | switch CFGetTypeID(value) { 424 | case AXUIElementGetTypeID(): 425 | return UIElement(value as! AXUIElement) 426 | case AXValueGetTypeID(): 427 | let type = AXValueGetType(value as! AXValue) 428 | switch type { 429 | case .axError: 430 | var result: AXError = .success 431 | let success = AXValueGetValue(value as! AXValue, type, &result) 432 | assert(success) 433 | return result 434 | case .cfRange: 435 | var result: CFRange = CFRange() 436 | let success = AXValueGetValue(value as! AXValue, type, &result) 437 | assert(success) 438 | return result 439 | case .cgPoint: 440 | var result: CGPoint = CGPoint.zero 441 | let success = AXValueGetValue(value as! AXValue, type, &result) 442 | assert(success) 443 | return result 444 | case .cgRect: 445 | var result: CGRect = CGRect.zero 446 | let success = AXValueGetValue(value as! AXValue, type, &result) 447 | assert(success) 448 | return result 449 | case .cgSize: 450 | var result: CGSize = CGSize.zero 451 | let success = AXValueGetValue(value as! AXValue, type, &result) 452 | assert(success) 453 | return result 454 | case .illegal: 455 | return value 456 | @unknown default: 457 | return value 458 | } 459 | default: 460 | return value 461 | } 462 | } 463 | 464 | // Checks if the value is one supported by AXValue and if so, wraps it. 465 | // If the value is a UIElement, unwraps it to an AXUIElement. 466 | fileprivate func packAXValue(_ value: Any) -> AnyObject { 467 | switch value { 468 | case let val as UIElement: 469 | return val.element 470 | case var val as CFRange: 471 | return AXValueCreate(AXValueType(rawValue: kAXValueCFRangeType)!, &val)! 472 | case var val as CGPoint: 473 | return AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &val)! 474 | case var val as CGRect: 475 | return AXValueCreate(AXValueType(rawValue: kAXValueCGRectType)!, &val)! 476 | case var val as CGSize: 477 | return AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &val)! 478 | default: 479 | return value as AnyObject // must be an object to pass to AX 480 | } 481 | } 482 | 483 | // MARK: - Actions 484 | 485 | /// Returns a list of actions that can be performed on the element. 486 | open func actions() throws -> [Action] { 487 | return try actionsAsStrings().compactMap({ Action(rawValue: $0) }) 488 | } 489 | 490 | open func actionsAsStrings() throws -> [String] { 491 | var names: CFArray? 492 | let error = AXUIElementCopyActionNames(element, &names) 493 | 494 | if error == .noValue || error == .attributeUnsupported { 495 | return [] 496 | } 497 | 498 | guard error == .success else { 499 | throw error 500 | } 501 | 502 | // We must first convert the CFArray to a native array, then downcast to an array of strings. 503 | return names! as [AnyObject] as! [String] 504 | } 505 | 506 | /// Returns the human-readable description of `action`. 507 | open func actionDescription(_ action: Action) throws -> String? { 508 | return try actionDescription(action.rawValue) 509 | } 510 | 511 | open func actionDescription(_ action: String) throws -> String? { 512 | var description: CFString? 513 | let error = AXUIElementCopyActionDescription(element, action as CFString, &description) 514 | 515 | if error == .noValue || error == .actionUnsupported { 516 | return nil 517 | } 518 | 519 | guard error == .success else { 520 | throw error 521 | } 522 | 523 | return description! as String 524 | } 525 | 526 | /// Performs the action `action` on the element, returning on success. 527 | /// 528 | /// - note: If the action times out, it might mean that the application is taking a long time to 529 | /// actually perform the action. It doesn't necessarily mean that the action wasn't 530 | /// performed. 531 | /// - throws: `Error.ActionUnsupported` if the action is not supported. 532 | open func performAction(_ action: Action) throws { 533 | try performAction(action.rawValue) 534 | } 535 | 536 | open func performAction(_ action: String) throws { 537 | let error = AXUIElementPerformAction(element, action as CFString) 538 | 539 | guard error == .success else { 540 | throw error 541 | } 542 | } 543 | 544 | // MARK: - 545 | 546 | /// Returns the process ID of the application that the element is a part of. 547 | /// 548 | /// Throws only if the element is invalid (`Errors.InvalidUIElement`). 549 | open func pid() throws -> pid_t { 550 | var pid: pid_t = -1 551 | let error = AXUIElementGetPid(element, &pid) 552 | 553 | guard error == .success else { 554 | throw error 555 | } 556 | 557 | return pid 558 | } 559 | 560 | /// The timeout in seconds for all messages sent to this element. Use this to control how long a 561 | /// method call can delay execution. The default is `0`, which means to use the global timeout. 562 | /// 563 | /// - note: Only applies to this instance of UIElement, not other instances that happen to equal 564 | /// it. 565 | /// - seeAlso: `UIElement.globalMessagingTimeout(_:)` 566 | open var messagingTimeout: Float = 0 { 567 | didSet { 568 | messagingTimeout = max(messagingTimeout, 0) 569 | let error = AXUIElementSetMessagingTimeout(element, messagingTimeout) 570 | 571 | // InvalidUIElement errors are only relevant when actually passing messages, so we can 572 | // ignore them here. 573 | guard error == .success || error == .invalidUIElement else { 574 | fatalError("Unexpected error setting messaging timeout: \(error)") 575 | } 576 | } 577 | } 578 | 579 | // Gets the element at the specified coordinates. 580 | // This can only be called on applications and the system-wide element, so it is internal here. 581 | func elementAtPosition(_ x: Float, _ y: Float) throws -> UIElement? { 582 | var result: AXUIElement? 583 | let error = AXUIElementCopyElementAtPosition(element, x, y, &result) 584 | 585 | if error == .noValue { 586 | return nil 587 | } 588 | 589 | guard error == .success else { 590 | throw error 591 | } 592 | 593 | return UIElement(result!) 594 | } 595 | 596 | // TODO: convenience functions for attributes 597 | // TODO: get any attribute as a UIElement or [UIElement] (or a subclass) 598 | // TODO: promoters 599 | } 600 | 601 | // MARK: - CustomStringConvertible 602 | 603 | extension UIElement: CustomStringConvertible { 604 | public var description: String { 605 | var roleString: String 606 | var description: String? 607 | let pid = try? self.pid() 608 | do { 609 | let role = try self.role() 610 | roleString = role?.rawValue ?? "UIElementNoRole" 611 | 612 | switch role { 613 | case .some(.application): 614 | description = pid 615 | .flatMap { NSRunningApplication(processIdentifier: $0) } 616 | .flatMap { $0.bundleIdentifier } ?? "" 617 | case .some(.window): 618 | description = (try? attribute(.title) ?? "") ?? "" 619 | default: 620 | break 621 | } 622 | } catch AXError.invalidUIElement { 623 | roleString = "InvalidUIElement" 624 | } catch { 625 | roleString = "UnknownUIElement" 626 | } 627 | 628 | let pidString = (pid == nil) ? "??" : String(pid!) 629 | return "<\(roleString) \"" 630 | + "\(description ?? String(describing: element))" 631 | + "\" (pid=\(pidString))>" 632 | } 633 | 634 | public var inspect: String { 635 | guard let attributeNames = try? attributes() else { 636 | return "InvalidUIElement" 637 | } 638 | guard let attributes = try? getMultipleAttributes(attributeNames) else { 639 | return "InvalidUIElement" 640 | } 641 | return "\(attributes)" 642 | } 643 | } 644 | 645 | // MARK: - Equatable 646 | 647 | extension UIElement: Equatable {} 648 | public func ==(lhs: UIElement, rhs: UIElement) -> Bool { 649 | return CFEqual(lhs.element, rhs.element) 650 | } 651 | 652 | // MARK: - Convenience getters 653 | 654 | extension UIElement { 655 | /// Returns the role (type) of the element, if it reports one. 656 | /// 657 | /// Almost all elements report a role, but this could return nil for elements that aren't 658 | /// finished initializing. 659 | /// 660 | /// - seeAlso: [Roles](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/doc/constant_group/Roles) 661 | public func role() throws -> Role? { 662 | // should this be non-optional? 663 | if let str: String = try self.attribute(.role) { 664 | return Role(rawValue: str) 665 | } else { 666 | return nil 667 | } 668 | } 669 | 670 | /// - seeAlso: [Subroles](https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSAccessibility_Protocol_Reference/index.html#//apple_ref/doc/constant_group/Subroles) 671 | public func subrole() throws -> Subrole? { 672 | if let str: String = try self.attribute(.subrole) { 673 | return Subrole(rawValue: str) 674 | } else { 675 | return nil 676 | } 677 | } 678 | } 679 | --------------------------------------------------------------------------------