diff --git a/.gitignore b/.gitignore index a7ac797..67a9e32 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ DerivedData/ # SwiftLint Remote Config Cache .swiftlint/RemoteConfigCache +.claude +MEGAREADME.md diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..7e252d4 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "532ea61d85af588cbc821303565bb3c649877a17d13a86df1b0238f71cd1480d", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version" : "1.7.0" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 23eada2..1f5f7c0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.11 import PackageDescription @@ -9,7 +9,10 @@ let package = Package( .library( name: "BetterSwiftAX", targets: ["AccessibilityControl"] - ), + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), ], targets: [ .target( @@ -26,5 +29,24 @@ let package = Package( name: "AccessibilityControl", dependencies: ["CAccessibilityControl", "WindowControl"] ), + .executableTarget( + name: "AXConstantsGenerator", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), + .plugin( + name: "GenerateAXConstants", + capability: .command( + intent: .custom( + verb: "generate-ax-constants", + description: "Regenerate AX constant Swift files from HIServices headers" + ), + permissions: [ + .writeToPackageDirectory(reason: "Writes generated Swift source files into Sources/AccessibilityControl") + ] + ), + dependencies: ["AXConstantsGenerator"] + ) ] ) diff --git a/Plugins/GenerateAXConstants/plugin.swift b/Plugins/GenerateAXConstants/plugin.swift new file mode 100644 index 0000000..76bb7ae --- /dev/null +++ b/Plugins/GenerateAXConstants/plugin.swift @@ -0,0 +1,25 @@ +import Foundation +import PackagePlugin + +@main +struct GenerateAXConstantsPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + let generator = try context.tool(named: "AXConstantsGenerator") + + let outputDir = context.package.directory + .appending(subpath: "Sources") + .appending(subpath: "AccessibilityControl") + + let process = Process() + process.executableURL = URL(fileURLWithPath: generator.path.string) + process.arguments = [outputDir.string] + + try process.run() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + Diagnostics.error("AXConstantsGenerator exited with code \(process.terminationStatus)") + return + } + } +} diff --git a/Sources/AXConstantsGenerator/main.swift b/Sources/AXConstantsGenerator/main.swift new file mode 100644 index 0000000..f8e8ed6 --- /dev/null +++ b/Sources/AXConstantsGenerator/main.swift @@ -0,0 +1,787 @@ +import ArgumentParser +import Foundation + +// MARK: - Entry Point + +@main +struct AXConstantsGenerator: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Generate Swift constant definitions from HIServices AX header files." + ) + + @Argument(help: "Output directory for generated Swift files") + var outputDirectory: String + + func run() throws { + let sdkPath = try findSDKPath() + let headersDir = sdkPath + + "/System/Library/Frameworks/ApplicationServices.framework" + + "/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers" + + var allConstants: [Constant] = [] + var commentMap: [String: String] = [:] + var groupMap: [String: String] = [:] + var seen: Set = [] + + for file in headerFiles { + let path = headersDir + "/" + file + let relativeHeaderPath = "System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/\(file)" + guard let content = try? String(contentsOfFile: path, encoding: .utf8) else { + fputs("warning: could not read \(path)\n", stderr) + continue + } + let metadata = parseHeaderMetadata(from: content) + commentMap.merge(metadata.comments) { old, _ in old } + groupMap.merge(metadata.groups) { old, _ in old } + for c in parseConstants(from: content, sourceHeaderPath: relativeHeaderPath) { + if seen.insert(c.define).inserted { + allConstants.append(c) + } + } + } + + var groups: [String: [Constant]] = [:] + for c in allConstants { + groups[c.suffix, default: []].append(c) + } + + let fm = FileManager.default + try fm.createDirectory(atPath: outputDirectory, withIntermediateDirectories: true) + + var totalCount = 0 + for config in outputConfigs { + guard let constants = groups[config.suffix], !constants.isEmpty else { continue } + let content = generateFile(config: config, constants: constants, comments: commentMap, groups: groupMap) + let path = outputDirectory + "/" + config.fileName + try content.write(toFile: path, atomically: true, encoding: .utf8) + totalCount += constants.count + print(" \(config.fileName) — \(constants.count) constants") + } + print("\nGenerated \(totalCount) constants across \(groups.count) files in \(outputDirectory)/") + } +} + +// MARK: - SDK Discovery + +private func findSDKPath() throws -> String { + let process = Process() + let pipe = Pipe() + process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + process.arguments = ["--show-sdk-path"] + process.standardOutput = pipe + process.standardError = FileHandle.nullDevice + try process.run() + process.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let path = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !path.isEmpty else { + throw ValidationError("Could not determine SDK path. Ensure Xcode or Command Line Tools are installed.") + } + return path +} + +// MARK: - Configuration + +private let headerFiles = [ + "AXRoleConstants.h", + "AXAttributeConstants.h", + "AXActionConstants.h", + "AXNotificationConstants.h", + "AXValueConstants.h", + "AXWebConstants.h", +] + +private let swiftKeywords: Set = [ + "associatedtype", "class", "deinit", "enum", "extension", "fileprivate", + "func", "import", "init", "inout", "internal", "let", "open", "operator", + "private", "precedencegroup", "protocol", "public", "rethrows", "static", + "struct", "subscript", "typealias", "var", + "break", "case", "catch", "continue", "default", "defer", "do", "else", + "fallthrough", "for", "guard", "if", "in", "repeat", "return", "switch", + "throw", "where", "while", + "Any", "as", "false", "is", "nil", "self", "Self", "super", + "throws", "true", "try", +] + +private let skipDefines: Set = ["kAXDescription"] + +// MARK: - Output Configuration + +private enum OutputMode { + case caselessEnum(name: String) + case extensionOnType(typeName: String) +} + +private struct OutputConfig { + let suffix: String + let mode: OutputMode + let fileName: String +} + +/// Longest suffix first so "ParameterizedAttribute" matches before "Attribute". +private let outputConfigs: [OutputConfig] = [ + .init(suffix: "ParameterizedAttribute", mode: .caselessEnum(name: "ParameterizedAttributeKey"), fileName: "Accessibility+ParameterizedAttributeKey.swift"), + .init(suffix: "Attribute", mode: .caselessEnum(name: "AttributeKey"), fileName: "Accessibility+AttributeKey.swift"), + .init(suffix: "Subrole", mode: .caselessEnum(name: "Subrole"), fileName: "Accessibility+Subrole.swift"), + .init(suffix: "Role", mode: .caselessEnum(name: "Role"), fileName: "Accessibility+Role.swift"), + .init(suffix: "Notification", mode: .extensionOnType(typeName: "Notification"), fileName: "Accessibility+Notification.swift"), + .init(suffix: "Action", mode: .extensionOnType(typeName: "Action.Name"), fileName: "Accessibility+Action.swift"), + .init(suffix: "Value", mode: .caselessEnum(name: "Value"), fileName: "Accessibility+Value.swift"), +] + +// MARK: - Parsed Constant + +private struct Constant { + let define: String + let suffix: String + let sourceHeaderPath: String + + var baseName: String { + var s = String(define.dropFirst(3)) // strip "kAX" + if s.hasSuffix(suffix) { s = String(s.dropLast(suffix.count)) } + return s + } + + var propertyName: String { + escaped(lowerCamelCase(baseName)) + } +} + +// MARK: - Name Transformation + +/// Converts PascalCase to lowerCamelCase, handling leading acronyms. +/// +/// "Application" → "application" +/// "URLDockItem" → "urlDockItem" +/// "AMPMField" → "ampmField" +/// "UIElementDestroyed" → "uiElementDestroyed" +private func lowerCamelCase(_ name: String) -> String { + guard !name.isEmpty else { return name } + let chars = Array(name) + var upperCount = 0 + for c in chars { + guard c.isUppercase else { break } + upperCount += 1 + } + if upperCount == 0 { return name } + if upperCount >= chars.count { return name.lowercased() } + if upperCount == 1 { + return String(chars[0]).lowercased() + String(chars[1...]) + } + return String(chars[0..<(upperCount - 1)]).lowercased() + + String(chars[(upperCount - 1)...]) +} + +private func escaped(_ name: String) -> String { + swiftKeywords.contains(name) ? "`\(name)`" : name +} + +// MARK: - Constant Parsing + +private let definePattern = try! NSRegularExpression( + pattern: #"^\s*#define\s+(kAX\w+)\s+CFSTR\("[^"]*"\)"#, + options: .anchorsMatchLines +) + +private func parseConstants(from content: String, sourceHeaderPath: String) -> [Constant] { + let ns = content as NSString + let matches = definePattern.matches(in: content, range: NSRange(location: 0, length: ns.length)) + var result: [Constant] = [] + for match in matches { + let define = ns.substring(with: match.range(at: 1)) + guard !skipDefines.contains(define) else { continue } + let stripped = String(define.dropFirst(3)) + for config in outputConfigs { + if stripped.hasSuffix(config.suffix) { + result.append(Constant(define: define, suffix: config.suffix, sourceHeaderPath: sourceHeaderPath)) + break + } + } + } + return result +} + +// MARK: - Comment & Group Extraction + +private struct HeaderMetadata { + var comments: [String: String] + var groups: [String: String] +} + +private struct BlockCommentCandidate { + let lines: [String] + let endLine: Int +} + +private struct DocSection { + let title: String? + let lines: [String] +} + +private struct DefineMatch { + let name: String + let trailingComment: String? +} + +private struct DoxygenTag { + let name: String + let value: String? +} + +private let defineLinePattern = try! NSRegularExpression( + pattern: #"^\s*#define\s+(kAX\w+)\s+CFSTR\("[^"]*"\)(?:\s*//\s*(.*))?\s*$"# +) + +private let doxygenTagPattern = try! NSRegularExpression( + pattern: #"^@([A-Za-z/]+)\b(?:\s+(.*))?$"# +) + +private func parseHeaderMetadata(from content: String) -> HeaderMetadata { + var comments: [String: String] = [:] + var groups: [String: String] = [:] + var quickReferenceGroups: [String: String] = [:] + + let lines = content.components(separatedBy: "\n") + var currentGroup: String? + var inBlock = false + var blockLines: [String] = [] + var lastBlockComment: BlockCommentCandidate? + var lineCommentBuffer: [String] = [] + var lastLineCommentLine = -10_000 + + var inQuickReference = false + var quickReferenceCategory: String? + + for (index, line) in lines.enumerated() { + let trimmed = line.trimmingCharacters(in: .whitespaces) + + if let lastBlock = lastBlockComment, (index - lastBlock.endLine) > 3 { + lastBlockComment = nil + } + + if inBlock { + blockLines.append(line) + updateQuickReferenceState( + trimmedLine: trimmed, + inQuickReference: &inQuickReference, + quickReferenceCategory: &quickReferenceCategory, + quickReferenceGroups: &quickReferenceGroups + ) + if trimmed.contains("*/") { + inBlock = false + let cleaned = cleanBlockComment(lines: blockLines) + if let groupName = extractGroupName(from: cleaned) { + currentGroup = groupName + } + lastBlockComment = BlockCommentCandidate(lines: blockLines, endLine: index) + blockLines = [] + } + continue + } + + if trimmed.hasPrefix("/*") { + inBlock = true + blockLines = [line] + updateQuickReferenceState( + trimmedLine: trimmed, + inQuickReference: &inQuickReference, + quickReferenceCategory: &quickReferenceCategory, + quickReferenceGroups: &quickReferenceGroups + ) + if trimmed.contains("*/") { + inBlock = false + let cleaned = cleanBlockComment(lines: blockLines) + if let groupName = extractGroupName(from: cleaned) { + currentGroup = groupName + } + lastBlockComment = BlockCommentCandidate(lines: blockLines, endLine: index) + blockLines = [] + } + lineCommentBuffer.removeAll(keepingCapacity: true) + continue + } + + if trimmed.hasPrefix("//") { + let text = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces) + if let sectionName = extractSectionHeadingFromLineComment(text) { + currentGroup = sectionName + lineCommentBuffer.removeAll(keepingCapacity: true) + } else if !text.isEmpty { + lineCommentBuffer.append(text) + lastLineCommentLine = index + } + continue + } + + if let define = parseDefine(from: line) { + if let quickReferenceGroup = quickReferenceGroups[define.name] { + if let currentGroup, isEquivalentGroup(currentGroup, quickReferenceGroup) { + groups[define.name] = groups[define.name] ?? currentGroup + } else { + groups[define.name] = groups[define.name] ?? quickReferenceGroup + } + } else if let currentGroup { + groups[define.name] = groups[define.name] ?? currentGroup + } + + var docParts: [String] = [] + if let lastBlockComment, (index - lastBlockComment.endLine) <= 3 { + if let documentation = extractDocumentation(from: lastBlockComment.lines) { + docParts.append(documentation) + } + } else if !lineCommentBuffer.isEmpty, (index - lastLineCommentLine) <= 2 { + let text = lineCommentBuffer.joined(separator: "\n") + if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + docParts.append(text) + } + } + + if let trailing = define.trailingComment?.trimmingCharacters(in: .whitespaces), + !trailing.isEmpty { + if docParts.isEmpty { + docParts.append(trailing) + } else { + docParts.append("Inline Note:\n\(trailing)") + } + } + + if !docParts.isEmpty { + comments[define.name] = comments[define.name] ?? docParts.joined(separator: "\n\n") + } + + lineCommentBuffer.removeAll(keepingCapacity: true) + lastBlockComment = nil + continue + } + + if !trimmed.isEmpty { + lineCommentBuffer.removeAll(keepingCapacity: true) + } + } + + return HeaderMetadata(comments: comments, groups: groups) +} + +private func updateQuickReferenceState( + trimmedLine: String, + inQuickReference: inout Bool, + quickReferenceCategory: inout String?, + quickReferenceGroups: inout [String: String] +) { + if trimmedLine.contains("Quick reference:") { + inQuickReference = true + quickReferenceCategory = nil + return + } + + guard inQuickReference else { return } + + if let heading = parseQuickReferenceHeading(from: trimmedLine) { + quickReferenceCategory = heading + } else if let defineName = extractAnyAXDefineName(from: trimmedLine), + let quickReferenceCategory { + quickReferenceGroups[defineName] = quickReferenceCategory + } + + if trimmedLine.contains("*/") { + inQuickReference = false + quickReferenceCategory = nil + } +} + +private func parseQuickReferenceHeading(from line: String) -> String? { + let trimmed = line.trimmingCharacters(in: .whitespaces) + guard trimmed.hasPrefix("//") else { return nil } + let text = String(trimmed.dropFirst(2)).trimmingCharacters(in: .whitespaces) + guard !text.isEmpty else { return nil } + guard !text.lowercased().contains("quick reference") else { return nil } + return normalizedGroupName(text) +} + +private func parseDefine(from line: String) -> DefineMatch? { + let ns = line as NSString + let range = NSRange(location: 0, length: ns.length) + guard let match = defineLinePattern.firstMatch(in: line, range: range) else { return nil } + let name = ns.substring(with: match.range(at: 1)) + let trailingComment: String? + if match.range(at: 2).location != NSNotFound { + trailingComment = ns.substring(with: match.range(at: 2)) + } else { + trailingComment = nil + } + return DefineMatch(name: name, trailingComment: trailingComment) +} + +private func extractAnyAXDefineName(from line: String) -> String? { + guard let range = line.range(of: #"\bkAX\w+\b"#, options: .regularExpression) else { return nil } + return String(line[range]) +} + +private func cleanBlockComment(lines: [String]) -> [String] { + let cleaned = lines.map { raw -> String in + var line = raw + line = line.replacingOccurrences( + of: #"^\s*/\*+!?"#, + with: "", + options: .regularExpression + ) + line = line.replacingOccurrences( + of: #"\*/\s*$"#, + with: "", + options: .regularExpression + ) + line = line.replacingOccurrences( + of: #"^\s*\* ?"#, + with: "", + options: .regularExpression + ) + return line.trimmingCharacters(in: .whitespaces) + } + return trimmingEmptyEdgeLines(cleaned) +} + +private func extractGroupName(from cleanedBlock: [String]) -> String? { + guard !cleanedBlock.isEmpty else { return nil } + + for line in cleanedBlock { + if let tag = parseDoxygenTag(from: line), tag.name.lowercased() == "group" { + if let value = tag.value?.trimmingCharacters(in: .whitespaces), !value.isEmpty { + return normalizedGroupName(value) + } + } + } + + guard cleanedBlock.count == 1 else { return nil } + let candidate = cleanedBlock[0] + guard looksLikeSectionHeading(candidate) else { return nil } + return normalizedGroupName(candidate) +} + +private func extractSectionHeadingFromLineComment(_ text: String) -> String? { + let trimmed = text.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { return nil } + + let lower = trimmed.lowercased() + if lower.contains("cfstringref") || lower.contains("cfbooleanref") || lower.hasSuffix("ref") { + return nil + } + + let headingKeywords = [ + "attribute", "attributes", + "subrole", "subroles", + "role", "roles", + "action", "actions", + "notification", "notifications", + "value", "values", + ] + guard headingKeywords.contains(where: { lower.contains($0) }) else { return nil } + guard looksLikeSectionHeading(trimmed) else { return nil } + return normalizedGroupName(trimmed) +} + +private func extractDocumentation(from blockLines: [String]) -> String? { + let cleaned = cleanBlockComment(lines: blockLines) + guard !cleaned.isEmpty else { return nil } + if cleaned.allSatisfy({ line in + line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + || line.range(of: #"[A-Za-z0-9]"#, options: .regularExpression) == nil + }) { + return nil + } + + let text = cleaned.joined(separator: "\n") + if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return nil } + if text.contains("Need discussion for following") { return nil } + if text.contains("@group") { return nil } + if text.contains("@header") && !text.contains("@defined") && !text.contains("@define") { return nil } + if cleaned.count == 1 && looksLikeSectionHeading(cleaned[0]) { return nil } + + var sections: [DocSection] = [] + var currentTitle: String? + var currentLines: [String] = [] + var sawDoxygenTag = false + + func flushSection() { + let trimmedLines = trimmingEmptyEdgeLines(currentLines) + guard !trimmedLines.isEmpty else { + currentLines.removeAll(keepingCapacity: true) + currentTitle = nil + return + } + sections.append(DocSection(title: currentTitle, lines: trimmedLines)) + currentLines.removeAll(keepingCapacity: true) + currentTitle = nil + } + + func startSection(title: String?) { + flushSection() + currentTitle = title + } + + for line in cleaned { + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty { + if !currentLines.isEmpty { + currentLines.append("") + } + continue + } + + if let tag = parseDoxygenTag(from: trimmed) { + sawDoxygenTag = true + let name = tag.name.lowercased() + let value = tag.value?.trimmingCharacters(in: .whitespaces) ?? "" + switch name { + case "define", "defined", "group", "header", "textblock", "/textblock": + continue + case "abstract": + startSection(title: "Abstract") + if !value.isEmpty { currentLines.append(value) } + case "discussion": + startSection(title: "Discussion") + if !value.isEmpty { currentLines.append(value) } + case "attributeblock": + startSection(title: value.isEmpty ? "Attribute" : value) + default: + startSection(title: friendlyTagTitle(name)) + if !value.isEmpty { currentLines.append(value) } + } + continue + } + + currentLines.append(trimmed) + } + flushSection() + + if sections.isEmpty { + if sawDoxygenTag { return nil } + let plain = text.trimmingCharacters(in: .whitespacesAndNewlines) + return plain.isEmpty ? nil : plain + } + + var rendered: [String] = [] + for (idx, section) in sections.enumerated() { + if let title = section.title { + rendered.append("\(title):") + } + rendered.append(contentsOf: section.lines) + if idx < (sections.count - 1) { + rendered.append("") + } + } + + return rendered.joined(separator: "\n") +} + +private func parseDoxygenTag(from line: String) -> DoxygenTag? { + let ns = line as NSString + let range = NSRange(location: 0, length: ns.length) + guard let match = doxygenTagPattern.firstMatch(in: line, range: range) else { return nil } + let name = ns.substring(with: match.range(at: 1)) + let value: String? + if match.range(at: 2).location != NSNotFound { + value = ns.substring(with: match.range(at: 2)) + } else { + value = nil + } + return DoxygenTag(name: name, value: value) +} + +private func looksLikeSectionHeading(_ text: String) -> Bool { + let normalized = text + .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespaces) + guard !normalized.isEmpty else { return false } + guard normalized.range(of: #"[A-Za-z]"#, options: .regularExpression) != nil else { return false } + guard !normalized.hasPrefix("@") else { return false } + guard normalized.count <= 80 else { return false } + + let lower = normalized.lowercased() + for banned in ["need discussion", "copyright", "quick reference", "tbd", "header"] { + if lower.contains(banned) { return false } + } + return true +} + +private func normalizedGroupName(_ text: String) -> String { + let collapsed = text + .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespacesAndNewlines) + + guard !collapsed.isEmpty else { return collapsed } + if collapsed == collapsed.lowercased(), let first = collapsed.first { + return String(first).uppercased() + String(collapsed.dropFirst()) + } + return collapsed +} + +private func isEquivalentGroup(_ lhs: String, _ rhs: String) -> Bool { + normalizeGroupForComparison(lhs) == normalizeGroupForComparison(rhs) +} + +private func normalizeGroupForComparison(_ text: String) -> String { + text + .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression) + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() +} + +private func friendlyTagTitle(_ tag: String) -> String { + switch tag { + case "seealso": + return "See Also" + default: + return String(tag.prefix(1)).uppercased() + String(tag.dropFirst()) + } +} + +private func trimmingEmptyEdgeLines(_ lines: [String]) -> [String] { + var start = 0 + var end = lines.count + + while start < end, lines[start].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + start += 1 + } + while end > start, lines[end - 1].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + end -= 1 + } + return Array(lines[start.. String { + let sourceHeaders = orderedUniqueSourceHeaders(constants) + let sourceComment = sourceHeaders.count == 1 + ? "// Extracted from: \(sourceHeaders[0])" + : "// Extracted from: \(sourceHeaders.joined(separator: ", "))" + + var lines: [String] = [ + "import ApplicationServices", + "", + sourceComment, + "", + ] + + switch config.mode { + case .caselessEnum(let name): + lines.append("extension Accessibility {") + lines.append(" public enum \(name) {") + appendConstants( + constants, + to: &lines, + comments: comments, + groups: groups, + indent: " " + ) { constant in + "public static let \(constant.propertyName) = \(constant.define)" + } + lines.append(" }") + lines.append("}") + + case .extensionOnType(let typeName): + lines.append("public extension Accessibility.\(typeName) {") + appendConstants( + constants, + to: &lines, + comments: comments, + groups: groups, + indent: " " + ) { constant in + "static let \(constant.propertyName) = Self(\(constant.define))" + } + lines.append("}") + } + + lines.append("") + return lines.joined(separator: "\n") +} + +private func appendConstants( + _ constants: [Constant], + to lines: inout [String], + comments: [String: String], + groups: [String: String], + indent: String, + propertyLine: (Constant) -> String +) { + let sections = buildConstantSections(constants: constants, groups: groups) + for (sectionIndex, section) in sections.enumerated() { + if sectionIndex > 0, lines.last?.isEmpty == false { + lines.append("") + } + if let title = section.title { + lines.append("\(indent)// MARK: - \(title)") + lines.append("") + } + + for constant in section.constants { + if let comment = comments[constant.define], + !comment.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + appendDocumentationBlock(comment, indent: indent, to: &lines) + } + lines.append("\(indent)\(propertyLine(constant))") + } + } +} + +private func appendDocumentationBlock(_ comment: String, indent: String, to lines: inout [String]) { + lines.append("\(indent)/**") + for line in comment.components(separatedBy: "\n") { + if line.isEmpty { + lines.append("\(indent) *") + } else { + lines.append("\(indent) * \(line)") + } + } + lines.append("\(indent) */") +} + +private struct ConstantSection { + let title: String? + var constants: [Constant] +} + +private func buildConstantSections(constants: [Constant], groups: [String: String]) -> [ConstantSection] { + var sections: [ConstantSection] = [] + var sectionIndexByGroupKey: [String: Int] = [:] + var ungroupedSectionIndex: Int? + + for constant in constants { + let group = groups[constant.define]?.trimmingCharacters(in: .whitespacesAndNewlines) + if let group, !group.isEmpty { + let groupKey = normalizeGroupForComparison(group) + if let sectionIndex = sectionIndexByGroupKey[groupKey] { + sections[sectionIndex].constants.append(constant) + } else { + sections.append(ConstantSection(title: group, constants: [constant])) + sectionIndexByGroupKey[groupKey] = sections.count - 1 + } + } else if let ungroupedSectionIndex { + sections[ungroupedSectionIndex].constants.append(constant) + } else { + sections.append(ConstantSection(title: nil, constants: [constant])) + ungroupedSectionIndex = sections.count - 1 + } + } + + return sections +} + +private func orderedUniqueSourceHeaders(_ constants: [Constant]) -> [String] { + var ordered: [String] = [] + var seen: Set = [] + + for constant in constants { + if seen.insert(constant.sourceHeaderPath).inserted { + ordered.append(constant.sourceHeaderPath) + } + } + return ordered +} diff --git a/Sources/AccessibilityControl/Accessibility+Action.swift b/Sources/AccessibilityControl/Accessibility+Action.swift new file mode 100644 index 0000000..5d80df3 --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+Action.swift @@ -0,0 +1,58 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXActionConstants.h + +public extension Accessibility.Action.Name { + // MARK: - Standard Actions + + /** + * Discussion: + * Simulate clicking the UIElement, such as a button. + */ + static let press = Self(kAXPressAction) + /** + * Discussion: + * Increment the value of the UIElement. + */ + static let increment = Self(kAXIncrementAction) + /** + * Discussion: + * Decrement the value of the UIElement. + */ + static let decrement = Self(kAXDecrementAction) + /** + * Discussion: + * Simulate pressing Return in the UIElement, such as a text field. + */ + static let confirm = Self(kAXConfirmAction) + /** + * Discussion: + * Simulate a Cancel action, such as hitting the Cancel button. + */ + static let cancel = Self(kAXCancelAction) + /** + * Discussion: + * Show alternate or hidden UI. + * This is often used to trigger the same change that would occur on a mouse hover. + */ + static let showAlternateUI = Self(kAXShowAlternateUIAction) + /** + * Discussion: + * Show default UI. + * This is often used to trigger the same change that would occur when a mouse hover ends. + */ + static let showDefaultUI = Self(kAXShowDefaultUIAction) + + // MARK: - New Actions + + static let raise = Self(kAXRaiseAction) + static let showMenu = Self(kAXShowMenuAction) + + // MARK: - Obsolete Actions + + /** + * Discussion: + * Select the UIElement, such as a menu item. + */ + static let pick = Self(kAXPickAction) +} diff --git a/Sources/AccessibilityControl/Accessibility+AttributeKey.swift b/Sources/AccessibilityControl/Accessibility+AttributeKey.swift new file mode 100644 index 0000000..274ded5 --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+AttributeKey.swift @@ -0,0 +1,1165 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXAttributeConstants.h, System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXWebConstants.h + +extension Accessibility { + public enum AttributeKey { + // MARK: - Informational Attributes + + /** + * Abstract: + * Identifies the basic type of an element. + * + * Value: + * A CFStringRef of one of the role strings defined in this header, or a new + * role string of your own invention. The string should not be localized, and it does + * not need to be human-readable. Instead of inventing new role strings, see if a + * custom element can be identified by an existing role string and a new subrole. See + * kAXSubroleAttribute. + * + * Writable: + * No + * + * Discussion: + * Required for all elements. Even in the worst case scenario where an element cannot + * figure out what its basic type is, it can still supply the value kAXUnknownRole. + * + * Carbon Accessorization Notes: + * If your HIObjectClass or Carbon Event handler provides + * the kAXRoleAttribute, it must also provide the kAXRoleDescriptionAttribute. + */ + public static let role = kAXRoleAttribute + /** + * Abstract: + * More specifically identifies the type of an element beyond the basic type provided + * by kAXRoleAttribute. + * + * Value: + * A CFStringRef of one of the subrole strings defined in this header, or a new + * subrole string of your own invention. The string should not be localized, and it does + * not need to be human-readable. + * + * Writable: + * No + * + * Discussion: + * Required only when an element's kAXRoleAttribute alone doesn't provide an assistive + * application with enough information to convey the meaning of this element to the user. + * + * An example element that offers the kAXSubroleAttribute is a window's close box. Its + * kAXRoleAttribute value is kAXButtonRole and its kAXSubroleAttribute is + * kAXCloseButtonSubrole. It is of role kAXButtonRole because it offers no additional + * actions or attributes above and beyond what other kAXButtonRole elements provide; it + * was given a subrole in order to allow an assistive app to communicate the close box's + * semantic difference to the user. + * + * Carbon Accessorization Notes: + * If your HIObjectClass or Carbon Event handler provides + * the kAXSubroleAttribute, it must also provide the kAXRoleDescriptionAttribute. + */ + public static let subrole = kAXSubroleAttribute + /** + * Discussion: + * A localized, human-readable string that an assistive application can present to the user + * as an explanation of an element's basic type or purpose. Examples would be "push button" + * or "secure text field". The string's language should match the language of the app that + * the element lives within. The string should be all lower-case and contain no punctuation. + * + * Two elements with the same kAXRoleAttribute and kAXSubroleAttribute should have the + * same kAXRoleDescriptionAttribute. + * + * Value: + * A localized, human-readable CFStringRef + * + * Writable: + * No + * + * Abstract: + * Required for all elements. Even in the worst case scenario where an element cannot + * figure out what its basic type is, it can still supply the value "unknown". + * + * Carbon Accessorization Notes: + * The HIObjectClass or Carbon Event handler that provides + * the AXRole and/or AXSubrole for an element is the one that must also handle the + * AXRoleDescription attribute. If an HIObjectClass or Carbon Event handler does not + * provide either the AXRole or AXSubrole, it must not provide the AXRoleDescription. + */ + public static let roleDescription = kAXRoleDescriptionAttribute + /** + * Abstract: + * A localized, human-readable CFStringRef that offers help content for an element. + * + * Discussion: + * This is often the same information that would be provided in a help tag for the element. + * + * Recommended for any element that has help data available. + * + * Value: + * A localized, human-readable CFStringRef. + * + * Writable: + * No. + */ + public static let help = kAXHelpAttribute + /** + * Discussion: + * The localized, human-readable string that is displayed as part of the element's + * normal visual interface. For example, an OK button's kAXTitleElement is the string + * "OK", and a menu item's kAXTitleElement is the text of the menu item. + * + * Required if the element draws a string as part of its normal visual interface. + * + * Value: + * A localized, human-readable CFStringRef + * + * Writable: + * No + */ + public static let title = kAXTitleAttribute + /** + * A localized, human-readable string that indicates an element's purpose in a way + * that is slightly more specific than the kAXRoleDescriptionAttribute, but which + * is less wordy than the kAXHelpAttribute. Typically, the description should be + * an adjective or short phrase that describes the element's usage. For example, + * the description of a slider in a font panel might be "font size". The string + * should be all lower-case and contain no punctuation. + * + * Value: A localized, human-readable CFStringRef. + * + * Writable? No. + * + * Recommended for all elements because it gives the user a concise indication of + * an element's purpose. + */ + public static let description = kAXDescriptionAttribute + + // MARK: - Value Attributes + + /** + * Discussion: + * A catch-all attribute that represents a user modifiable setting of an element. For + * example, the contents of an editable text field, the position of a scroll bar thumb, + * and whether a check box is checked are all communicated by the kAXValueAttribute of + * their respective elements. + * + * Required for many user manipulatable elements, or those whose value state conveys + * important information. + * + * Value: + * Varies, but will always be the same type for a given kind of element. Each + * role that offers kAXValueAttribute will specify the type of data that will be used + * for its value. + * + * Writable: + * Generally yes. However, it does not need to be writable if some other form + * of direct manipulation is more appropriate for causing a value change. For example, + * a kAXScrollBar's kAXValueAttribute is writable because it allows an efficient way + * for the user to get to a specific position in the element being scrolled. By + * contrast, a kAXCheckBox's kAXValueAttribute is not settable because underlying + * functionality of the check box widget relies on it being clicked on; therefore, it + * changes its own kAXValueAttribute appropriately in response to the kAXPressAction. + * + * Required for many user manipulatable elements, or those whose value state conveys + * important information. + */ + public static let value = kAXValueAttribute + /** + * Used to supplement kAXValueAttribute. This attribute returns a string description that best + * describes the current value stored in kAXValueAttribute. This is useful for things like + * slider where the numeric value in kAXValueAttribute does not always convey enough information + * about the adjustment made on the slider. As an example, a color slider that adjusts thru various + * colors cannot be well-described by the numeric value in existing AXValueAttribute. This is where + * the kAXValueDescriptionAttribute comes in handy. In this example, the developer can provide the + * color information using this attribute. + * + * Value: A localized, human-readable CFStringRef. + * + * Writable? No. + * + * Recommended for elements that support kAXValueAttribute. + */ + public static let valueDescription = kAXValueDescriptionAttribute + /** + * Only used in conjunction with kAXValueAttribute and kAXMaxValueAttribute, this + * attribute represents the minimum value that an element can display. This is useful + * for things like sliders and scroll bars, where the user needs to have an understanding + * of how much the kAXValueAttribute can vary. + * + * Value: Same data type as the element's kAXValueAttribute. + * + * Writable? No. + * + * Required for many user maniipulatable elements. See kAXValueAttribute for more + * details. + */ + public static let minValue = kAXMinValueAttribute + /** + * Only used in conjunction with kAXValueAttribute and kAXMinValueAttribute, this + * attribute represents the maximum value that an element can display. This is useful + * for things like sliders and scroll bars, where the user needs to have an understanding + * of how much the kAXValueAttribute can vary. + * + * Value: Same data type as the element's kAXValueAttribute. + * + * Writable? No. + * + * Required for many user maniipulatable elements. See kAXValueAttribute for more + * details. + */ + public static let maxValue = kAXMaxValueAttribute + /** + * Only used in conjunction with kAXValueAttribute, this attribute represents the amount + * a value will change in one action on the given element. In particular, it is used on + * elements of role kAXIncrementorRole in order to give the user an idea of how much its + * value will change with a single click on the up or down arrow. + * + * Value: Same data type as the element's kAXValueAttribute. + * + * Writable? No. + * + * Recommended for kAXIncrementorRole and other similar elements. + */ + public static let valueIncrement = kAXValueIncrementAttribute + /** + * An array of the allowed values for a slider or other widget that displays + * a large value range, but which can only be set to a small subset of values + * within that range. + * + * Value: A CFArrayRef of whatever type the element uses for its kAXValueAttribute. + * + * Writable? No. + * + * Recommended for sliders or other elements that can only be set to a small + * set of values. + */ + public static let allowedValues = kAXAllowedValuesAttribute + /** + * kAXPlaceholderValueAttribute + * + * The value of placeholder text as found in a text field. + * + * Value: A CFStringRef. + * + * Writable? No. + * + * Recommended for text fields and other elements that have a placeholder value. + */ + public static let placeholderValue = kAXPlaceholderValueAttribute + public static let insertionPointLineNumber = kAXInsertionPointLineNumberAttribute + /** + * kAXFullScreenButtonAttribute + * + * A convenience attribute so assistive apps can quickly access a window's full screen + * button element. + * + * Value: An AXUIElementRef of the window's full screen button element. + * + * Writable? No. + * + * Required for all window elements that have a full screen button. + */ + public static let fullScreenButton = kAXFullScreenButtonAttribute + public static let valueWraps = kAXValueWrapsAttribute + + // MARK: - Visual state attributes + + /** + * Indicates whether the element can be interacted with by the user. For example, + * a disabled push button's kAXEnabledAttribute will be false. + * + * Value: A CFBooleanRef. True means enabled, false means disabled. + * + * Writable? No. + * + * Required for all views, menus, and menu items. Not required for windows. + */ + public static let enabled = kAXEnabledAttribute + /** + * Indicates whether the element is the current keyboard focus. It should be writable + * for any element that can accept keyboard focus, though you can only set the value + * of kAXFocusedAttribute to true. You cannot unfocus an element by setting the value + * to false. Only one element in a window's entire accessibility hierarchy should be + * marked as focused. + * + * Value: A CFBooleanRef. True means focused, false means not focused. + * + * Writable? Yes, for any focusable element. No in all other cases. + * + * Required for any focusable element. Not required for other elements, though it is + * often offered for non-focusable elements in a read-only fashion. + */ + public static let focused = kAXFocusedAttribute + /** + * The global screen position of the top-left corner of an element. + * + * Value: An AXValueRef with type kAXValueCGPointType. 0,0 is the top-left + * corner of the screen that displays the menu bar. The value of the horizontal + * axis increases to the right. The value of the vertical axis increases + * downward. Units are points. + * + * Writable? Generally no. However, some elements that can be moved by the user + * through direct manipulation (like windows) should offer a writable position + * attribute. + * + * Required for all elements that are visible on the screen, which is virtually + * all elements. + */ + public static let position = kAXPositionAttribute + /** + * The vertical and horizontal dimensions of the element. + * + * Value: An AXValueRef with type kAXValueCGSizeType. Units are points. + * + * Writable? Generally no. However, some elements that can be resized by the user + * through direct manipulation (like windows) should offer a writable size attribute. + * + * Required for all elements that are visible on the screen, which is virtually + * all elements. + */ + public static let size = kAXSizeAttribute + + // MARK: - Miscellaneous or role-specific attributes + + /** + * Indicates that an element is busy. This status conveys + * that an element is in the process of updating or loading and that + * new information will be available when the busy state completes. + * + * Value: A CFBooleanRef. True means busy, false means not busy. + * + * Writable? Yes. + */ + public static let elementBusy = kAXElementBusyAttribute + /** + * An indication of whether an element is drawn and/or interacted with in a + * vertical or horizontal manner. Elements such as scroll bars and sliders offer + * the kAXOrientationAttribute. + * + * Value: kAXHorizontalOrientationValue, kAXVerticalOrientationValue, or rarely + * kAXUnknownOrientationValue. + * + * Writable? No. + * + * Required for scroll bars, sliders, or other elements whose semantic or + * associative meaning changes based on their orientation. + */ + public static let orientation = kAXOrientationAttribute + /** + * A convenience attribute whose value is an element that is a header for another + * element. For example, an outline element has a header attribute whose value is + * a element of role AXGroup that contains the header buttons for each column. + * Used for things like tables, outlines, columns, etc. + * + * Value: An AXUIElementRef whose role varies. + * + * Writable? No. + * + * Recommended for elements that have header elements contained within them that an + * assistive application might want convenient access to. + */ + public static let header = kAXHeaderAttribute + public static let edited = kAXEditedAttribute + public static let tabs = kAXTabsAttribute + public static let horizontalScrollBar = kAXHorizontalScrollBarAttribute + public static let verticalScrollBar = kAXVerticalScrollBarAttribute + public static let overflowButton = kAXOverflowButtonAttribute + public static let filename = kAXFilenameAttribute + public static let expanded = kAXExpandedAttribute + public static let selected = kAXSelectedAttribute + public static let splitters = kAXSplittersAttribute + public static let nextContents = kAXNextContentsAttribute + public static let document = kAXDocumentAttribute + public static let decrementButton = kAXDecrementButtonAttribute + public static let incrementButton = kAXIncrementButtonAttribute + public static let previousContents = kAXPreviousContentsAttribute + /** + * A convenience attribute so assistive apps can find interesting child elements + * of a given element, while at the same time avoiding non-interesting child + * elements. For example, the contents of a scroll area are the children that get + * scrolled, and not the horizontal and/or vertical scroll bars. The contents of + * a tab group does not include the tabs themselves. + * + * Value: A CFArrayRef of AXUIElementRefs. + * + * Writable? No. + * + * Recommended for elements that have children that act upon or are separate from + * other children. + */ + public static let contents = kAXContentsAttribute + /** + * Convenience attribute that yields the incrementor of a time field or date + * field element. + * + * Value: A AXUIElementRef of role kAXIncrementorRole. + * + * Writable? No. + * + * Required for time field and date field elements that display an incrementor. + */ + public static let incrementor = kAXIncrementorAttribute + public static let columnTitle = kAXColumnTitleAttribute + /** + * Value: A CFURLRef. + * + * Writable? No. + * + * Required for elements that represent a disk or network item. + */ + public static let url = kAXURLAttribute + public static let labelUIElements = kAXLabelUIElementsAttribute + public static let labelValue = kAXLabelValueAttribute + public static let shownMenuUIElement = kAXShownMenuUIElementAttribute + public static let isApplicationRunning = kAXIsApplicationRunningAttribute + public static let focusedApplication = kAXFocusedApplicationAttribute + public static let alternateUIVisible = kAXAlternateUIVisibleAttribute + + // MARK: - Hierarchy or relationship attributes + + /** + * Indicates the element's container element in the visual element hierarchy. A push + * button's kAXParentElement might be a window element or a group. A sheet's + * kAXParentElement will be a window element. A window's kAXParentElement will be the + * application element. A menu item's kAXParentElement will be a menu element. + * + * Value: An AXUIElementRef. + * + * Writable? No. + * + * Required for every element except the application. Everything else in the visual + * element hierarchy must have a parent. + */ + public static let parent = kAXParentAttribute + /** + * Indicates the sub elements of a given element in the visual element hierarchy. A tab + * group's kAXChildrenAttribute is an array of tab radio button elements. A window's + * kAXChildrenAttribute is an array of the first-order views elements within the window. + * A menu's kAXChildrenAttribute is an array of the menu item elements. + * + * A given element may only be in the child array of one other element. If an element is + * in the child array of some other element, the element's kAXParentAttribute must be + * the other element. + * + * Value: A CFArrayRef of AXUIElementRefs. + * + * Writable? No. + * + * Required for elements that contain sub elements. + */ + public static let children = kAXChildrenAttribute + /** + * Indicates the selected sub elements of a given element in the visual element hierarchy. + * This is a the subset of the element's kAXChildrenAttribute that are selected. This is + * commonly used in lists so an assistive app can know which list item are selected. + * + * Value: A CFArrayRef of AXUIElementRefs. + * + * Writable? Only if there is no other way to manipulate the set of selected elements via + * accessibilty attributes or actions. Even if other ways exist, this attribute can be + * writable as a convenience to assistive applications and their users. If + * kAXSelectedChildrenAttribute is writable, a write request with a value of an empty + * array should deselect all selected children. + * + * Required for elements that contain selectable sub elements. + */ + public static let selectedChildren = kAXSelectedChildrenAttribute + /** + * Indicates the visible sub elements of a given element in the visual element hierarchy. + * This is a the subset of the element's kAXChildrenAttribute that a sighted user can + * see on the screen. In a list element, kAXVisibleChildrenAttribute would be an array + * of child elements that are currently scrolled into view. + * + * Value: A CFArrayRef of AXUIElementRefs. + * + * Writable? No. + * + * Recommended for elements whose child elements can be occluded or scrolled out of view. + */ + public static let visibleChildren = kAXVisibleChildrenAttribute + /** + * A short cut for traversing an element's parent hierarchy until an element of role + * kAXWindowRole is found. Note that the value for kAXWindowAttribute should not be + * an element of role kAXSheetRole or kAXDrawerRole; instead, the value should be the + * element of kAXWindowRole that the sheet or drawer is attached to. + * + * Value: an AXUIElementRef of role kAXWindowRole. + * + * Writable? No. + * + * Required for any element that has an element of role kAXWindowRole somewhere + * in its parent chain. + */ + public static let window = kAXWindowAttribute + /** + * This is very much like the kAXWindowAttribute, except that the value of this + * attribute can be an element with role kAXSheetRole or kAXDrawerRole. It is + * a short cut for traversing an element's parent hierarchy until an element of + * role kAXWindowRole, kAXSheetRole, or kAXDrawerRole is found. + * + * Value: An AXUIElementRef of role kAXWindowRole, kAXSheetRole, or kAXDrawerRole. + * + * Writable? No. + * + * Required for any element that has an appropriate element somewhere in its + * parent chain. + */ + public static let topLevelUIElement = kAXTopLevelUIElementAttribute + /** + * Returns an array of elements that also have keyboard focus when a given element has + * keyboard focus. A common usage of this attribute is to report that both a search + * text field and a list of resulting suggestions share keyboard focus because keyboard + * events can be handled by either UI element. In this example, the text field would be + * the first responder and it would report the list of suggestions as an element in the + * array returned for kAXSharedFocusElementsAttribute. + * + * Value: A CFArrayRef of AXUIElementsRefs. + * + * Writable? No. + */ + public static let sharedFocusElements = kAXSharedFocusElementsAttribute + public static let titleUIElement = kAXTitleUIElementAttribute + public static let servesAsTitleForUIElements = kAXServesAsTitleForUIElementsAttribute + public static let linkedUIElements = kAXLinkedUIElementsAttribute + + // MARK: - Text-specific attributes + + /** + * The selected text of an editable text element. + * + * Value: A CFStringRef with the currently selected text of the element. + * + * Writable? No. + * + * Required for all editable text elements. + */ + public static let selectedText = kAXSelectedTextAttribute + /** + * The range of characters (not bytes) that defines the current selection of an + * editable text element. + * + * Value: An AXValueRef of type kAXValueCFRange. + * + * Writable? Yes. + * + * Required for all editable text elements. + */ + public static let selectedTextRange = kAXSelectedTextRangeAttribute + /** + * An array of noncontiguous ranges of characters (not bytes) that defines the current selections of an + * editable text element. + * + * Value: A CFArrayRef of kAXValueCFRanges. + * + * Writable? Yes. + * + * Recommended for text elements that support noncontiguous selections. + */ + public static let selectedTextRanges = kAXSelectedTextRangesAttribute + /** + * The range of characters (not bytes) that are scrolled into view in the editable + * text element. + * + * Value: An AXValueRef of type kAXValueCFRange. + * + * Writable? No. + * + * Required for elements of role kAXTextAreaRole. Not required for any other + * elements, including those of role kAXTextFieldRole. + */ + public static let visibleCharacterRange = kAXVisibleCharacterRangeAttribute + /** + * The total number of characters (not bytes) in an editable text element. + * + * Value: CFNumberRef + * + * Writable? No. + * + * Required for editable text elements. + */ + public static let numberOfCharacters = kAXNumberOfCharactersAttribute + /** + * Value: CFArrayRef of AXUIElementRefs + * + * Writable? No. + * + * Optional? + */ + public static let sharedTextUIElements = kAXSharedTextUIElementsAttribute + /** + * Value: AXValueRef of type kAXValueCFRangeType + * + * Writable? No. + * + * Optional? + */ + public static let sharedCharacterRange = kAXSharedCharacterRangeAttribute + + // MARK: - Window, sheet, or drawer-specific attributes + + /** + * Whether a window is the main document window of an application. For an active + * app, the main window is the single active document window. For an inactive app, + * the main window is the single document window which would be active if the app + * were active. Main does not necessarily imply that the window has key focus. + * + * Value: A CFBooleanRef. True means the window is main. False means it is not. + * + * Writable? Yes. + * + * Required for all window elements. + */ + public static let main = kAXMainAttribute + /** + * Whether a window is currently minimized to the dock. + * + * Value: A CFBooleanRef. True means minimized. + * + * Writable? Yes. + * + * Required for all window elements that can be minimized. + */ + public static let minimized = kAXMinimizedAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's close + * button element. + * + * Value: An AXUIElementRef of the window's close button element. + * + * Writable? No. + * + * Required for all window elements that have a close button. + */ + public static let closeButton = kAXCloseButtonAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's zoom + * button element. + * + * Value: An AXUIElementRef of the window's zoom button element. + * + * Writable? No. + * + * Required for all window elements that have a zoom button. + */ + public static let zoomButton = kAXZoomButtonAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's minimize + * button element. + * + * Value: An AXUIElementRef of the window's minimize button element. + * + * Writable? No. + * + * Required for all window elements that have a minimize button. + */ + public static let minimizeButton = kAXMinimizeButtonAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's toolbar + * button element. + * + * Value: An AXUIElementRef of the window's toolbar button element. + * + * Writable? No. + * + * Required for all window elements that have a toolbar button. + */ + public static let toolbarButton = kAXToolbarButtonAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's document + * proxy element. + * + * Value: An AXUIElementRef of the window's document proxy element. + * + * Writable? No. + * + * Required for all window elements that have a document proxy. + */ + public static let proxy = kAXProxyAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's grow + * area element. + * + * Value: An AXUIElementRef of the window's grow area element. + * + * Writable? No. + * + * Required for all window elements that have a grow area. + */ + public static let growArea = kAXGrowAreaAttribute + /** + * Whether a window is modal. + * + * Value: A CFBooleanRef. True means the window is modal. + * + * Writable? No. + * + * Required for all window elements. + */ + public static let modal = kAXModalAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's default + * button element, if any. + * + * Value: An AXUIElementRef of the window's default button element. + * + * Writable? No. + * + * Required for all window elements that have a default button. + */ + public static let defaultButton = kAXDefaultButtonAttribute + /** + * A convenience attribute so assistive apps can quickly access a window's cancel + * button element, if any. + * + * Value: An AXUIElementRef of the window's cancel button element. + * + * Writable? No. + * + * Required for all window elements that have a cancel button. + */ + public static let cancelButton = kAXCancelButtonAttribute + + // MARK: - Menu or menu item-specific attributes + + public static let menuItemCmdChar = kAXMenuItemCmdCharAttribute + public static let menuItemCmdVirtualKey = kAXMenuItemCmdVirtualKeyAttribute + public static let menuItemCmdGlyph = kAXMenuItemCmdGlyphAttribute + public static let menuItemCmdModifiers = kAXMenuItemCmdModifiersAttribute + public static let menuItemMarkChar = kAXMenuItemMarkCharAttribute + public static let menuItemPrimaryUIElement = kAXMenuItemPrimaryUIElementAttribute + + // MARK: - Application element-specific attributes + + public static let menuBar = kAXMenuBarAttribute + public static let windows = kAXWindowsAttribute + public static let frontmost = kAXFrontmostAttribute + public static let hidden = kAXHiddenAttribute + public static let mainWindow = kAXMainWindowAttribute + public static let focusedWindow = kAXFocusedWindowAttribute + public static let focusedUIElement = kAXFocusedUIElementAttribute + public static let extrasMenuBar = kAXExtrasMenuBarAttribute + + // MARK: - Date/time-specific attributes + + /** + * Convenience attribute that yields the hour field of a time field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * hours in a time field element. + * + * Writable? No. + * + * Required for time field elements that display hours. + */ + public static let hourField = kAXHourFieldAttribute + /** + * Convenience attribute that yields the minute field of a time field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * minutes in a time field element. + * + * Writable? No. + * + * Required for time field elements that display minutes. + */ + public static let minuteField = kAXMinuteFieldAttribute + /** + * Convenience attribute that yields the seconds field of a time field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * seconds in a time field element. + * + * Writable? No. + * + * Required for time field elements that display seconds. + */ + public static let secondField = kAXSecondFieldAttribute + /** + * Convenience attribute that yields the AM/PM field of a time field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * AM/PM setting in a time field element. + * + * Writable? No. + * + * Required for time field elements that displays an AM/PM setting. + */ + public static let ampmField = kAXAMPMFieldAttribute + /** + * Convenience attribute that yields the day field of a date field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * day in a date field element. + * + * Writable? No. + * + * Required for date field elements that display days. + */ + public static let dayField = kAXDayFieldAttribute + /** + * Convenience attribute that yields the month field of a date field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * month in a date field element. + * + * Writable? No. + * + * Required for date field elements that display months. + */ + public static let monthField = kAXMonthFieldAttribute + /** + * Convenience attribute that yields the year field of a date field element. + * + * Value: A AXUIElementRef of role kAXTextFieldRole that is used to edit the + * year in a date field element. + * + * Writable? No. + * + * Required for date field elements that display years. + */ + public static let yearField = kAXYearFieldAttribute + + // MARK: - Table, outline, or browser-specific attributes + + public static let rows = kAXRowsAttribute + public static let visibleRows = kAXVisibleRowsAttribute + public static let selectedRows = kAXSelectedRowsAttribute + public static let columns = kAXColumnsAttribute + /** + * Indicates the visible column sub-elements of a kAXBrowserRole element. + * This is the subset of a browser's kAXColumnsAttribute where each column in the + * array is one that is currently scrolled into view within the browser. It does + * not include any columns that are currently scrolled out of view. + * + * Value: A CFArrayRef of AXUIElementRefs representing the columns of a browser. + * The columns will be grandchild elements of the browser, and will generally be + * of role kAXScrollArea. + * + * Writable? No. + * + * Required for all browser elements. + */ + public static let visibleColumns = kAXVisibleColumnsAttribute + public static let selectedColumns = kAXSelectedColumnsAttribute + public static let sortDirection = kAXSortDirectionAttribute + public static let index = kAXIndexAttribute + public static let disclosing = kAXDisclosingAttribute + public static let disclosedRows = kAXDisclosedRowsAttribute + public static let disclosedByRow = kAXDisclosedByRowAttribute + public static let columnHeaderUIElements = kAXColumnHeaderUIElementsAttribute + + // MARK: - Outline attributes + + public static let disclosureLevel = kAXDisclosureLevelAttribute + + // MARK: - Matte-specific attributes + + public static let matteHole = kAXMatteHoleAttribute + public static let matteContentUIElement = kAXMatteContentUIElementAttribute + + // MARK: - Ruler-specific attributes + + public static let markerUIElements = kAXMarkerUIElementsAttribute + public static let units = kAXUnitsAttribute + public static let unitDescription = kAXUnitDescriptionAttribute + public static let markerType = kAXMarkerTypeAttribute + public static let markerTypeDescription = kAXMarkerTypeDescriptionAttribute + + // MARK: - Search field attributes + + public static let searchButton = kAXSearchButtonAttribute + public static let clearButton = kAXClearButtonAttribute + + // MARK: - Grid attributes + + public static let rowCount = kAXRowCountAttribute + public static let columnCount = kAXColumnCountAttribute + public static let orderedByRow = kAXOrderedByRowAttribute + + // MARK: - Level indicator attributes + + public static let warningValue = kAXWarningValueAttribute + public static let criticalValue = kAXCriticalValueAttribute + + // MARK: - Cell-based table attributes + + public static let selectedCells = kAXSelectedCellsAttribute + public static let visibleCells = kAXVisibleCellsAttribute + public static let rowHeaderUIElements = kAXRowHeaderUIElementsAttribute + + // MARK: - Cell attributes + + public static let rowIndexRange = kAXRowIndexRangeAttribute + public static let columnIndexRange = kAXColumnIndexRangeAttribute + + // MARK: - Layout area attributes + + public static let horizontalUnits = kAXHorizontalUnitsAttribute + public static let verticalUnits = kAXVerticalUnitsAttribute + public static let horizontalUnitDescription = kAXHorizontalUnitDescriptionAttribute + public static let verticalUnitDescription = kAXVerticalUnitDescriptionAttribute + public static let handles = kAXHandlesAttribute + + // MARK: - Obsolete/unknown attributes + + public static let text = kAXTextAttribute + public static let visibleText = kAXVisibleTextAttribute + public static let isEditable = kAXIsEditableAttribute + public static let columnTitles = kAXColumnTitlesAttribute + + // MARK: - UI element identification attributes + + public static let identifier = kAXIdentifierAttribute + + // MARK: - Attributes + + public static let ariaAtomic = kAXARIAAtomicAttribute + /** + * CFNumberRef, 1-based + */ + public static let ariaColumnCount = kAXARIAColumnCountAttribute + /** + * CFNumberRef, 1-based + */ + public static let ariaColumnIndex = kAXARIAColumnIndexAttribute + /** + * CFStringRef + */ + public static let ariaCurrent = kAXARIACurrentAttribute + /** + * CFStringRef + */ + public static let ariaLive = kAXARIALiveAttribute + /** + * CFNumberRef, 1-based + */ + public static let ariaPosInSet = kAXARIAPosInSetAttribute + /** + * CFStringRef + */ + public static let ariaRelevant = kAXARIARelevantAttribute + /** + * CFNumberRef, 1-based + */ + public static let ariaRowCount = kAXARIARowCountAttribute + /** + * CFNumberRef, 1-based + */ + public static let ariaRowIndex = kAXARIARowIndexAttribute + /** + * CFNumberRef, 1-based + */ + public static let ariaSetSize = kAXARIASetSizeAttribute + /** + * CFStringRef + */ + public static let accessKey = kAXAccessKeyAttribute + /** + * AXUIElementRef + */ + public static let activeElement = kAXActiveElementAttribute + /** + * CFStringRef + */ + public static let brailleLabel = kAXBrailleLabelAttribute + /** + * CFStringRef + */ + public static let brailleRoleDescription = kAXBrailleRoleDescriptionAttribute + /** + * CFBooleanRef + */ + public static let caretBrowsingEnabled = kAXCaretBrowsingEnabledAttribute + /** + * CFArrayRef of CFStringRef + */ + public static let domClassList = kAXDOMClassListAttribute + /** + * CFStringRef + */ + public static let domIdentifier = kAXDOMIdentifierAttribute + /** + * CFStringRef + */ + public static let datetimeValue = kAXDatetimeValueAttribute + /** + * CFArrayRef of AXUIElementRef + */ + public static let describedBy = kAXDescribedByAttribute + /** + * CFArrayRef of CFStringRef + */ + public static let dropEffects = kAXDropEffectsAttribute + /** + * AXUIElementRef + */ + public static let editableAncestor = kAXEditableAncestorAttribute + /** + * AXTextMarkerRef + */ + public static let endTextMarker = kAXEndTextMarkerAttribute + /** + * CFArrayRef of AXUIElementRef + */ + public static let errorMessageElements = kAXErrorMessageElementsAttribute + /** + * CFBooleanRef + */ + public static let expandedTextValue = kAXExpandedTextValueAttribute + /** + * AXUIElementRef + */ + public static let focusableAncestor = kAXFocusableAncestorAttribute + /** + * CFBooleanRef + */ + public static let grabbed = kAXGrabbedAttribute + /** + * CFBooleanRef + */ + public static let hasDocumentRoleAncestor = kAXHasDocumentRoleAncestorAttribute + /** + * CFBooleanRef + */ + public static let hasPopup = kAXHasPopupAttribute + /** + * CFBooleanRef + */ + public static let hasWebApplicationAncestor = kAXHasWebApplicationAncestorAttribute + /** + * AXUIElementRef + */ + public static let highestEditableAncestor = kAXHighestEditableAncestorAttribute + /** + * CFBooleanRef + */ + public static let inlineText = kAXInlineTextAttribute + /** + * CFRange + */ + public static let intersectionWithSelectionRange = kAXIntersectionWithSelectionRangeAttribute + /** + * CFStringRef + */ + public static let invalid = kAXInvalidAttribute + /** + * CFStringRef + */ + public static let keyShortcuts = kAXKeyShortcutsAttribute + /** + * CFArrayRef of AXUIElementRef + */ + public static let linkUIElements = kAXLinkUIElementsAttribute + /** + * CFBooleanRef + */ + public static let loaded = kAXLoadedAttribute + /** + * CFNumber, double, 0.0 - 1.0 + */ + public static let loadingProgress = kAXLoadingProgressAttribute + /** + * AXUIElementRef + */ + public static let mathBase = kAXMathBaseAttribute + /** + * CFStringRef + */ + public static let mathFencedClose = kAXMathFencedCloseAttribute + /** + * CFStringRef + */ + public static let mathFencedOpen = kAXMathFencedOpenAttribute + /** + * AXUIElementRef + */ + public static let mathFractionDenominator = kAXMathFractionDenominatorAttribute + /** + * AXUIElementRef + */ + public static let mathFractionNumerator = kAXMathFractionNumeratorAttribute + /** + * CFNumberRef + */ + public static let mathLineThickness = kAXMathLineThicknessAttribute + /** + * AXUIElementRef + */ + public static let mathOver = kAXMathOverAttribute + /** + * CFArrayRef of CFDictionary + */ + public static let mathPostscripts = kAXMathPostscriptsAttribute + /** + * CFArrayRef of CFDictionary + */ + public static let mathPrescripts = kAXMathPrescriptsAttribute + /** + * AXUIElementRef + */ + public static let mathRootIndex = kAXMathRootIndexAttribute + /** + * CFArrayRef of AXUIElementRef + */ + public static let mathRootRadicand = kAXMathRootRadicandAttribute + /** + * AXUIElementRef + */ + public static let mathSubscript = kAXMathSubscriptAttribute + /** + * AXUIElementRef + */ + public static let mathSuperscript = kAXMathSuperscriptAttribute + /** + * AXUIElementRef + */ + public static let mathUnder = kAXMathUnderAttribute + /** + * CFArrayRef of AXUIElementRef + */ + public static let owns = kAXOwnsAttribute + /** + * CFStringRef + */ + public static let popupValue = kAXPopupValueAttribute + /** + * CFBooleanRef + */ + public static let preventKeyboardDOMEventDispatch = kAXPreventKeyboardDOMEventDispatchAttribute + /** + * AXTextMarkerRangeRef + */ + public static let selectedTextMarkerRange = kAXSelectedTextMarkerRangeAttribute + /** + * AXTextMarkerRef + */ + public static let startTextMarker = kAXStartTextMarkerAttribute + /** + * AXTextMarkerRangeRef + */ + public static let textInputMarkedTextMarkerRange = kAXTextInputMarkedTextMarkerRangeAttribute + /** + * CFBooleanRef + */ + public static let valueAutofillAvailable = kAXValueAutofillAvailableAttribute + + // MARK: - Attributed string keys + + public static let didSpellCheckString = kAXDidSpellCheckStringAttribute + /** + * CFBooleanRef + */ + public static let highlightString = kAXHighlightStringAttribute + /** + * CFBooleanRef + */ + public static let isSuggestedDeletionString = kAXIsSuggestedDeletionStringAttribute + /** + * CFBooleanRef + */ + public static let isSuggestedInsertionString = kAXIsSuggestedInsertionStringAttribute + /** + * CFBooleanRef + */ + public static let isSuggestionString = kAXIsSuggestionStringAttribute + } +} diff --git a/Sources/AccessibilityControl/Accessibility+Notification.swift b/Sources/AccessibilityControl/Accessibility+Notification.swift new file mode 100644 index 0000000..748662a --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+Notification.swift @@ -0,0 +1,296 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXNotificationConstants.h, System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXWebConstants.h + +public extension Accessibility.Notification { + // MARK: - Focus Notifications + + /** + * Abstract: + * Notification of a change in the main window. + * + * Discussion: + * Value is the new main window UIElement or the + * Application UIElement if there's no main window. + */ + static let mainWindowChanged = Self(kAXMainWindowChangedNotification) + /** + * Abstract: + * Notification that the focused window changed. + */ + static let focusedWindowChanged = Self(kAXFocusedWindowChangedNotification) + /** + * Abstract: + * Notification that the focused UI element has changed. + * + * Discussion: + * Value is the new focused UIElement or + * the Application UIElement if there's no focus + */ + static let focusedUIElementChanged = Self(kAXFocusedUIElementChangedNotification) + + // MARK: - Application Notifications + + /** + * Abstract: + * Notification that an application was activated. + * + * Discussion: + * Value is an application UIElement. + */ + static let applicationActivated = Self(kAXApplicationActivatedNotification) + /** + * Abstract: + * Notification that an application was deactivated. + * + * Discussion: + * Value is an application UIElement + */ + static let applicationDeactivated = Self(kAXApplicationDeactivatedNotification) + /** + * Abstract: + * Notification that an application has been hidden. + * + * Discussion: + * Value is an application UIElement + */ + static let applicationHidden = Self(kAXApplicationHiddenNotification) + /** + * Abstract: + * Notification that an application is no longer hidden. + * + * Discussion: + * Value is an application UIElement + */ + static let applicationShown = Self(kAXApplicationShownNotification) + + // MARK: - Window Notifications + + /** + * Abstract: + * Notification that a window was created. + * + * Discussion: + * Value is a new window UIElement + */ + static let windowCreated = Self(kAXWindowCreatedNotification) + /** + * Abstract: + * Notification that a window moved. + * + * Discussion: + * This notification is sent at the end of the window move, not continuously as the window is being moved. + * + * Value is the moved window UIElement + */ + static let windowMoved = Self(kAXWindowMovedNotification) + /** + * Abstract: + * Notification that a window was resized. + * + * Discussion: + * This notification is sent at the end of the window resize, not continuously as the window is being resized. + * + * Value is the resized window UIElement + */ + static let windowResized = Self(kAXWindowResizedNotification) + /** + * Abstract: + * Notification that a window was minimized. + * + * Discussion: + * Value is the minimized window UIElement + */ + static let windowMiniaturized = Self(kAXWindowMiniaturizedNotification) + /** + * Abstract: + * Notification that a window is no longer minimized. + * + * Discussion: + * Value is the unminimized window UIElement + */ + static let windowDeminiaturized = Self(kAXWindowDeminiaturizedNotification) + + // MARK: - New Drawer, Sheet, and Help Notifications + + /** + * Abstract: + * Notification that a drawer was created. + */ + static let drawerCreated = Self(kAXDrawerCreatedNotification) + /** + * Abstract: + * Notification that a sheet was created. + */ + static let sheetCreated = Self(kAXSheetCreatedNotification) + /** + * Abstract: + * Notification that a help tag was created. + */ + static let helpTagCreated = Self(kAXHelpTagCreatedNotification) + + // MARK: - Element Notifications + + /** + * Discussion: + * This notification is sent when the value of the UIElement's value attribute has changed, not when the value of any other attribute has changed. + * + * Value is the modified UIElement + */ + static let valueChanged = Self(kAXValueChangedNotification) + /** + * Discussion: + * The returned UIElement is no longer valid in the target application. You can still use the local reference + * with calls like CFEqual (for example, to remove it from a list), but you should not pass it to the accessibility APIs. + * + * Value is the destroyed UIElement + */ + static let uiElementDestroyed = Self(kAXUIElementDestroyedNotification) + /** + * Abstract: + * Notification that an element's busy state has changed. + * + * Discussion: + * Value is the (un)busy UIElement. + */ + static let elementBusyChanged = Self(kAXElementBusyChangedNotification) + + // MARK: - Menu Notifications + + /** + * Abstract: + * Notification that a menu has been opened. + * + * Discussion: + * Value is the opened menu UIElement. + */ + static let menuOpened = Self(kAXMenuOpenedNotification) + /** + * Abstract: + * Notification that a menu has been closed. + * + * Discussion: + * Value is the closed menu UIElement. + */ + static let menuClosed = Self(kAXMenuClosedNotification) + /** + * Abstract: + * Notification that a menu item has been seleted. + * + * Discussion: + * Value is the selected menu item UIElement. + */ + static let menuItemSelected = Self(kAXMenuItemSelectedNotification) + + // MARK: - Table/outline notifications + + /** + * Abstract: + * Notification that the number of rows in this table has changed. + */ + static let rowCountChanged = Self(kAXRowCountChangedNotification) + + // MARK: - Outline notifications + + /** + * Abstract: + * Notification that a row in an outline has been expanded. + * + * Discussion: + * The value is the collapsed row UIElement. + */ + static let rowExpanded = Self(kAXRowExpandedNotification) + /** + * Abstract: + * Notification that a row in an outline has been collapsed. + * + * Discussion: + * The value is the collapsed row UIElement. + */ + static let rowCollapsed = Self(kAXRowCollapsedNotification) + + // MARK: - Cell-based table notifications + + /** + * Abstract: + * Notification that the selected cells have changed. + */ + static let selectedCellsChanged = Self(kAXSelectedCellsChangedNotification) + + // MARK: - Layout area notifications + + /** + * Abstract: + * Notification that the units have changed. + */ + static let unitsChanged = Self(kAXUnitsChangedNotification) + /** + * Abstract: + * Notification that the selected children have moved. + */ + static let selectedChildrenMoved = Self(kAXSelectedChildrenMovedNotification) + + // MARK: - Other notifications + + /** + * Abstract: + * Notification that a different subset of this element's children were selected. + */ + static let selectedChildrenChanged = Self(kAXSelectedChildrenChangedNotification) + /** + * Abstract: + * Notification that this element has been resized. + */ + static let resized = Self(kAXResizedNotification) + /** + * Abstract: + * Notification that this element has moved. + */ + static let moved = Self(kAXMovedNotification) + /** + * Abstract: + * Notification that an element was created. + */ + static let created = Self(kAXCreatedNotification) + /** + * Abstract: + * Notification that the set of selected rows changed. + */ + static let selectedRowsChanged = Self(kAXSelectedRowsChangedNotification) + /** + * Abstract: + * Notification that the set of selected columns changed. + */ + static let selectedColumnsChanged = Self(kAXSelectedColumnsChangedNotification) + /** + * Abstract: + * Notification that a different set of text was selected. + */ + static let selectedTextChanged = Self(kAXSelectedTextChangedNotification) + /** + * Abstract: + * Notification that the title changed. + */ + static let titleChanged = Self(kAXTitleChangedNotification) + /** + * Abstract: + * Notification that the layout changed. + */ + static let layoutChanged = Self(kAXLayoutChangedNotification) + /** + * Abstract: + * Notification to request an announcement to be spoken. + */ + static let announcementRequested = Self(kAXAnnouncementRequestedNotification) + + // MARK: - Notifications + + static let activeElementChanged = Self(kAXActiveElementChangedNotification) + static let currentStateChanged = Self(kAXCurrentStateChangedNotification) + static let expandedChanged = Self(kAXExpandedChangedNotification) + static let invalidStatusChanged = Self(kAXInvalidStatusChangedNotification) + static let layoutComplete = Self(kAXLayoutCompleteNotification) + static let liveRegionChanged = Self(kAXLiveRegionChangedNotification) + static let liveRegionCreated = Self(kAXLiveRegionCreatedNotification) + static let loadComplete = Self(kAXLoadCompleteNotification) +} diff --git a/Sources/AccessibilityControl/Accessibility+ParameterizedAttributeKey.swift b/Sources/AccessibilityControl/Accessibility+ParameterizedAttributeKey.swift new file mode 100644 index 0000000..f3b3faf --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+ParameterizedAttributeKey.swift @@ -0,0 +1,158 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXAttributeConstants.h, System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXWebConstants.h + +extension Accessibility { + public enum ParameterizedAttributeKey { + // MARK: - Text Suite Parameterized Attributes + + public static let lineForIndex = kAXLineForIndexParameterizedAttribute + public static let rangeForLine = kAXRangeForLineParameterizedAttribute + public static let stringForRange = kAXStringForRangeParameterizedAttribute + public static let rangeForPosition = kAXRangeForPositionParameterizedAttribute + public static let rangeForIndex = kAXRangeForIndexParameterizedAttribute + public static let boundsForRange = kAXBoundsForRangeParameterizedAttribute + public static let rtfForRange = kAXRTFForRangeParameterizedAttribute + public static let attributedStringForRange = kAXAttributedStringForRangeParameterizedAttribute + public static let styleRangeForIndex = kAXStyleRangeForIndexParameterizedAttribute + + // MARK: - Cell-based table parameterized attributes + + public static let cellForColumnAndRow = kAXCellForColumnAndRowParameterizedAttribute + + // MARK: - Layout area parameterized attributes + + public static let layoutPointForScreenPoint = kAXLayoutPointForScreenPointParameterizedAttribute + public static let layoutSizeForScreenSize = kAXLayoutSizeForScreenSizeParameterizedAttribute + public static let screenPointForLayoutPoint = kAXScreenPointForLayoutPointParameterizedAttribute + public static let screenSizeForLayoutSize = kAXScreenSizeForLayoutSizeParameterizedAttribute + + // MARK: - Parameterized Attributes + + public static let attributedStringForTextMarkerRange = kAXAttributedStringForTextMarkerRangeParameterizedAttribute + /** + * (NSValue *) - (rectValue); param: AXTextMarkerRangeRef + */ + public static let boundsForTextMarkerRange = kAXBoundsForTextMarkerRangeParameterizedAttribute + + // MARK: - (NSValue *) - (rectValue); param: (NSValue *) - (rectValue) + + public static let convertRelativeFrame = kAXConvertRelativeFrameParameterizedAttribute + /** + * CFNumberRef; param: AXTextMarkerRef + */ + public static let indexForTextMarker = kAXIndexForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let leftLineTextMarkerRangeForTextMarker = kAXLeftLineTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let leftWordTextMarkerRangeForTextMarker = kAXLeftWordTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * CFNumberRef; param: AXTextMarkerRangeRef + */ + public static let lengthForTextMarkerRange = kAXLengthForTextMarkerRangeParameterizedAttribute + /** + * CFNumberRef; param: AXTextMarkerRef + */ + public static let lineForTextMarker = kAXLineForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let lineTextMarkerRangeForTextMarker = kAXLineTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let nextLineEndTextMarkerForTextMarker = kAXNextLineEndTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let nextParagraphEndTextMarkerForTextMarker = kAXNextParagraphEndTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let nextSentenceEndTextMarkerForTextMarker = kAXNextSentenceEndTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let nextTextMarkerForTextMarker = kAXNextTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let nextWordEndTextMarkerForTextMarker = kAXNextWordEndTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let paragraphTextMarkerRangeForTextMarker = kAXParagraphTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let previousLineStartTextMarkerForTextMarker = kAXPreviousLineStartTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let previousParagraphStartTextMarkerForTextMarker = kAXPreviousParagraphStartTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let previousSentenceStartTextMarkerForTextMarker = kAXPreviousSentenceStartTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let previousTextMarkerForTextMarker = kAXPreviousTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: AXTextMarkerRef + */ + public static let previousWordStartTextMarkerForTextMarker = kAXPreviousWordStartTextMarkerForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let rightLineTextMarkerRangeForTextMarker = kAXRightLineTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let rightWordTextMarkerRangeForTextMarker = kAXRightWordTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let sentenceTextMarkerRangeForTextMarker = kAXSentenceTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * CFStringRef; param: AXTextMarkerRef + */ + public static let stringForTextMarkerRange = kAXStringForTextMarkerRangeParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXTextMarkerRef + */ + public static let styleTextMarkerRangeForTextMarker = kAXStyleTextMarkerRangeForTextMarkerParameterizedAttribute + /** + * AXTextMarkerRef; param: CFNumberRef + */ + public static let textMarkerForIndex = kAXTextMarkerForIndexParameterizedAttribute + + // MARK: - AXTextMarkerRef; param: (NSValue *) - (pointValue) + + public static let textMarkerForPosition = kAXTextMarkerForPositionParameterizedAttribute + /** + * CFBooleanRef; param: AXTextMarkerRef + */ + public static let textMarkerIsValid = kAXTextMarkerIsValidParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: CFNumberRef + */ + public static let textMarkerRangeForLine = kAXTextMarkerRangeForLineParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: AXUIElementRef + */ + public static let textMarkerRangeForUIElement = kAXTextMarkerRangeForUIElementParameterizedAttribute + /** + * AXTextMarkerRangeRef; param: CFArrayRef of AXTextMarkerRef + */ + public static let textMarkerRangeForUnorderedTextMarkers = kAXTextMarkerRangeForUnorderedTextMarkersParameterizedAttribute + /** + * AXUIElementRef; param: AXTextMarkerRef + */ + public static let uiElementForTextMarker = kAXUIElementForTextMarkerParameterizedAttribute + } +} diff --git a/Sources/AccessibilityControl/Accessibility+Role.swift b/Sources/AccessibilityControl/Accessibility+Role.swift new file mode 100644 index 0000000..8b5bf00 --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+Role.swift @@ -0,0 +1,176 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXRoleConstants.h, System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXWebConstants.h + +extension Accessibility { + public enum Role { + // MARK: - Standard Roles + + public static let application = kAXApplicationRole + public static let systemWide = kAXSystemWideRole + public static let window = kAXWindowRole + public static let sheet = kAXSheetRole + public static let drawer = kAXDrawerRole + public static let growArea = kAXGrowAreaRole + public static let image = kAXImageRole + public static let unknown = kAXUnknownRole + public static let button = kAXButtonRole + public static let radioButton = kAXRadioButtonRole + public static let checkBox = kAXCheckBoxRole + public static let popUpButton = kAXPopUpButtonRole + public static let menuButton = kAXMenuButtonRole + public static let tabGroup = kAXTabGroupRole + public static let table = kAXTableRole + public static let column = kAXColumnRole + public static let row = kAXRowRole + /** + * Discussion: + * An element that contains row-based data. It may use disclosure triangles to manage the + * display of hierarchies within the data. It may arrange each row's data into columns and + * offer a header button above each column. The best example is the list view in a Finder + * window or Open/Save dialog. + * + * Outlines are typically children of AXScrollAreas, which manages the horizontal and/or + * vertical scrolling for the outline. Outlines are expected to follow certain conventions + * with respect to their hierarchy of sub-elements. In particular, if the outline uses + * columns, the data should be accessible via either rows or columns. Thus, the data in a + * given cell will be represented as two diffrent elements. Here's a hierarchy for a + * typical outline: + * + *
+         * AXScrollArea (parent of the outline)
+         * AXScrollBar (if necessary, horizontal)
+         * AXScrollBar (if necessary, vertical)
+         * AXOutline
+         * AXGroup (header buttons, optional)
+         * AXButton, AXMenuButton, or  (header button)
+         * ...
+         * AXRow (first row)
+         * AXStaticText (just one possible example)
+         * AXButton (just another possible example)
+         * AXTextField (ditto)
+         * AXCheckBox (ditto)
+         * AXRow (as above)
+         * ...
+         * AXColumn (first column)
+         * AXStaticText (assumes the first column displays text)
+         * AXStaticText
+         * ...
+         * AXColumn (second column)
+         * AXButton (assumes the second column displays buttons)
+         * AXButton
+         * ...
+         * ...
+         * 
+ * + * Supported attributes: + * + *
+ *
AXFocused
+ *
(w)
+ *
AXRows
+ *
Array of subset of AXChildren that are rows
+ *
AXVisibleRows
+ *
Array of subset of AXRows that are visible
+ *
AXSelectedRows
+ *
Array of subset of AXRows that are selected (w)
+ *
AXColumns
+ *
Array of subset of children that are columns
+ *
AXVisibleColumns
+ *
Array of subset of columns that are visible
+ *
AXSelectedColumns
+ *
Array of subset of columns that are selected (o)
+ *
AXHeader
+ *
The AXGroup element that contains the header buttons (o)
+ *
+ */ + public static let outline = kAXOutlineRole + /** + * Discussion: + * An element that contains columns of hierarchical data. Examples include the column view + * in Finder windows and Open/Save dialogs. Carbon's Data Browser in column view mode + * represents itself as an AXBrowser. Cocoa's NSBrowser represents itself as an AXBrowser. + * + * Browser elements are expected to have a particular hierarchy of sub-elements within it. + * In particular, the child of an AXBrowser must be an AXScrollArea that manages the + * horizontal scrolling. The horizontal AXScrollArea must include a child for each column + * the interface displays. Columns can be any role that makes sense. Typically, columns + * are vertical AXScrollAreas with AXList children. Here's a hierarchy for a typical + * browser: + * + *
+         * AXBrowser
+         * AXScrollArea (manages the horizontal scrolling)
+         * AXScrollBar (horizontal scroll bar)
+         * AXScrollArea (first column)
+         * AXScrollBar (column's vertical scroll bar)
+         * AXList (column content is typically a list, but it could be another role)
+         *  (cell)
+         * ...
+         *  (cell)
+         * AXScrollArea (second column)
+         * ...
+         * AXScrollArea (third column)
+         * ...
+         * AXGroup (preview column)
+         * ...
+         * 
+ * + * Attributes: + *
    + *
  • AXFocused (w)
  • + *
  • AXColumns - Array of the grandchild column elements, which are typically + * of the AXScrollArea role.
  • + *
  • AXVisibleColumns - Array of the subset of elements in the AXColumns array + * that are currently visible.
  • + *
  • AXColumnTitles (o)
  • + *
  • AXHorizontalScrollBar - The horizontal AXScrollBar of the browser's child + * AXScrollArea.
  • + *
+ */ + public static let browser = kAXBrowserRole + public static let scrollArea = kAXScrollAreaRole + public static let scrollBar = kAXScrollBarRole + public static let radioGroup = kAXRadioGroupRole + public static let list = kAXListRole + public static let group = kAXGroupRole + public static let valueIndicator = kAXValueIndicatorRole + public static let comboBox = kAXComboBoxRole + public static let slider = kAXSliderRole + public static let incrementor = kAXIncrementorRole + public static let busyIndicator = kAXBusyIndicatorRole + public static let progressIndicator = kAXProgressIndicatorRole + public static let relevanceIndicator = kAXRelevanceIndicatorRole + public static let toolbar = kAXToolbarRole + public static let disclosureTriangle = kAXDisclosureTriangleRole + public static let textField = kAXTextFieldRole + public static let textArea = kAXTextAreaRole + public static let staticText = kAXStaticTextRole + public static let heading = kAXHeadingRole + public static let menuBar = kAXMenuBarRole + public static let menuBarItem = kAXMenuBarItemRole + public static let menu = kAXMenuRole + public static let menuItem = kAXMenuItemRole + public static let splitGroup = kAXSplitGroupRole + public static let splitter = kAXSplitterRole + public static let colorWell = kAXColorWellRole + public static let timeField = kAXTimeFieldRole + public static let dateField = kAXDateFieldRole + public static let helpTag = kAXHelpTagRole + public static let matte = kAXMatteRole + public static let dockItem = kAXDockItemRole + public static let ruler = kAXRulerRole + public static let rulerMarker = kAXRulerMarkerRole + public static let grid = kAXGridRole + public static let levelIndicator = kAXLevelIndicatorRole + public static let cell = kAXCellRole + public static let layoutArea = kAXLayoutAreaRole + public static let layoutItem = kAXLayoutItemRole + public static let handle = kAXHandleRole + public static let popover = kAXPopoverRole + + // MARK: - Roles + + public static let imageMap = kAXImageMapRole + } +} diff --git a/Sources/AccessibilityControl/Accessibility+Subrole.swift b/Sources/AccessibilityControl/Accessibility+Subrole.swift new file mode 100644 index 0000000..336919a --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+Subrole.swift @@ -0,0 +1,118 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXRoleConstants.h, System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXWebConstants.h + +extension Accessibility { + public enum Subrole { + // MARK: - Standard subroles + + public static let closeButton = kAXCloseButtonSubrole + public static let minimizeButton = kAXMinimizeButtonSubrole + public static let zoomButton = kAXZoomButtonSubrole + public static let toolbarButton = kAXToolbarButtonSubrole + public static let fullScreenButton = kAXFullScreenButtonSubrole + public static let secureTextField = kAXSecureTextFieldSubrole + public static let tableRow = kAXTableRowSubrole + public static let outlineRow = kAXOutlineRowSubrole + public static let unknown = kAXUnknownSubrole + + // MARK: - New subroles + + public static let standardWindow = kAXStandardWindowSubrole + public static let dialog = kAXDialogSubrole + public static let systemDialog = kAXSystemDialogSubrole + public static let floatingWindow = kAXFloatingWindowSubrole + public static let systemFloatingWindow = kAXSystemFloatingWindowSubrole + public static let decorative = kAXDecorativeSubrole + public static let incrementArrow = kAXIncrementArrowSubrole + public static let decrementArrow = kAXDecrementArrowSubrole + public static let incrementPage = kAXIncrementPageSubrole + public static let decrementPage = kAXDecrementPageSubrole + public static let sortButton = kAXSortButtonSubrole + public static let searchField = kAXSearchFieldSubrole + public static let timeline = kAXTimelineSubrole + public static let ratingIndicator = kAXRatingIndicatorSubrole + public static let contentList = kAXContentListSubrole + /** + * superceded by kAXDescriptionListSubrole in OS X 10.9 + */ + public static let definitionList = kAXDefinitionListSubrole + /** + * OS X 10.9 and later + */ + public static let descriptionList = kAXDescriptionListSubrole + public static let toggle = kAXToggleSubrole + public static let `switch` = kAXSwitchSubrole + + // MARK: - Dock subroles + + public static let applicationDockItem = kAXApplicationDockItemSubrole + public static let documentDockItem = kAXDocumentDockItemSubrole + public static let folderDockItem = kAXFolderDockItemSubrole + public static let minimizedWindowDockItem = kAXMinimizedWindowDockItemSubrole + public static let urlDockItem = kAXURLDockItemSubrole + public static let dockExtraDockItem = kAXDockExtraDockItemSubrole + public static let trashDockItem = kAXTrashDockItemSubrole + public static let separatorDockItem = kAXSeparatorDockItemSubrole + public static let processSwitcherList = kAXProcessSwitcherListSubrole + + // MARK: - Subroles + + public static let applicationAlertDialog = kAXApplicationAlertDialogSubrole + public static let applicationAlert = kAXApplicationAlertSubrole + public static let applicationDialog = kAXApplicationDialogSubrole + public static let applicationGroup = kAXApplicationGroupSubrole + public static let applicationLog = kAXApplicationLogSubrole + public static let applicationMarquee = kAXApplicationMarqueeSubrole + public static let applicationStatus = kAXApplicationStatusSubrole + public static let applicationTimer = kAXApplicationTimerSubrole + public static let audio = kAXAudioSubrole + public static let codeStyleGroup = kAXCodeStyleGroupSubrole + public static let definition = kAXDefinitionSubrole + public static let deleteStyleGroup = kAXDeleteStyleGroupSubrole + public static let details = kAXDetailsSubrole + public static let documentArticle = kAXDocumentArticleSubrole + public static let documentMath = kAXDocumentMathSubrole + public static let documentNote = kAXDocumentNoteSubrole + public static let emptyGroup = kAXEmptyGroupSubrole + public static let fieldset = kAXFieldsetSubrole + public static let fileUploadButton = kAXFileUploadButtonSubrole + public static let insertStyleGroup = kAXInsertStyleGroupSubrole + public static let landmarkBanner = kAXLandmarkBannerSubrole + public static let landmarkComplementary = kAXLandmarkComplementarySubrole + public static let landmarkContentInfo = kAXLandmarkContentInfoSubrole + public static let landmarkMain = kAXLandmarkMainSubrole + public static let landmarkNavigation = kAXLandmarkNavigationSubrole + public static let landmarkRegion = kAXLandmarkRegionSubrole + public static let landmarkSearch = kAXLandmarkSearchSubrole + public static let mathFenceOperator = kAXMathFenceOperatorSubrole + public static let mathFenced = kAXMathFencedSubrole + public static let mathFraction = kAXMathFractionSubrole + public static let mathIdentifier = kAXMathIdentifierSubrole + public static let mathMultiscript = kAXMathMultiscriptSubrole + public static let mathNumber = kAXMathNumberSubrole + public static let mathOperator = kAXMathOperatorSubrole + public static let mathRoot = kAXMathRootSubrole + public static let mathRow = kAXMathRowSubrole + public static let mathSeparatorOperator = kAXMathSeparatorOperatorSubrole + public static let mathSquareRoot = kAXMathSquareRootSubrole + public static let mathSubscriptSuperscript = kAXMathSubscriptSuperscriptSubrole + public static let mathTableCell = kAXMathTableCellSubrole + public static let mathTableRow = kAXMathTableRowSubrole + public static let mathTable = kAXMathTableSubrole + public static let mathText = kAXMathTextSubrole + public static let mathUnderOver = kAXMathUnderOverSubrole + public static let meter = kAXMeterSubrole + public static let rubyInline = kAXRubyInlineSubrole + public static let rubyText = kAXRubyTextSubrole + public static let subscriptStyleGroup = kAXSubscriptStyleGroupSubrole + public static let summary = kAXSummarySubrole + public static let superscriptStyleGroup = kAXSuperscriptStyleGroupSubrole + public static let tabPanel = kAXTabPanelSubrole + public static let term = kAXTermSubrole + public static let timeGroup = kAXTimeGroupSubrole + public static let userInterfaceTooltip = kAXUserInterfaceTooltipSubrole + public static let video = kAXVideoSubrole + public static let webApplication = kAXWebApplicationSubrole + } +} diff --git a/Sources/AccessibilityControl/Accessibility+Value.swift b/Sources/AccessibilityControl/Accessibility+Value.swift new file mode 100644 index 0000000..ec6c932 --- /dev/null +++ b/Sources/AccessibilityControl/Accessibility+Value.swift @@ -0,0 +1,19 @@ +import ApplicationServices + +// Extracted from: System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Headers/AXValueConstants.h + +extension Accessibility { + public enum Value { + // MARK: - orientations (see kAXOrientationAttribute in AXAttributeConstants.h) + + public static let horizontalOrientation = kAXHorizontalOrientationValue + public static let verticalOrientation = kAXVerticalOrientationValue + public static let unknownOrientation = kAXUnknownOrientationValue + + // MARK: - sort directions (see kAXSortDirectionAttribute in AXAttributeConstants.h) + + public static let ascendingSortDirection = kAXAscendingSortDirectionValue + public static let descendingSortDirection = kAXDescendingSortDirectionValue + public static let unknownSortDirection = kAXUnknownSortDirectionValue + } +} diff --git a/Sources/AccessibilityControl/Accessibility.swift b/Sources/AccessibilityControl/Accessibility.swift index 3f3fd73..670a285 100644 --- a/Sources/AccessibilityControl/Accessibility.swift +++ b/Sources/AccessibilityControl/Accessibility.swift @@ -105,7 +105,9 @@ public enum Accessibility { public typealias MutableAttributeName = MutableAttribute.Name public typealias ParameterizedAttributeName = ParameterizedAttribute.Name - init() {} + init() { + + } } public static func isTrusted(shouldPrompt: Bool = false) -> Bool { diff --git a/Sources/AccessibilityControl/Element+Hierarchy.swift b/Sources/AccessibilityControl/Element+Hierarchy.swift new file mode 100644 index 0000000..f892178 --- /dev/null +++ b/Sources/AccessibilityControl/Element+Hierarchy.swift @@ -0,0 +1,252 @@ +import Foundation +import ApplicationServices + +// MARK: - Wire Keys + +/// Stable IPC protocol keys used by `AXUIElementCopyHierarchy`. +/// These are hardcoded wire values — no runtime resolution needed. +private enum HierarchyWireKey { + // Option keys + static let arrayAttributes = "AXCHAA" + static let skipInspection = "AXCHSIA" + static let maxArrayCount = "AXCHMAC" + static let maxDepth = "AXCHMD" + static let returnErrors = "AXCHRE" + static let truncateStrings = "AXTRUNC" + + // Result keys + static let value = "value" + static let count = "count" + static let error = "error" + static let incomplete = "incmplt" +} + +// MARK: - dlsym resolution + +private typealias AXUIElementCopyHierarchyFn = @convention(c) ( + AXUIElement, + CFArray, + CFTypeRef?, + UnsafeMutablePointer?>? +) -> AXError + +private let _copyHierarchy: AXUIElementCopyHierarchyFn? = { + guard let sym = dlsym(dlopen(nil, RTLD_NOW), "AXUIElementCopyHierarchy") else { + return nil + } + return unsafeBitCast(sym, to: AXUIElementCopyHierarchyFn.self) +}() + +// MARK: - HierarchyOptions + +extension Accessibility { + /// Options for `AXUIElementCopyHierarchy`. + public struct HierarchyOptions { + /// Extra array-valued attributes to traverse into (e.g. `kAXChildrenAttribute`). + public var arrayAttributes: [String]? + + /// Attributes to return values for but NOT recurse into. + public var skipInspectionAttributes: [String]? + + /// Cap per-attribute array expansion. + public var maxArrayCount: Int? + + /// Max traversal depth (effect varies by macOS version). + public var maxDepth: Int? + + /// Include error wrappers for failed attribute fetches. + public var returnAttributeErrors: Bool? + + /// Truncate string values to 512 characters. + public var truncateStrings: Bool? + + public init( + arrayAttributes: [String]? = nil, + skipInspectionAttributes: [String]? = nil, + maxArrayCount: Int? = nil, + maxDepth: Int? = nil, + returnAttributeErrors: Bool? = nil, + truncateStrings: Bool? = nil + ) { + self.arrayAttributes = arrayAttributes + self.skipInspectionAttributes = skipInspectionAttributes + self.maxArrayCount = maxArrayCount + self.maxDepth = maxDepth + self.returnAttributeErrors = returnAttributeErrors + self.truncateStrings = truncateStrings + } + + fileprivate func toCFDictionary() -> CFDictionary? { + var dict = [String: Any]() + + if let arrayAttributes { + dict[HierarchyWireKey.arrayAttributes] = arrayAttributes + } + if let skipInspectionAttributes { + dict[HierarchyWireKey.skipInspection] = skipInspectionAttributes + } + if let maxArrayCount { + dict[HierarchyWireKey.maxArrayCount] = maxArrayCount + } + if let maxDepth { + dict[HierarchyWireKey.maxDepth] = maxDepth + } + if let returnAttributeErrors { + dict[HierarchyWireKey.returnErrors] = returnAttributeErrors + } + if let truncateStrings { + dict[HierarchyWireKey.truncateStrings] = truncateStrings + } + + guard !dict.isEmpty else { return nil } + return dict as CFDictionary + } + } +} + +// MARK: - HierarchyResult + +extension Accessibility { + /// Result of `AXUIElementCopyHierarchy`, wrapping the raw output dictionary. + /// + /// The raw dictionary is keyed by `AXUIElementRef` with per-element attribute data as values. + public struct HierarchyResult { + /// The raw `NSDictionary` returned by the API. + public let raw: NSDictionary + + /// Number of element entries in the result. + public var elementCount: Int { raw.count } + + /// All `AXUIElement` keys as `Element`s. + public var elements: [Element] { + raw.allKeys.compactMap { key -> Element? in + guard CFGetTypeID(key as CFTypeRef) == AXUIElementGetTypeID() else { return nil } + return Element(raw: key as! AXUIElement) + } + } + + /// Look up one element's attribute data. + public func snapshot(for element: Element) -> ElementSnapshot? { + guard let perElement = raw[element.raw] as? NSDictionary else { return nil } + return ElementSnapshot(raw: perElement) + } + + /// Iterate all element snapshots. + public func allSnapshots() -> [(element: Element, snapshot: ElementSnapshot)] { + raw.compactMap { key, value -> (Element, ElementSnapshot)? in + guard CFGetTypeID(key as CFTypeRef) == AXUIElementGetTypeID(), + let perElement = value as? NSDictionary else { return nil } + return (Element(raw: key as! AXUIElement), ElementSnapshot(raw: perElement)) + } + } + + // MARK: - ElementSnapshot + + /// Per-element attribute data from a hierarchy result. + public struct ElementSnapshot { + fileprivate let raw: NSDictionary + + /// `true` for uninspectable sentinel entries (`{ "incmplt": true }`). + public var isIncomplete: Bool { + raw.count == 1 + && (raw[HierarchyWireKey.incomplete] as? Bool) == true + } + + /// Look up one attribute by name string. + public func entry(for attributeName: String) -> AttributeEntry? { + guard let wrapper = raw[attributeName] as? NSDictionary else { return nil } + return AttributeEntry(raw: wrapper) + } + + /// Look up one attribute by typed `Attribute.Name`. + public func entry(for name: Attribute.Name) -> AttributeEntry? { + entry(for: name.value) + } + + /// All attribute names present in this snapshot. + public var attributeNames: [String] { + raw.allKeys.compactMap { $0 as? String } + } + } + + // MARK: - AttributeEntry + + /// Per-attribute wrapper from a hierarchy result. + public struct AttributeEntry { + fileprivate let raw: NSDictionary + + /// The raw value for this attribute. + public var value: Any? { + raw[HierarchyWireKey.value] + } + + /// The value as an array, if it is one. + public var arrayValue: [Any]? { + raw[HierarchyWireKey.value] as? [Any] + } + + /// The value as a string, if it is one. + public var stringValue: String? { + raw[HierarchyWireKey.value] as? String + } + + /// The value as element references, if applicable. + public var elementValues: [Element]? { + guard let arr = raw[HierarchyWireKey.value] as? [AnyObject] else { return nil } + let elements = arr.compactMap { Element(erased: $0 as CFTypeRef) } + guard elements.count == arr.count else { return nil } + return elements + } + + /// True array count (may exceed `arrayValue.count` when capped by `maxArrayCount`). + public var count: Int? { + (raw[HierarchyWireKey.count] as? NSNumber)?.intValue + } + + /// Error code for this attribute (only present with `returnAttributeErrors`). + public var error: AXError? { + guard let num = raw[HierarchyWireKey.error] as? NSNumber else { return nil } + return AXError(rawValue: num.int32Value) + } + + /// Whether this entry represents an error. + public var isError: Bool { + raw[HierarchyWireKey.error] != nil + } + } + } +} + +// MARK: - Element.copyHierarchy + +extension Accessibility.Element { + /// Bulk-fetch the accessibility hierarchy in a single IPC round-trip. + /// + /// This calls the private `AXUIElementCopyHierarchy` API, which is dramatically faster + /// than recursively calling `AXUIElementCopyAttributeValue` per element. + /// + /// - Parameters: + /// - attributes: Attribute names to fetch for each element (e.g. `[kAXRoleAttribute, kAXChildrenAttribute]`). + /// - options: Optional configuration for traversal behavior. + /// - Returns: A `HierarchyResult` containing per-element attribute data. + /// - Throws: `AccessibilityError` if the call fails, or if the symbol is unavailable. + public func copyHierarchy( + requesting attributes: [String], + options: Accessibility.HierarchyOptions? = nil, + file: StaticString = #fileID, + line: UInt = #line + ) throws -> Accessibility.HierarchyResult { + guard let fn = _copyHierarchy else { + throw AccessibilityError(.failure, file: file, line: line) + } + var outRef: Unmanaged? + try Accessibility.check( + fn(raw, attributes as CFArray, options?.toCFDictionary(), &outRef), + file: file, line: line + ) + guard let result = outRef?.takeRetainedValue() as? NSDictionary else { + throw AccessibilityError(.failure, file: file, line: line) + } + return Accessibility.HierarchyResult(raw: result) + } +} diff --git a/Sources/AccessibilityControl/Element+Utilities.swift b/Sources/AccessibilityControl/Element+Utilities.swift new file mode 100644 index 0000000..984632b --- /dev/null +++ b/Sources/AccessibilityControl/Element+Utilities.swift @@ -0,0 +1,92 @@ +import CoreFoundation +import os.log + +private let log = OSLog(subsystem: "com.betterswiftax", category: "accessibility") + +public extension Accessibility.Element { + var isValid: Bool { + (try? pid()) != nil + } + + var isFrameValid: Bool { + (try? self.frame()) != nil + } + + var isInViewport: Bool { + (try? self.frame()) != CGRect.null + } + + // - breadth-first, seems faster than dfs + // - default max complexity to 1,800; if i dump the complexity of the Messages app right now i get ~360. x10 that, should be plenty + // - we can't turn `AXUIElement`s into e.g. `ObjectIdentifier`s and use that to track a set of seen elements and avoid cycles because + // the objects aren't pooled; any given instance of `AXUIElement` in memory is "transient" and another may take its place + func recursiveChildren(maxTraversalComplexity: Int = 3_600) -> AnySequence { + // incremented for every element with children that we discover; not "depth" since it's a running tally + var traversalComplexity = 0 + + return AnySequence(sequence(state: [self] as [Accessibility.Element]) { queue -> Accessibility.Element? in + guard traversalComplexity < maxTraversalComplexity else { + os_log(.error, log: log, "HIT RECURSIVE TRAVERSAL COMPLEXITY LIMIT (%d > %d, queue count: %d), terminating early", traversalComplexity, maxTraversalComplexity, queue.count) + return nil + } + + guard !queue.isEmpty else { + // queue is empty, we're done + return nil + } + + let elt = queue.removeFirst() + + if let children = try? elt.children() { + defer { traversalComplexity += 1 } + queue.append(contentsOf: children) + } + return elt + }) + } + + func recursiveSelectedChildren() -> AnySequence { + AnySequence(sequence(state: [self]) { queue -> Accessibility.Element? in + guard !queue.isEmpty else { return nil } + let elt = queue.removeFirst() + if let selectedChildren = try? elt.selectedChildren() { + queue.append(contentsOf: selectedChildren) + } + return elt + }) + } + + func recursivelyFindChild(withID id: String) -> Accessibility.Element? { + recursiveChildren().lazy.first { + (try? $0.identifier()) == id + } + } + + func setFrame(_ frame: CGRect) throws { + DispatchQueue.concurrentPerform(iterations: 2) { i in + switch i { + case 0: + try? self.position(assign: frame.origin) + case 1: + try? self.size(assign: frame.size) + default: + break + } + } + } + + func closeWindow() throws { + guard let closeButton = try? self.windowCloseButton() else { + throw AccessibilityError(.failure) + } + try closeButton.press() + } +} + +public extension Accessibility.Element { + func firstChild(withRole role: KeyPath) -> Accessibility.Element? { + try? self.children().first { child in + (try? child.role()) == Accessibility.Role.self[keyPath: role] + } + } +} diff --git a/Sources/AccessibilityControl/Element+XMLDump.swift b/Sources/AccessibilityControl/Element+XMLDump.swift new file mode 100644 index 0000000..35379b8 --- /dev/null +++ b/Sources/AccessibilityControl/Element+XMLDump.swift @@ -0,0 +1,251 @@ +import ApplicationServices +import Foundation + +private func printAttribute(to output: inout some TextOutputStream, name: String, value: Any?, omitTrailingSpace: Bool = false) { + guard let value else { return } + let valueEscaped = if let struc = Accessibility.Struct(erased: value as AnyObject) { + // this looks _much nicer_ compared to the default description + "\(struc)".xmlEscaped + } else { + "\(value)".xmlEscaped + } + print("\(name)=\(valueEscaped.quoted)", terminator: omitTrailingSpace ? "" : " ", to: &output) +} + +public struct XMLDumper { + public static let defaultExcludedAttributes: Set = [ + // children are printed specially, so we don't need to print them out as attributes + "AXChildren", "AXChildrenInNavigationOrder", + + // avoid redundantly printing parent elements, which can quickly bloat the result + "AXTopLevelUIElement", "AXMenuItemPrimaryUIElement", "AXParent", "AXWindow", + ] + + static var attributesLikelyToContainPII: Set { + [ + kAXHelpAttribute, + kAXDescriptionAttribute, + kAXTitleAttribute, + + // text entry area values + kAXValueAttribute, + + kAXLabelValueAttribute, + kAXSelectedTextAttribute, + kAXPlaceholderValueAttribute, + kAXFilenameAttribute, + kAXDocumentAttribute, + ] + } + + public var maxDepth: Int? = nil + public var indentation = " " + public var excludedRoles: Set = [] + public var excludedAttributes: Set = XMLDumper.defaultExcludedAttributes + public var intendingToExcludePII = false + public var includeActions = true + public var includeSections = true + public var shallow = false + + public init( + maxDepth: Int? = nil, + indentation: String = " ", + excludedRoles: Set = [], + excludedAttributes: Set = XMLDumper.defaultExcludedAttributes, + intendingToExcludePII: Bool = false, + includeActions: Bool = true, + includeSections: Bool = true, + shallow: Bool = false + ) { + self.maxDepth = maxDepth + self.indentation = indentation + self.excludedRoles = excludedRoles + self.excludedAttributes = excludedAttributes + self.intendingToExcludePII = intendingToExcludePII + self.includeActions = includeActions + self.includeSections = includeSections + self.shallow = shallow + } + + public func dump( + _ element: Accessibility.Element, + to output: inout some TextOutputStream, + depth: Int = 0, + indent: Int = 0, + shallow: Bool? = nil, + preamble: String? = nil, + ) throws { + let shallow = shallow ?? self.shallow + let whitespace = String(repeating: indentation, count: indent) + + if let maxDepth { + guard depth < maxDepth else { + print(whitespace + "⚠️ reached max depth (\(depth) >= \(maxDepth))".asComment, to: &output) + return + } + } + + let role: String + do { + role = try element.role() + } catch { + print(whitespace + "⚠️ couldn't obtain role, skipping \(element): \(String(describing: error))".asComment, to: &output) + return + } + + guard !excludedRoles.contains(role) else { + return + } + + let injectedComment = if let preamble { preamble.asComment + " " } else { "" } + print("\(whitespace)\(injectedComment)<\(role)", terminator: " ", to: &output) + + // keep track of attributes that contain ui elements (as opposed to string/rect/etc.), so we can emit them as child XML elements instead of XML attributes + var attributesPointingToElements = [String: [Accessibility.Element]]() + + if let supportedAttributes = try? element.supportedAttributes() { + let attributes = Dictionary( + supportedAttributes + .filter { !excludedAttributes.contains($0.name.value) } + .map { attribute in (attribute.name.value, try? attribute()) }, + uniquingKeysWith: { first, second in second }, + ) + + let lastNonNilIndex = Array(attributes.values).lastIndex(where: { $0 != nil }) + + for (index, (name, value)) in attributes.sorted(by: { $0.key < $1.key }).enumerated() { + if let array = value as? [Any] { + guard let first = array.first, CFGetTypeID(first as CFTypeRef) == AXUIElementGetTypeID() else { + continue + } + // attribute value is of an array of ui elements + attributesPointingToElements[name, default: []].append(contentsOf: array.map { + Accessibility.Element(raw: $0 as! AXUIElement) + }) + continue + } else if let element = Accessibility.Element(erased: value as CFTypeRef) { + // attribute value is of a singular ui element + attributesPointingToElements[name, default: []].append(element) + continue + } + + printAttribute(to: &output, name: name, value: value, omitTrailingSpace: index == lastNonNilIndex) + } + } + + // closing angle bracket of the opening tag + print("> \(String(describing: element.raw).asComment)", to: &output) + let whitespace2 = String(repeating: indentation, count: indent + 1) + + if !shallow { + for (attributeContainingElementsName, containedElements) in attributesPointingToElements.sorted(by: { $0.key < $1.key }) { + print(whitespace2 + "attribute containing elements".asComment + " <\(attributeContainingElementsName)>", to: &output) + for element in containedElements { + try dump(element, to: &output, depth: depth + 1, indent: indent + 2, shallow: true) + } + print("\(whitespace2)", to: &output) + } + } + + defer { print("\(whitespace)", to: &output) } + + if includeActions, let actions = try? element.supportedActions(), !actions.isEmpty { + for (index, action) in actions.sorted(by: { $0.name.value < $1.name.value }).enumerated() { + print(whitespace2 + "actions[\(index)]".asComment + " + // Target:0x0 + // Selector:(null)" + // + // (yes, including the newlines.) skip these if we are intending to exclude PII + if (intendingToExcludePII && actionName.hasPrefix("AX")) || !intendingToExcludePII { + printAttribute(to: &output, name: "name", value: actionName) + } + + if !excludedAttributes.contains(kAXDescriptionAttribute) { + printAttribute(to: &output, name: "description", value: action.description, omitTrailingSpace: true) + } + print(">", to: &output) + } + } + + guard !shallow else { + // used to avoid following cycles endlessly, since sections can point back up the tree etc. + print(whitespace2 + "...".asComment, to: &output) + return + } + + if includeSections, let sections = try? element.sections() as [[String: Any]] { + for (index, section) in sections.enumerated() { + print(whitespace2 + "[\(index)]".asComment + " ", to: &output) + if let element = Accessibility.Element(axRaw: object) { + try dump(element, to: &output, depth: depth + 1, indent: indent + 2) + } + print("\(whitespace2)", to: &output) + } else { + print(">", to: &output) + } + } + } + + guard let children = try? element.children() else { return } + for (index, child) in children.enumerated() { + try dump(child, to: &output, depth: depth + 1, indent: indent + 1, preamble: "children[\(index)]") + } + } +} + +public extension Accessibility.Element { + func dumpXML( + to output: inout some TextOutputStream, + shallow: Bool = false, + maxDepth: Int? = nil, + excludingPII: Bool = false, + excludingElementsWithRoles excludedRoles: Set = [], + excludingAttributes excludedAttributes: Set = XMLDumper.defaultExcludedAttributes, + includeActions: Bool = true, + includeSections: Bool = true, + ) throws { + var excludedAttributes = excludedAttributes + if excludingPII { + excludedAttributes.formUnion(XMLDumper.attributesLikelyToContainPII) + } + + return try XMLDumper( + maxDepth: maxDepth, + excludedRoles: excludedRoles, + excludedAttributes: excludedAttributes, + intendingToExcludePII: excludingPII, + includeActions: includeActions, + includeSections: includeSections, + shallow: shallow, + ).dump(self, to: &output) + } +} + +// MARK: - + +private extension String { + var xmlEscaped: String { + replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "\"", with: "'") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + } + + var quoted: String { + "\"\(self)\"" + } + + var asComment: String { + "" + } +} diff --git a/Sources/AccessibilityControl/Element.swift b/Sources/AccessibilityControl/Element.swift index 6d84220..72db2cb 100644 --- a/Sources/AccessibilityControl/Element.swift +++ b/Sources/AccessibilityControl/Element.swift @@ -125,3 +125,12 @@ extension Accessibility.Element { return .init(raw: id) } } + + +// MARK: - Conformances + +extension Accessibility.Element: Equatable { + public static func == (lhs: Accessibility.Element, rhs: Accessibility.Element) -> Bool { + CFEqual(lhs.raw, rhs.raw) + } +} diff --git a/Sources/AccessibilityControl/NSRunningApplication+Accessibility.swift b/Sources/AccessibilityControl/NSRunningApplication+Accessibility.swift new file mode 100644 index 0000000..295ba70 --- /dev/null +++ b/Sources/AccessibilityControl/NSRunningApplication+Accessibility.swift @@ -0,0 +1,13 @@ +import AppKit +import WindowControl +import Cocoa + +extension NSRunningApplication { + public var _accessibilityElement: Accessibility.Element { + .init(pid: self.processIdentifier) + } + + public var _accessibilityWindow: WindowControl.Window? { + try? self._accessibilityElement.window() + } +} diff --git a/Sources/AccessibilityControl/Names+Standard.swift b/Sources/AccessibilityControl/Names+Standard.swift new file mode 100644 index 0000000..3e47cf7 --- /dev/null +++ b/Sources/AccessibilityControl/Names+Standard.swift @@ -0,0 +1,79 @@ +import ApplicationServices + +// refer to AXAttributeConstants.h +// https://gist.github.com/p6p/24fbac5d12891fcfffa2b53761f4343e +// https://developer.apple.com/documentation/applicationservices/axattributeconstants_h/miscellaneous_defines +// https://github.com/tmandry/AXSwift/blob/main/Sources/Constants.swift +public extension Accessibility.Names { + var rows: AttributeName<[Accessibility.Element]> { .init(kAXRowsAttribute) } + var children: AttributeName<[Accessibility.Element]> { .init(kAXChildrenAttribute) } + var selectedChildren: AttributeName<[Accessibility.Element]> { .init(kAXSelectedChildrenAttribute) } + var linkedElements: AttributeName<[Accessibility.Element]> { .init(kAXLinkedUIElementsAttribute) } + // ["SectionObject": {pid=16768}, "SectionUniqueID": 11266711619528580561, "SectionDescription": Messages] + var sections: AttributeName<[[String: CFTypeRef]]> { "AXSections" } + var parent: AttributeName { .init(kAXParentAttribute) } + var valueDescription: AttributeName { .init(kAXValueDescriptionAttribute) } + + var isExpanded: AttributeName { .init(kAXExpandedAttribute) } + var isHidden: AttributeName { .init(kAXHiddenAttribute) } + + // this wont work without the com.apple.private.accessibility.inspection entitlement + // https://stackoverflow.com/questions/45590888/how-to-get-the-objective-c-class-name-corresponding-to-an-axuielement + var className: AttributeName { "AXClassName" } + + var value: MutableAttributeName { .init(kAXValueAttribute) } + var placeholderValue: AttributeName { .init(kAXPlaceholderValueAttribute) } + + var position: MutableAttributeName { .init(kAXPositionAttribute) } + var size: MutableAttributeName { .init(kAXSizeAttribute) } + var frame: AttributeName { "AXFrame" } + + var title: AttributeName { .init(kAXTitleAttribute) } + var titleUIElement: AttributeName { .init(kAXTitleUIElementAttribute) } + var localizedDescription: AttributeName { .init(kAXDescriptionAttribute) } + var identifier: AttributeName { .init(kAXIdentifierAttribute) } + var role: AttributeName { .init(kAXRoleAttribute) } + var subrole: AttributeName { .init(kAXSubroleAttribute) } + var roleDescription: AttributeName { .init(kAXRoleDescriptionAttribute) } + var help: AttributeName { .init(kAXHelpAttribute) } + + var noOfChars: AttributeName { .init(kAXNumberOfCharactersAttribute) } + + var isSelected: MutableAttributeName { .init(kAXSelectedAttribute) } + var isFocused: MutableAttributeName { .init(kAXFocusedAttribute) } + var isEnabled: AttributeName { .init(kAXEnabledAttribute) } + + var selectedRows: AttributeName<[AXUIElement]> { .init(kAXSelectedRowsAttribute) } + var selectedColumns: AttributeName<[AXUIElement]> { .init(kAXSelectedColumnsAttribute) } + var selectedCells: AttributeName<[AXUIElement]> { .init(kAXSelectedCellsAttribute) } + + // https://developer.apple.com/documentation/applicationservices/axactionconstants_h/miscellaneous_defines + var press: ActionName { .init(kAXPressAction) } + var showMenu: ActionName { .init(kAXShowMenuAction) } + var cancel: ActionName { .init(kAXCancelAction) } + var scrollToVisible: ActionName { "AXScrollToVisible" } + + var increment: ActionName { .init(kAXIncrementAction) } + +#if DEBUG + var decrement: ActionName { .init(kAXDecrementAction) } + + var minValue: AttributeName { .init(kAXMinValueAttribute) } + var maxValue: AttributeName { .init(kAXMaxValueAttribute) } +#endif + + // App-specific + var appWindows: AttributeName<[Accessibility.Element]> { .init(kAXWindowsAttribute) } + var appMainWindow: AttributeName { .init(kAXMainWindowAttribute) } + var appFocusedWindow: AttributeName { .init(kAXFocusedWindowAttribute) } + var focusedElement: AttributeName { .init(kAXFocusedUIElementAttribute) } + + // Window-specific + var windowIsMain: MutableAttributeName { .init(kAXMainAttribute) } + var windowIsModal: AttributeName { .init(kAXModalAttribute) } + var windowIsMinimized: MutableAttributeName { .init(kAXMinimizedAttribute) } + var windowIsFullScreen: MutableAttributeName { "AXFullScreen" } + var windowCloseButton: AttributeName { .init(kAXCloseButtonAttribute) } + var windowDefaultButton: AttributeName { .init(kAXDefaultButtonAttribute) } + var windowCancelButton: AttributeName { .init(kAXCancelButtonAttribute) } +} diff --git a/Sources/AccessibilityControl/Observer.swift b/Sources/AccessibilityControl/Observer.swift index 8c0a36e..90cf7df 100644 --- a/Sources/AccessibilityControl/Observer.swift +++ b/Sources/AccessibilityControl/Observer.swift @@ -1,35 +1,50 @@ -import Foundation +import AppKit import ApplicationServices +import Combine +import Foundation private func observerCallback( observer _: AXObserver, - element _: AXUIElement, + element: AXUIElement, notification _: CFString, info: CFDictionary, context: UnsafeMutableRawPointer? ) { guard let context = context else { return } + var dict = info as? [AnyHashable: Any] ?? [:] + // Include the element that triggered the notification + dict["AXUIElement"] = Accessibility.Element(raw: element) Unmanaged> .fromOpaque(context) .takeUnretainedValue() - .value(info as? [AnyHashable: Any] ?? [:]) + .value(dict) } extension Accessibility { public struct Notification: AccessibilityPhantomName { public let value: String + public init(_ value: String) { self.value = value } } public final class Observer { - public final class Token { - private let remove: () -> Void + public final class Token: Cancellable { + private var removeAction: (() -> Void)? + fileprivate init(remove: @escaping () -> Void) { - self.remove = remove + self.removeAction = remove + } + + public func cancel() { + self.removeAction?() + self.removeAction = nil + } + + deinit { + cancel() } - deinit { remove() } } public typealias Callback = (_ info: [AnyHashable: Any]) -> Void @@ -38,7 +53,7 @@ extension Accessibility { // no need to retain the entire observer so long as the individual // tokens are retained - public init(pid: pid_t, on runLoop: RunLoop = .current) throws { + public init(pid: pid_t, on runLoop: RunLoop = .main) throws { var raw: AXObserver? try check(AXObserverCreateWithInfoCallback(pid, observerCallback, &raw)) guard let raw = raw else { @@ -57,9 +72,21 @@ extension Accessibility { _ notification: Notification, for element: Element, callback: @escaping Callback + ) throws -> Token { + return try observe( + NSAccessibility.Notification(from: notification), + for: element, + callback: callback + ) + } + + public func observe( + _ notification: NSAccessibility.Notification, + for element: Element, + callback: @escaping Callback ) throws -> Token { let callback = Box(callback) - let cfNotif = notification.value as CFString + let cfNotif = notification.rawValue as CFString try check( AXObserverAddNotification( raw, @@ -79,16 +106,31 @@ extension Accessibility { } } -extension Accessibility.Element { +extension NSAccessibility.Notification { + public init(from accessibilityNotification: Accessibility.Notification) { + self.init(rawValue: accessibilityNotification.value) + } +} +extension Accessibility.Element { // the token must be retained public func observe( - _ notification: Accessibility.Notification, - on runLoop: RunLoop = .current, + _ notification: NSAccessibility.Notification, + on runLoop: RunLoop = .main, callback: @escaping Accessibility.Observer.Callback ) throws -> Accessibility.Observer.Token { try Accessibility.Observer(pid: pid(), on: runLoop) .observe(notification, for: self, callback: callback) } + public func publisher( + for notification: NSAccessibility.Notification, + on runLoop: RunLoop = .main, + callback: @escaping Accessibility.Observer.Callback + ) throws -> AnyCancellable { + let token = try Accessibility.Observer(pid: pid(), on: runLoop) + .observe(notification, for: self, callback: callback) + + return AnyCancellable(token) + } } diff --git a/Sources/WindowControl/Dock.swift b/Sources/WindowControl/Dock.swift index 505a8b0..97b8c6a 100644 --- a/Sources/WindowControl/Dock.swift +++ b/Sources/WindowControl/Dock.swift @@ -11,10 +11,10 @@ public enum Dock { static let bundleID = "com.apple.dock" public static var pid: pid_t? { - self.getApp()?.processIdentifier + self.runningApplication()?.processIdentifier } - public static func getApp() -> NSRunningApplication? { + public static func runningApplication() -> NSRunningApplication? { NSRunningApplication.runningApplications(withBundleIdentifier: Dock.bundleID).first } @@ -29,7 +29,7 @@ public enum Dock { private func onDockTerminate() { debugLog("dock terminated") try? retry(withTimeout: 5, interval: 0.1) { - guard Dock.getApp() != nil else { throw ErrorMessage("Dock not running") } // we wait for a max of 5s for dock to relaunch + guard Dock.runningApplication() != nil else { throw ErrorMessage("Dock not running") } // we wait for a max of 5s for dock to relaunch self.onExit() try? self.observe() } @@ -37,7 +37,7 @@ public enum Dock { private func observe() throws { try retry(withTimeout: 5, interval: 0.1) { - guard Dock.getApp() != nil, let pid = Dock.pid else { throw ErrorMessage("Dock not running") } + guard Dock.runningApplication() != nil, let pid = Dock.pid else { throw ErrorMessage("Dock not running") } debugLog("observing dock exit with pid=\(pid)") try Process.monitorExit(pid: pid, self.onDockTerminate) } diff --git a/Sources/WindowControl/Space.swift b/Sources/WindowControl/Space.swift index e38da8a..8e1ae3a 100644 --- a/Sources/WindowControl/Space.swift +++ b/Sources/WindowControl/Space.swift @@ -93,7 +93,7 @@ public class Space: Hashable { destroyWhenDone: Bool = true, display: Display = .main, connection: GraphicsConnection = .main - ) throws { + ) { isUnknownKind = kind == .unknown var values: [String: Any] = [ // "wsid": 1234 as CFNumber, // Compat ID, can be used with SLSMoveWorkspaceWindowList(conn, {windowID}, 1, wsid)