Convert AXspector to AXorcist Swift package with CLI tool

Removes Xcode project structure and implements Swift Package Manager
with accessibility inspection commands and test framework.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-05-22 02:46:39 +02:00
parent b3920f883d
commit 13d0e93369
45 changed files with 6854 additions and 780 deletions

View File

@ -1,556 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXContainerItemProxy section */
785C57082DDD38FF00BB9827 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 785C56F12DDD38FD00BB9827 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 785C56F82DDD38FD00BB9827;
remoteInfo = AXspector;
};
785C57122DDD38FF00BB9827 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 785C56F12DDD38FD00BB9827 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 785C56F82DDD38FD00BB9827;
remoteInfo = AXspector;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
785C56F92DDD38FD00BB9827 /* AXspector.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AXspector.app; sourceTree = BUILT_PRODUCTS_DIR; };
785C57072DDD38FF00BB9827 /* AXspectorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AXspectorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
785C57112DDD38FF00BB9827 /* AXspectorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AXspectorUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
785C56FB2DDD38FD00BB9827 /* AXspector */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AXspector;
sourceTree = "<group>";
};
785C570A2DDD38FF00BB9827 /* AXspectorTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AXspectorTests;
sourceTree = "<group>";
};
785C57142DDD38FF00BB9827 /* AXspectorUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AXspectorUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
785C56F62DDD38FD00BB9827 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
785C57042DDD38FF00BB9827 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
785C570E2DDD38FF00BB9827 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
785C56F02DDD38FD00BB9827 = {
isa = PBXGroup;
children = (
785C56FB2DDD38FD00BB9827 /* AXspector */,
785C570A2DDD38FF00BB9827 /* AXspectorTests */,
785C57142DDD38FF00BB9827 /* AXspectorUITests */,
785C56FA2DDD38FD00BB9827 /* Products */,
);
sourceTree = "<group>";
};
785C56FA2DDD38FD00BB9827 /* Products */ = {
isa = PBXGroup;
children = (
785C56F92DDD38FD00BB9827 /* AXspector.app */,
785C57072DDD38FF00BB9827 /* AXspectorTests.xctest */,
785C57112DDD38FF00BB9827 /* AXspectorUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
785C56F82DDD38FD00BB9827 /* AXspector */ = {
isa = PBXNativeTarget;
buildConfigurationList = 785C571B2DDD38FF00BB9827 /* Build configuration list for PBXNativeTarget "AXspector" */;
buildPhases = (
785C56F52DDD38FD00BB9827 /* Sources */,
785C56F62DDD38FD00BB9827 /* Frameworks */,
785C56F72DDD38FD00BB9827 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
785C56FB2DDD38FD00BB9827 /* AXspector */,
);
name = AXspector;
packageProductDependencies = (
);
productName = AXspector;
productReference = 785C56F92DDD38FD00BB9827 /* AXspector.app */;
productType = "com.apple.product-type.application";
};
785C57062DDD38FF00BB9827 /* AXspectorTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 785C571E2DDD38FF00BB9827 /* Build configuration list for PBXNativeTarget "AXspectorTests" */;
buildPhases = (
785C57032DDD38FF00BB9827 /* Sources */,
785C57042DDD38FF00BB9827 /* Frameworks */,
785C57052DDD38FF00BB9827 /* Resources */,
);
buildRules = (
);
dependencies = (
785C57092DDD38FF00BB9827 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
785C570A2DDD38FF00BB9827 /* AXspectorTests */,
);
name = AXspectorTests;
packageProductDependencies = (
);
productName = AXspectorTests;
productReference = 785C57072DDD38FF00BB9827 /* AXspectorTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
785C57102DDD38FF00BB9827 /* AXspectorUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 785C57212DDD38FF00BB9827 /* Build configuration list for PBXNativeTarget "AXspectorUITests" */;
buildPhases = (
785C570D2DDD38FF00BB9827 /* Sources */,
785C570E2DDD38FF00BB9827 /* Frameworks */,
785C570F2DDD38FF00BB9827 /* Resources */,
);
buildRules = (
);
dependencies = (
785C57132DDD38FF00BB9827 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
785C57142DDD38FF00BB9827 /* AXspectorUITests */,
);
name = AXspectorUITests;
packageProductDependencies = (
);
productName = AXspectorUITests;
productReference = 785C57112DDD38FF00BB9827 /* AXspectorUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
785C56F12DDD38FD00BB9827 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1640;
TargetAttributes = {
785C56F82DDD38FD00BB9827 = {
CreatedOnToolsVersion = 16.4;
};
785C57062DDD38FF00BB9827 = {
CreatedOnToolsVersion = 16.4;
TestTargetID = 785C56F82DDD38FD00BB9827;
};
785C57102DDD38FF00BB9827 = {
CreatedOnToolsVersion = 16.4;
TestTargetID = 785C56F82DDD38FD00BB9827;
};
};
};
buildConfigurationList = 785C56F42DDD38FD00BB9827 /* Build configuration list for PBXProject "AXspector" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 785C56F02DDD38FD00BB9827;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 785C56FA2DDD38FD00BB9827 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
785C56F82DDD38FD00BB9827 /* AXspector */,
785C57062DDD38FF00BB9827 /* AXspectorTests */,
785C57102DDD38FF00BB9827 /* AXspectorUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
785C56F72DDD38FD00BB9827 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
785C57052DDD38FF00BB9827 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
785C570F2DDD38FF00BB9827 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
785C56F52DDD38FD00BB9827 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
785C57032DDD38FF00BB9827 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
785C570D2DDD38FF00BB9827 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
785C57092DDD38FF00BB9827 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 785C56F82DDD38FD00BB9827 /* AXspector */;
targetProxy = 785C57082DDD38FF00BB9827 /* PBXContainerItemProxy */;
};
785C57132DDD38FF00BB9827 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 785C56F82DDD38FD00BB9827 /* AXspector */;
targetProxy = 785C57122DDD38FF00BB9827 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
785C57192DDD38FF00BB9827 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = Y5PE65HELJ;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 15.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
785C571A2DDD38FF00BB9827 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = Y5PE65HELJ;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 15.5;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
785C571C2DDD38FF00BB9827 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = AXspector/AXspector.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y5PE65HELJ;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.AXspector;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
785C571D2DDD38FF00BB9827 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = AXspector/AXspector.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y5PE65HELJ;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.AXspector;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
785C571F2DDD38FF00BB9827 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y5PE65HELJ;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.AXspectorTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AXspector.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AXspector";
};
name = Debug;
};
785C57202DDD38FF00BB9827 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y5PE65HELJ;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.AXspectorTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AXspector.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AXspector";
};
name = Release;
};
785C57222DDD38FF00BB9827 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y5PE65HELJ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.AXspectorUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = AXspector;
};
name = Debug;
};
785C57232DDD38FF00BB9827 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Y5PE65HELJ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = me.steipete.AXspectorUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = AXspector;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
785C56F42DDD38FD00BB9827 /* Build configuration list for PBXProject "AXspector" */ = {
isa = XCConfigurationList;
buildConfigurations = (
785C57192DDD38FF00BB9827 /* Debug */,
785C571A2DDD38FF00BB9827 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
785C571B2DDD38FF00BB9827 /* Build configuration list for PBXNativeTarget "AXspector" */ = {
isa = XCConfigurationList;
buildConfigurations = (
785C571C2DDD38FF00BB9827 /* Debug */,
785C571D2DDD38FF00BB9827 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
785C571E2DDD38FF00BB9827 /* Build configuration list for PBXNativeTarget "AXspectorTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
785C571F2DDD38FF00BB9827 /* Debug */,
785C57202DDD38FF00BB9827 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
785C57212DDD38FF00BB9827 /* Build configuration list for PBXNativeTarget "AXspectorUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
785C57222DDD38FF00BB9827 /* Debug */,
785C57232DDD38FF00BB9827 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 785C56F12DDD38FD00BB9827 /* Project object */;
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@ -1,17 +0,0 @@
//
// AXspectorApp.swift
// AXspector
//
// Created by Peter Steinberger on 21.05.25.
//
import SwiftUI
@main
struct AXspectorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

View File

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,58 +0,0 @@
{
"images" : [
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,24 +0,0 @@
//
// ContentView.swift
// AXspector
//
// Created by Peter Steinberger on 21.05.25.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}

View File

@ -1,17 +0,0 @@
//
// AXspectorTests.swift
// AXspectorTests
//
// Created by Peter Steinberger on 21.05.25.
//
import Testing
@testable import AXspector
struct AXspectorTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
}

View File

@ -1,41 +0,0 @@
//
// AXspectorUITests.swift
// AXspectorUITests
//
// Created by Peter Steinberger on 21.05.25.
//
import XCTest
final class AXspectorUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}

View File

@ -1,33 +0,0 @@
//
// AXspectorUITestsLaunchTests.swift
// AXspectorUITests
//
// Created by Peter Steinberger on 21.05.25.
//
import XCTest
final class AXspectorUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

39
Makefile Normal file
View File

@ -0,0 +1,39 @@
# Makefile for axorc helper
# Define the output binary name
BINARY_NAME = axorc
UNIVERSAL_BINARY_PATH = ./$(BINARY_NAME)
RELEASE_BUILD_DIR := ./.build/arm64-apple-macosx/release
RELEASE_BUILD_DIR_X86 := ./.build/x86_64-apple-macosx/release
# Build for arm64 and x86_64, then lipo them together
# -Xswiftc -Osize: Optimize for size
# -Xlinker -Wl,-dead_strip: Remove dead code
# strip -x: Strip symbol table and debug info
# Ensure old binary is removed first
all:
@echo "Cleaning old binary and build artifacts..."
rm -f $(UNIVERSAL_BINARY_PATH)
swift package clean
@echo "Building for arm64..."
swift build --arch arm64 -c release -Xswiftc -Osize -Xlinker -dead_strip
@echo "Building for x86_64..."
swift build --arch x86_64 -c release -Xswiftc -Osize -Xlinker -dead_strip
@echo "Creating universal binary..."
lipo -create -output $(UNIVERSAL_BINARY_PATH) $(RELEASE_BUILD_DIR)/$(BINARY_NAME) $(RELEASE_BUILD_DIR_X86)/$(BINARY_NAME)
@echo "Stripping symbols from universal binary..."
strip -x $(UNIVERSAL_BINARY_PATH)
@echo "Build complete: $(UNIVERSAL_BINARY_PATH)"
@ls -l $(UNIVERSAL_BINARY_PATH)
@codesign -s - $(UNIVERSAL_BINARY_PATH)
@echo "Codesigned $(UNIVERSAL_BINARY_PATH)"
clean:
@echo "Cleaning build artifacts..."
swift package clean
rm -f $(UNIVERSAL_BINARY_PATH)
@echo "Clean complete."
# Default target
.DEFAULT_GOAL := all

32
Package.resolved Normal file
View File

@ -0,0 +1,32 @@
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{
"identity" : "swift-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-testing.git",
"state" : {
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
"version" : "0.99.0"
}
}
],
"version" : 2
}

44
Package.swift Normal file
View File

@ -0,0 +1,44 @@
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "axPackage", // Renamed package slightly to avoid any confusion with executable name
platforms: [
.macOS(.v13) // macOS 13.0 or later
],
products: [
.library(name: "AXorcist", targets: ["AXorcist"]),
.executable(name: "axorc", targets: ["axorc"]) // Product 'axorc' comes from target 'axorc'
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), // Added swift-argument-parser
.package(url: "https://github.com/apple/swift-testing.git", from: "0.6.0") // Added swift-testing
],
targets: [
.target(
name: "AXorcist", // New library target name
path: "Sources/AXorcist" // Explicit path
// Sources will be inferred by SPM
),
.executableTarget(
name: "axorc", // Executable target name
dependencies: [
"AXorcist",
.product(name: "ArgumentParser", package: "swift-argument-parser") // Added dependency product
],
path: "Sources/axorc" // Explicit path
// Sources (axorc.swift) will be inferred by SPM
),
.testTarget(
name: "AXorcistTests",
dependencies: [
"AXorcist", // Test target depends on the library
.product(name: "Testing", package: "swift-testing") // Added swift-testing dependency
],
path: "Tests/AXorcistTests" // Explicit path
// Sources will be inferred by SPM
)
]
)

View File

@ -0,0 +1,960 @@
import Foundation
import ApplicationServices
import AppKit
// Placeholder for the actual accessibility logic.
// For now, this module is very thin and AXorcist.swift is the main public API.
// Other files like Element.swift, Models.swift, Search.swift, etc. are in Core/ Utils/ etc.
public struct HandlerResponse {
public var data: AXElement?
public var error: String?
public var debug_logs: [String]?
public init(data: AXElement? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.data = data
self.error = error
self.debug_logs = debug_logs
}
}
public class AXorcist {
private let focusedAppKeyValue = "focused"
private var recursiveCallDebugLogs: [String] = [] // Added for recursive logging
public init() {
// Future initialization logic can go here.
// For now, ensure debug logs can be collected if needed.
// Note: The actual logging enable/disable should be managed per-call.
// This init doesn't take global logging flags anymore.
}
// Placeholder for getting the focused element.
// It should accept debug logging parameters and update logs.
@MainActor
public func handleGetFocusedElement(
for appIdentifierOrNil: String? = nil,
requestedAttributes: [String]? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleGetFocusedElement] Handling for app: \(appIdentifier)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMsgText = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleGetFocusedElement] \(errorMsgText)")
return HandlerResponse(data: nil, error: errorMsgText, debug_logs: currentDebugLogs)
}
dLog("[AXorcist.handleGetFocusedElement] Successfully obtained application element for \(appIdentifier)")
var cfValue: CFTypeRef?
let copyAttributeStatus = AXUIElementCopyAttributeValue(appElement.underlyingElement, kAXFocusedUIElementAttribute as CFString, &cfValue)
guard copyAttributeStatus == .success, let rawAXElement = cfValue else {
dLog("[AXorcist.handleGetFocusedElement] Failed to copy focused element attribute or it was nil. Status: \(axErrorToString(copyAttributeStatus)). Application: \(appIdentifier)")
return HandlerResponse(data: nil, error: "Could not get the focused UI element for \(appIdentifier). Ensure a window of the application is focused. AXError: \(axErrorToString(copyAttributeStatus))", debug_logs: currentDebugLogs)
}
guard CFGetTypeID(rawAXElement) == AXUIElementGetTypeID() else {
dLog("[AXorcist.handleGetFocusedElement] Focused element attribute was not an AXUIElement. Application: \(appIdentifier)")
return HandlerResponse(data: nil, error: "Focused element was not a valid UI element for \(appIdentifier).", debug_logs: currentDebugLogs)
}
let focusedElement = Element(rawAXElement as! AXUIElement)
dLog("[AXorcist.handleGetFocusedElement] Successfully obtained focused element: \(focusedElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) for application \(appIdentifier)")
let fetchedAttributes = getElementAttributes(
focusedElement,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: nil,
outputFormat: .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
let elementPathArray = focusedElement.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: fetchedAttributes, path: elementPathArray)
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
}
// Handle getting attributes for a specific element using locator
@MainActor
public func handleGetAttributes(
for appIdentifierOrNil: String? = nil,
locator: Locator,
requestedAttributes: [String]? = nil,
pathHint: [String]? = nil,
maxDepth: Int? = nil,
outputFormat: OutputFormat? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleGetAttributes] Handling for app: \(appIdentifier)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleGetAttributes] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
// Find element to get attributes from
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
let pathHintString = pathHint.joined(separator: " -> ")
_ = pathHintString // Silences compiler warning
let logMessage = "[AXorcist.handleGetAttributes] Navigating with path_hint: \(pathHintString)"
dLog(logMessage)
if let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
effectiveElement = navigatedElement
} else {
let pathHintStringForError = pathHint.joined(separator: " -> ")
_ = pathHintStringForError // Silences compiler warning
let errorMessageText = "Element not found via path hint: \(pathHintStringForError)"
dLog("[AXorcist.handleGetAttributes] \(errorMessageText)")
return HandlerResponse(data: nil, error: errorMessageText, debug_logs: currentDebugLogs)
}
}
let rootElementDescription = effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = rootElementDescription // Silences compiler warning
let searchLogMessage = "[AXorcist.handleGetAttributes] Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)"
dLog(searchLogMessage)
let foundElement = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if let elementToQuery = foundElement {
let elementDescription = elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = elementDescription // Silences compiler warning
let attributesDescription = (requestedAttributes ?? ["all"]).description
_ = attributesDescription // Silences compiler warning
let foundElementLogMessage = "[AXorcist.handleGetAttributes] Element found: \(elementDescription). Fetching attributes: \(attributesDescription)..."
dLog(foundElementLogMessage)
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToQuery.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("[AXorcist.handleGetAttributes] Successfully fetched attributes for element \(elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)).")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
let errorMessage = "No element found for get_attributes with locator: \(String(describing: locator))"
dLog("[AXorcist.handleGetAttributes] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Handle query command - find an element matching criteria
@MainActor
public func handleQuery(
for appIdentifierOrNil: String? = nil,
locator: Locator,
pathHint: [String]? = nil,
maxDepth: Int? = nil,
requestedAttributes: [String]? = nil,
outputFormat: OutputFormat? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleQuery] Handling query for app: \(appIdentifier)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
let pathHintString = pathHint.joined(separator: " -> ")
_ = pathHintString // Silences compiler warning
dLog("[AXorcist.handleQuery] Navigating with path_hint: \(pathHintString)")
if let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint: \(pathHintString)"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Check if this is an app-only locator (only application/bundle_id/pid/path criteria)
let appSpecifiers = ["application", "bundle_id", "pid", "path"]
let criteriaKeys = locator.criteria.keys
let isAppOnlyLocator = criteriaKeys.allSatisfy { appSpecifiers.contains($0) } && criteriaKeys.count == 1
var foundElement: Element? = nil
if isAppOnlyLocator {
dLog("[AXorcist.handleQuery] Locator is app-only (criteria: \(locator.criteria)). Using appElement directly.")
foundElement = effectiveElement
} else {
dLog("[AXorcist.handleQuery] Locator contains element-specific criteria. Proceeding with search.")
var searchStartElementForLocator = appElement
if let rootPathHint = locator.root_element_path_hint, !rootPathHint.isEmpty {
let rootPathHintString = rootPathHint.joined(separator: " -> ")
_ = rootPathHintString // Silences compiler warning
dLog("[AXorcist.handleQuery] Locator has root_element_path_hint: \(rootPathHintString). Navigating from app element first.")
guard let containerElement = navigateToElement(from: appElement, pathHint: rootPathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Container for locator not found via root_element_path_hint: \(rootPathHintString)"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
searchStartElementForLocator = containerElement
let containerDescription = searchStartElementForLocator.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = containerDescription // Silences compiler warning
dLog("[AXorcist.handleQuery] Searching with locator within container found by root_element_path_hint: \(containerDescription)")
} else {
searchStartElementForLocator = effectiveElement
let searchDescription = searchStartElementForLocator.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = searchDescription // Silences compiler warning
dLog("[AXorcist.handleQuery] Searching with locator from element (determined by main path_hint or app root): \(searchDescription)")
}
let finalSearchTarget = (pathHint != nil && !pathHint!.isEmpty) ? effectiveElement : searchStartElementForLocator
foundElement = search(
element: finalSearchTarget,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
}
if let elementToQuery = foundElement {
let elementDescription = elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = elementDescription // Silences compiler warning
dLog("[AXorcist.handleQuery] Element found: \(elementDescription). Fetching attributes...")
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToQuery.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("[AXorcist.handleQuery] Successfully found and processed element with query.")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
let errorMessage = "No element matches query criteria with locator: \(String(describing: locator))"
dLog("[AXorcist.handleQuery] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Handle describe element command - provides comprehensive details about a specific element
@MainActor
public func handleDescribeElement(
for appIdentifierOrNil: String? = nil,
locator: Locator,
pathHint: [String]? = nil,
maxDepth: Int? = nil,
outputFormat: OutputFormat? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handleDescribeElement] Handling for app: \(appIdentifier)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
let pathHintString = pathHint.joined(separator: " -> ")
_ = pathHintString // Silences compiler warning
dLog("[AXorcist.handleDescribeElement] Navigating with path_hint: \(pathHintString)")
if let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint for describe_element: \(pathHintString)"
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
let rootElementDescription = effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = rootElementDescription // Silences compiler warning
dLog("[AXorcist.handleDescribeElement] Searching for element with locator: \(locator.criteria) from root: \(rootElementDescription)")
let foundElement = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if let elementToDescribe = foundElement {
let elementDescription = elementToDescribe.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
_ = elementDescription // Silences compiler warning
dLog("[AXorcist.handleDescribeElement] Element found: \(elementDescription). Describing with verbose output...")
// For describe_element, we typically want ALL attributes with verbose output
var attributes = getElementAttributes(
elementToDescribe,
requestedAttributes: [], // Empty means 'all standard' or 'all known'
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: .verbose, // Describe implies verbose
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
if outputFormat == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
let elementPathArray = elementToDescribe.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("[AXorcist.handleDescribeElement] Successfully described element \(elementToDescribe.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)).")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
} else {
let errorMessage = "No element found for describe_element with locator: \(String(describing: locator))"
dLog("[AXorcist.handleDescribeElement] \(errorMessage)")
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
}
// Add other public API methods here as they are refactored or created.
// For example:
// public func handlePerformAction(...) async -> HandlerResponse { ... }
@MainActor
public func handlePerformAction(
for appIdentifierOrNil: String? = nil,
locator: Locator,
pathHint: [String]? = nil,
actionName: String,
actionValue: AnyCodable?,
maxDepth: Int? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("[AXorcist.handlePerformAction] Handling for app: \(appIdentifier), action: \(actionName)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let error = "[AXorcist.handlePerformAction] Failed to get application element for identifier: \(appIdentifier)"
dLog(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("[AXorcist.handlePerformAction] Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
guard let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let error = "[AXorcist.handlePerformAction] Failed to navigate using path hint: \(pathHint.joined(separator: " -> "))"
dLog(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
effectiveElement = navigatedElement
}
dLog("[AXorcist.handlePerformAction] Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
guard let foundElement = search(element: effectiveElement, locator: locator, requireAction: locator.requireAction, maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let error = "[AXorcist.handlePerformAction] Failed to find element with locator: \(locator)"
dLog(error)
return HandlerResponse(data: nil, error: error, debug_logs: currentDebugLogs)
}
dLog("[AXorcist.handlePerformAction] Found element: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
if let actionValue = actionValue {
// Attempt to get a string representation of actionValue.value for logging
// This is a basic attempt; complex types might not log well.
let valueDescription = String(describing: actionValue.value)
dLog("[AXorcist.handlePerformAction] Performing action '\(actionName)' with value: \(valueDescription)")
} else {
dLog("[AXorcist.handlePerformAction] Performing action '\(actionName)'")
}
var errorMessage: String?
var axStatus: AXError = .success // Initialize to success
switch actionName.lowercased() {
case "press":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXPressAction as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform press action: \(axErrorToString(axStatus))"
}
case "increment":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXIncrementAction as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform increment action: \(axErrorToString(axStatus))"
}
case "decrement":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXDecrementAction as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform decrement action: \(axErrorToString(axStatus))"
}
case "showmenu":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXShowMenuAction as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform showmenu action: \(axErrorToString(axStatus))"
}
case "pick":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXPickAction as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform pick action: \(axErrorToString(axStatus))"
}
case "cancel":
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, kAXCancelAction as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform cancel action: \(axErrorToString(axStatus))"
}
default:
if actionName.hasPrefix("AX") {
axStatus = AXUIElementPerformAction(foundElement.underlyingElement, actionName as CFString)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to perform action '\(actionName)': \(axErrorToString(axStatus))"
}
} else {
if let actionValue = actionValue {
var cfValue: CFTypeRef?
// Convert basic Swift types to CFTypeRef for setting attributes
switch actionValue.value {
case let stringValue as String:
cfValue = stringValue as CFString
case let boolValue as Bool:
cfValue = boolValue as CFBoolean
case let intValue as Int:
var number = intValue
cfValue = CFNumberCreate(kCFAllocatorDefault, .intType, &number)
case let doubleValue as Double:
var number = doubleValue
cfValue = CFNumberCreate(kCFAllocatorDefault, .doubleType, &number)
// TODO: Consider other CFNumber types if necessary (CGFloat, etc.)
// TODO: Consider CFArray, CFDictionary if complex values are needed.
default:
// For other types, attempt a direct cast if possible, or log/error.
// This is a simplification; robust conversion is more involved.
if CFGetTypeID(actionValue.value as AnyObject) != 0 { // Basic check if it *might* be a CFType
cfValue = actionValue.value as AnyObject // bridge from Any to AnyObject then to CFTypeRef
dLog("[AXorcist.handlePerformAction] Warning: Attempting to use actionValue of type '\(type(of: actionValue.value))' directly as CFTypeRef for attribute '\(actionName)'. This might not work as expected.")
} else {
errorMessage = "[AXorcist.handlePerformAction] Unsupported value type '\(type(of: actionValue.value))' for attribute '\(actionName)'. Cannot convert to CFTypeRef."
dLog(errorMessage!)
}
}
if errorMessage == nil, let finalCFValue = cfValue {
axStatus = AXUIElementSetAttributeValue(foundElement.underlyingElement, actionName as CFString, finalCFValue)
if axStatus != .success {
errorMessage = "[AXorcist.handlePerformAction] Failed to set attribute '\(actionName)' to value '\(String(describing: actionValue.value))': \(axErrorToString(axStatus))"
}
} else if errorMessage == nil { // cfValue was nil, means conversion failed earlier but wasn't caught by the default error
errorMessage = "[AXorcist.handlePerformAction] Failed to convert value for attribute '\(actionName)' to a CoreFoundation type."
}
} else {
errorMessage = "[AXorcist.handlePerformAction] Unknown action '\(actionName)' and no action_value provided to interpret as an attribute."
}
}
}
if let currentErrorMessage = errorMessage {
dLog(currentErrorMessage)
return HandlerResponse(data: nil, error: currentErrorMessage, debug_logs: currentDebugLogs)
}
dLog("[AXorcist.handlePerformAction] Action '\(actionName)' performed successfully.")
return HandlerResponse(data: nil, error: nil, debug_logs: currentDebugLogs)
}
@MainActor
public func handleExtractText(
for appIdentifierOrNil: String? = nil,
locator: Locator,
pathHint: [String]? = nil,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> HandlerResponse {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append("[handleExtractText] \(message)")
}
}
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("Starting text extraction for app: \(appIdentifier)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Failed to get application element for \(appIdentifier)"
dLog(errorMessage)
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
var effectiveElement = appElement
if let pathHint = pathHint, !pathHint.isEmpty {
dLog("Navigating to element using path hint: \(pathHint.joined(separator: " -> "))")
guard let navigatedElement = navigateToElement(from: appElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Failed to navigate to element using path hint: \(pathHint.joined(separator: " -> "))"
dLog(errorMessage)
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
effectiveElement = navigatedElement
}
dLog("Searching for target element with locator: \(locator)")
// Assuming DEFAULT_MAX_DEPTH_SEARCH is defined elsewhere, e.g., in AXConstants.swift or similar.
// If not, replace with a sensible default like 10.
guard let foundElement = search(element: effectiveElement, locator: locator, requireAction: locator.requireAction, maxDepth: DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
let errorMessage = "Target element not found for locator: \(locator)"
dLog(errorMessage)
return HandlerResponse(data: nil, error: errorMessage, debug_logs: currentDebugLogs)
}
dLog("Target element found: \(foundElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)), attempting to extract text")
var attributes: [String: AnyCodable] = [:]
var extractedValueText: String?
var extractedSelectedText: String?
var cfValue: CFTypeRef?
if AXUIElementCopyAttributeValue(foundElement.underlyingElement, kAXValueAttribute as CFString, &cfValue) == .success, let value = cfValue {
if CFGetTypeID(value) == CFStringGetTypeID() {
extractedValueText = (value as! CFString) as String
if let extractedValueText = extractedValueText, !extractedValueText.isEmpty {
attributes["extractedValue"] = AnyCodable(extractedValueText)
dLog("Extracted text from kAXValueAttribute (length: \(extractedValueText.count)): \(extractedValueText.prefix(80))...")
} else {
dLog("kAXValueAttribute was empty or not a string.")
}
} else {
dLog("kAXValueAttribute was present but not a CFString. TypeID: \(CFGetTypeID(value))")
}
} else {
dLog("Failed to get kAXValueAttribute or it was nil.")
}
cfValue = nil // Reset for next attribute
if AXUIElementCopyAttributeValue(foundElement.underlyingElement, kAXSelectedTextAttribute as CFString, &cfValue) == .success, let selectedValue = cfValue {
if CFGetTypeID(selectedValue) == CFStringGetTypeID() {
extractedSelectedText = (selectedValue as! CFString) as String
if let extractedSelectedText = extractedSelectedText, !extractedSelectedText.isEmpty {
attributes["extractedSelectedText"] = AnyCodable(extractedSelectedText)
dLog("Extracted selected text from kAXSelectedTextAttribute (length: \(extractedSelectedText.count)): \(extractedSelectedText.prefix(80))...")
} else {
dLog("kAXSelectedTextAttribute was empty or not a string.")
}
} else {
dLog("kAXSelectedTextAttribute was present but not a CFString. TypeID: \(CFGetTypeID(selectedValue))")
}
} else {
dLog("Failed to get kAXSelectedTextAttribute or it was nil.")
}
if attributes.isEmpty {
dLog("Warning: No text could be extracted from the element via kAXValueAttribute or kAXSelectedTextAttribute.")
// It's not an error, just means no text content via these primary attributes.
// Other attributes might still be relevant, so we return the element.
}
let elementPathArray = foundElement.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
// Include any other relevant attributes if needed, for now just the extracted text
let axElement = AXElement(attributes: attributes, path: elementPathArray)
dLog("Text extraction process completed.")
return HandlerResponse(data: axElement, error: nil, debug_logs: currentDebugLogs)
}
@MainActor
public func handleBatchCommands(
batchCommandID: String, // The ID of the overall batch command
subCommands: [CommandEnvelope], // The array of sub-commands to process
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]
) -> [HandlerResponse] {
// Local debug logging function
func dLog(_ message: String, subCommandID: String? = nil) {
if isDebugLoggingEnabled {
let prefix = subCommandID != nil ? "[AXorcist.handleBatchCommands][SubCmdID: \(subCommandID!)]" : "[AXorcist.handleBatchCommands][BatchID: \(batchCommandID)]"
currentDebugLogs.append("\(prefix) \(message)")
}
}
dLog("Starting batch processing with \(subCommands.count) sub-commands.")
var batchResults: [HandlerResponse] = []
for subCommandEnvelope in subCommands {
let subCmdID = subCommandEnvelope.command_id
// Create a temporary log array for this specific sub-command to pass to handlers if needed,
// or decide if currentDebugLogs should be directly mutated by sub-handlers and reflect cumulative logs.
// For simplicity here, let's assume sub-handlers append to the main currentDebugLogs.
dLog("Processing sub-command: \(subCmdID), type: \(subCommandEnvelope.command)", subCommandID: subCmdID)
var subCommandResponse: HandlerResponse
switch subCommandEnvelope.command {
case .getFocusedElement:
subCommandResponse = self.handleGetFocusedElement(
for: subCommandEnvelope.application,
requestedAttributes: subCommandEnvelope.attributes,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass the main log array
)
case .getAttributes:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for getAttributes in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil) // Keep debug_logs nil for specific error, main logs will have the dLog entry
break
}
subCommandResponse = self.handleGetAttributes(
for: subCommandEnvelope.application,
locator: locator,
requestedAttributes: subCommandEnvelope.attributes,
pathHint: subCommandEnvelope.path_hint,
maxDepth: subCommandEnvelope.max_elements,
outputFormat: subCommandEnvelope.output_format,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .query:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for query in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handleQuery(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,
maxDepth: subCommandEnvelope.max_elements,
requestedAttributes: subCommandEnvelope.attributes,
outputFormat: subCommandEnvelope.output_format,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .describeElement:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for describeElement in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handleDescribeElement(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,
maxDepth: subCommandEnvelope.max_elements,
outputFormat: subCommandEnvelope.output_format,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .performAction:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for performAction in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
guard let actionName = subCommandEnvelope.action_name else {
let errorMsg = "Action name missing for performAction in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handlePerformAction(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,
actionName: actionName,
actionValue: subCommandEnvelope.action_value,
maxDepth: subCommandEnvelope.max_elements, // Added maxDepth, though performAction doesn't currently use it directly, for consistency
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .extractText:
guard let locator = subCommandEnvelope.locator else {
let errorMsg = "Locator missing for extractText in batch (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
break
}
subCommandResponse = self.handleExtractText(
for: subCommandEnvelope.application,
locator: locator,
pathHint: subCommandEnvelope.path_hint,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs
)
case .ping:
let pingMsg = "Ping command handled within batch (sub-command ID: \(subCmdID))"
dLog(pingMsg, subCommandID: subCmdID)
// For ping, the handlerResponse itself won't carry much data from AXorcist,
// but it should indicate success and carry the logs up to this point for this sub-command.
subCommandResponse = HandlerResponse(data: nil, error: nil, debug_logs: isDebugLoggingEnabled ? currentDebugLogs : nil)
// .batch command cannot be nested. .collectAll is also not handled by AXorcist lib directly.
case .collectAll, .batch:
let errorMsg = "Command type '\(subCommandEnvelope.command)' not supported within batch execution by AXorcist (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
// default case for any command types that might be added to CommandType enum
// but not handled by this switch statement within handleBatchCommands.
// This is distinct from commands axorc itself might handle outside of AXorcist library.
// @unknown default: // This would be better if Swift enums allowed it easily here for non-frozen enums from other modules.
// Since CommandType is in axorc, this default captures any CommandType case not explicitly handled above.
default:
let errorMsg = "Unknown or unhandled command type '\(subCommandEnvelope.command)' in batch processing within AXorcist (sub-command ID: \(subCmdID))"
dLog(errorMsg, subCommandID: subCmdID)
subCommandResponse = HandlerResponse(data: nil, error: errorMsg, debug_logs: nil)
}
batchResults.append(subCommandResponse)
}
dLog("Completed batch command processing, returning \(batchResults.count) results.")
return batchResults
}
@MainActor
public func handleCollectAll(
for appIdentifierOrNil: String?,
locator: Locator?,
pathHint: [String]?,
maxDepth: Int?,
requestedAttributes: [String]?,
outputFormat: OutputFormat?,
isDebugLoggingEnabled: Bool,
currentDebugLogs: [String] // No longer inout, logs from caller
) -> HandlerResponse {
self.recursiveCallDebugLogs.removeAll()
self.recursiveCallDebugLogs.append(contentsOf: currentDebugLogs) // Incorporate initial logs
// Local dLog now appends to self.recursiveCallDebugLogs
func dLog(_ message: String) {
if isDebugLoggingEnabled {
let logMessage = "[AXorcist.handleCollectAll] \(message)"
self.recursiveCallDebugLogs.append(logMessage)
}
}
dLog("Starting handleCollectAll")
let appIdentifier = appIdentifierOrNil ?? focusedAppKeyValue
dLog("Using app identifier: \(appIdentifier)")
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs) else {
let errorMsg = "Failed to get app element for identifier: \(appIdentifier)"
dLog(errorMsg)
// Return all accumulated logs up to this point
return HandlerResponse(data: nil, error: errorMsg, debug_logs: self.recursiveCallDebugLogs)
}
var startElement: Element
if let hint = pathHint, !hint.isEmpty {
dLog("Navigating to path hint: \(hint.joined(separator: " -> "))")
guard let navigatedElement = navigateToElement(from: appElement, pathHint: hint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs) else {
let errorMsg = "Failed to navigate to path: \(hint.joined(separator: " -> "))"
dLog(errorMsg)
return HandlerResponse(data: nil, error: errorMsg, debug_logs: self.recursiveCallDebugLogs)
}
startElement = navigatedElement
} else {
dLog("Using app element as start element")
startElement = appElement
}
var collectedAXElements: [AXElement] = []
let effectiveMaxDepth = maxDepth ?? 8
dLog("Max collection depth: \(effectiveMaxDepth)")
var collectRecursively: ((AXUIElement, Int) -> Void)!
collectRecursively = { axUIElement, currentDepth in
if currentDepth > effectiveMaxDepth {
// Pass &self.recursiveCallDebugLogs to briefDescription
dLog("Reached max depth \(effectiveMaxDepth) at element \(Element(axUIElement).briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)), stopping recursion for this branch.")
return
}
let currentElement = Element(axUIElement)
var shouldIncludeElement = true // Default to include if no locator
if let loc = locator {
let matchStatus = evaluateElementAgainstCriteria(
element: currentElement,
locator: loc,
actionToVerify: loc.requireAction, // Pass requireAction from locator
depth: currentDepth, // Pass currentDepth
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &self.recursiveCallDebugLogs
)
if matchStatus != .fullMatch {
shouldIncludeElement = false
// Log if not a full match, but still recurse for children
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth) did not fully match locator (status: \(matchStatus)), not collecting it.")
}
}
if shouldIncludeElement {
dLog("Collecting element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)")
let fetchedAttrs = getElementAttributes(
currentElement,
requestedAttributes: requestedAttributes ?? [],
forMultiDefault: true,
targetRole: nil as String?,
outputFormat: outputFormat ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &self.recursiveCallDebugLogs // Pass self.recursiveCallDebugLogs
)
let elementPath = currentElement.generatePathArray(
upTo: appElement,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &self.recursiveCallDebugLogs // Pass self.recursiveCallDebugLogs
)
let axElement = AXElement(attributes: fetchedAttrs, path: elementPath)
collectedAXElements.append(axElement)
} else if locator != nil {
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) did not match locator. Still checking children.")
}
var childrenRef: CFTypeRef?
let childrenResult = AXUIElementCopyAttributeValue(axUIElement, kAXChildrenAttribute as CFString, &childrenRef)
if childrenResult == .success, let children = childrenRef as? [AXUIElement] {
dLog("Element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) has \(children.count) children at depth \(currentDepth). Recursing.")
for childElement in children {
collectRecursively(childElement, currentDepth + 1)
}
} else if childrenResult != .success {
dLog("Failed to get children for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)): \(axErrorToString(childrenResult))")
} else {
dLog("No children found for element \(currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)) at depth \(currentDepth)")
}
}
dLog("Starting recursive collection from start element: \(startElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs))")
collectRecursively(startElement.underlyingElement, 0)
dLog("Collection complete. Found \(collectedAXElements.count) elements matching criteria (if any). Naming them 'collected_elements' in response.")
let responseDataElement = AXElement(
attributes: ["collected_elements": AnyCodable(collectedAXElements)],
path: startElement.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &self.recursiveCallDebugLogs)
)
return HandlerResponse(data: responseDataElement, error: nil, debug_logs: self.recursiveCallDebugLogs)
}
internal func getElementAttributes(for axElement: AXUIElement, requestedAttributes: [String]?, outputFormat: OutputFormat, pathHint: [String]?, currentDebugLogs: inout [String]) -> (attributes: ElementAttributes, collectedAttrData: [String: AttributeData]) {
var attributesToReturn: ElementAttributes = [:]
var collectedAttributesForComputedName: [String: AttributeData] = [:] // Still collect for computed name
dLog("getElementAttributes starting for element: \(axElement.descriptionForLogs), format: \(outputFormat.rawValue)", &currentDebugLogs)
// Temporarily, only process ComputedName
// First, we need to fetch attributes that might be used by generateComputedName, e.g., AXRole, AXTitle, AXValue, etc.
// This part remains similar to ensure `collectedAttributesForComputedName` is populated.
let attributesNeededForComputedName = [kAXRoleAttribute, kAXTitleAttribute, kAXValueAttribute, kAXDescriptionAttribute, kAXPlaceholderValueAttribute, "AXIdentifier", "AXLabel"]
for attrNameCFS in attributesNeededForComputedName {
let attrName = attrNameCFS as String
let (value, source) = getAttributeValueAndSource(for: axElement, attributeName: attrName, outputFormat: .smart) // Use .smart to get raw values
collectedAttributesForComputedName[attrName] = AttributeData(value: AnyCodable(value), source: source)
}
if let computedNameString = generateComputedName(for: axElement, from: collectedAttributesForComputedName) {
attributesToReturn["ComputedName"] = AnyCodable(computedNameString)
dLog("Added ComputedName: \(computedNameString)", &currentDebugLogs)
}
// Skip all other attribute processing for this debug step
dLog("TEMPORARY DEBUG: Skipped all other attribute processing.", &currentDebugLogs)
// Still need to populate AXActionNames for consistency if other parts of axorc rely on it
// but ensure it uses only encodable values.
var actionNames: [String] = []
// Simplified action fetching or make it empty for now to avoid encoding issues from it
// Example: actionNames.append("debug_action")
attributesToReturn[kAXActionNamesAttribute as String] = AnyCodable(actionNames) // Ensure this is an array of strings
dLog("getElementAttributes finished. Result keys: \(attributesToReturn.keys.joined(separator: \", \"))", &currentDebugLogs)
return (attributesToReturn, collectedAttributesForComputedName) // Return the possibly minimal attributes
}
// Helper to get an attribute's value and its source (e.g., direct, placeholder, computed)
// ... existing code ...
}

View File

@ -0,0 +1,71 @@
import Foundation
import ApplicationServices
import AppKit
// Placeholder for GetAttributesCommand if it were a distinct struct
// public struct GetAttributesCommand: Codable { ... }
@MainActor
public func handleGetAttributes(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) throws -> QueryResponse {
var handlerLogs: [String] = [] // Local logs for this handler
func dLog(_ message: String) { if isDebugLoggingEnabled { handlerLogs.append(message) } }
dLog("Handling get_attributes command for app: \(cmd.application ?? "focused app")")
let appIdentifier = cmd.application ?? focusedApplicationKey
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs) else {
let errorMessage = "Application not found: \(appIdentifier)"
dLog("handleGetAttributes: \(errorMessage)")
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: errorMessage, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
// Find element to get attributes from
var effectiveElement = appElement
if let pathHint = cmd.path_hint, !pathHint.isEmpty {
dLog("handleGetAttributes: Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
if let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs) {
effectiveElement = navigatedElement
} else {
let errorMessage = "Element not found via path hint: \(pathHint.joined(separator: " -> "))"
dLog("handleGetAttributes: \(errorMessage)")
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: errorMessage, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}
guard let locator = cmd.locator else {
let errorMessage = "Locator not provided for get_attributes."
dLog("handleGetAttributes: \(errorMessage)")
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: errorMessage, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
dLog("handleGetAttributes: Searching for element with locator: \(locator.criteria) from root: \(effectiveElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs))")
let foundElement = search(
element: effectiveElement,
locator: locator,
requireAction: locator.requireAction,
maxDepth: cmd.max_elements ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &handlerLogs
)
if let elementToQuery = foundElement {
dLog("handleGetAttributes: Element found: \(elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs)). Fetching attributes: \(cmd.attributes ?? ["all"])...")
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: cmd.attributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: cmd.output_format ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &handlerLogs
)
if cmd.output_format == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
dLog("Successfully fetched attributes for element \(elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs)).")
return QueryResponse(command_id: cmd.command_id, attributes: attributes, error: nil, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
} else {
let errorMessage = "No element found for get_attributes with locator: \(String(describing: locator))"
dLog("handleGetAttributes: \(errorMessage)")
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: errorMessage, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}

View File

@ -0,0 +1,92 @@
import Foundation
import ApplicationServices
import AppKit
// Note: Relies on applicationElement, navigateToElement, search, getElementAttributes,
// DEFAULT_MAX_DEPTH_SEARCH, CommandEnvelope, QueryResponse, Locator.
@MainActor
public func handleQuery(cmd: CommandEnvelope, isDebugLoggingEnabled: Bool) async throws -> QueryResponse {
var handlerLogs: [String] = [] // Local logs for this handler
func dLog(_ message: String) { if isDebugLoggingEnabled { handlerLogs.append(message) } }
let appIdentifier = cmd.application ?? focusedApplicationKey
dLog("Handling query for app: \(appIdentifier)")
// Pass logging parameters to applicationElement
guard let appElement = applicationElement(for: appIdentifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs) else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "Application not found: \(appIdentifier)", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
var effectiveElement = appElement
if let pathHint = cmd.path_hint, !pathHint.isEmpty {
dLog("Navigating with path_hint: \(pathHint.joined(separator: " -> "))")
// Pass logging parameters to navigateToElement
if let navigatedElement = navigateToElement(from: effectiveElement, pathHint: pathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs) {
effectiveElement = navigatedElement
} else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "Element not found via path hint: \(pathHint.joined(separator: " -> "))", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}
guard let locator = cmd.locator else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "Locator not provided in command.", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
let appSpecifiers = ["application", "bundle_id", "pid", "path"]
let criteriaKeys = locator.criteria.keys
let isAppOnlyLocator = criteriaKeys.allSatisfy { appSpecifiers.contains($0) } && criteriaKeys.count == 1
var foundElement: Element? = nil
if isAppOnlyLocator {
dLog("Locator is app-only (criteria: \(locator.criteria)). Using appElement directly.")
foundElement = effectiveElement
} else {
dLog("Locator contains element-specific criteria or is complex. Proceeding with search.")
var searchStartElementForLocator = appElement
if let rootPathHint = locator.root_element_path_hint, !rootPathHint.isEmpty {
dLog("Locator has root_element_path_hint: \(rootPathHint.joined(separator: " -> ")). Navigating from app element first.")
// Pass logging parameters to navigateToElement
guard let containerElement = navigateToElement(from: appElement, pathHint: rootPathHint, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs) else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "Container for locator not found via root_element_path_hint: \(rootPathHint.joined(separator: " -> "))", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
searchStartElementForLocator = containerElement
dLog("Searching with locator within container found by root_element_path_hint: \(searchStartElementForLocator.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs))")
} else {
searchStartElementForLocator = effectiveElement
dLog("Searching with locator from element (determined by main path_hint or app root): \(searchStartElementForLocator.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &handlerLogs))")
}
let finalSearchTarget = (cmd.path_hint != nil && !cmd.path_hint!.isEmpty) ? effectiveElement : searchStartElementForLocator
// Pass logging parameters to search
foundElement = search(
element: finalSearchTarget,
locator: locator,
requireAction: locator.requireAction,
maxDepth: cmd.max_elements ?? DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &handlerLogs
)
}
if let elementToQuery = foundElement {
// Pass logging parameters to getElementAttributes
var attributes = getElementAttributes(
elementToQuery,
requestedAttributes: cmd.attributes ?? [],
forMultiDefault: false,
targetRole: locator.criteria[kAXRoleAttribute],
outputFormat: cmd.output_format ?? .smart,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &handlerLogs
)
if cmd.output_format == .json_string {
attributes = encodeAttributesToJSONStringRepresentation(attributes)
}
return QueryResponse(command_id: cmd.command_id, attributes: attributes, error: nil, debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
} else {
return QueryResponse(command_id: cmd.command_id, attributes: nil, error: "No element matches single query criteria with locator or app-only locator failed to resolve.", debug_logs: isDebugLoggingEnabled ? handlerLogs : nil)
}
}

View File

@ -0,0 +1,201 @@
// AccessibilityConstants.swift - Defines global constants used throughout the accessibility helper
import Foundation
import ApplicationServices // Added for AXError type
import AppKit // Added for NSAccessibility
// Configuration Constants
public let MAX_COLLECT_ALL_HITS = 200 // Default max elements for collect_all if not specified in command
public let DEFAULT_MAX_DEPTH_SEARCH = 20 // Default max recursion depth for search
public let DEFAULT_MAX_DEPTH_COLLECT_ALL = 15 // Default max recursion depth for collect_all
public let AX_BINARY_VERSION = "1.1.7" // Updated version
public let BINARY_VERSION = "1.1.7" // Updated version without AX prefix
// Standard Accessibility Attributes - Values should match CFSTR defined in AXAttributeConstants.h
public let kAXRoleAttribute = "AXRole" // Reverted to String literal
public let kAXSubroleAttribute = "AXSubrole"
public let kAXRoleDescriptionAttribute = "AXRoleDescription"
public let kAXTitleAttribute = "AXTitle"
public let kAXValueAttribute = "AXValue"
public let kAXValueDescriptionAttribute = "AXValueDescription" // New
public let kAXDescriptionAttribute = "AXDescription"
public let kAXHelpAttribute = "AXHelp"
public let kAXIdentifierAttribute = "AXIdentifier"
public let kAXPlaceholderValueAttribute = "AXPlaceholderValue"
public let kAXLabelUIElementAttribute = "AXLabelUIElement"
public let kAXTitleUIElementAttribute = "AXTitleUIElement"
public let kAXLabelValueAttribute = "AXLabelValue"
public let kAXElementBusyAttribute = "AXElementBusy" // New
public let kAXAlternateUIVisibleAttribute = "AXAlternateUIVisible" // New
public let kAXChildrenAttribute = "AXChildren"
public let kAXParentAttribute = "AXParent"
public let kAXWindowsAttribute = "AXWindows"
public let kAXMainWindowAttribute = "AXMainWindow"
public let kAXFocusedWindowAttribute = "AXFocusedWindow"
public let kAXFocusedUIElementAttribute = "AXFocusedUIElement"
public let kAXEnabledAttribute = "AXEnabled"
public let kAXFocusedAttribute = "AXFocused"
public let kAXMainAttribute = "AXMain" // Window-specific
public let kAXMinimizedAttribute = "AXMinimized" // New, Window-specific
public let kAXCloseButtonAttribute = "AXCloseButton" // New, Window-specific
public let kAXZoomButtonAttribute = "AXZoomButton" // New, Window-specific
public let kAXMinimizeButtonAttribute = "AXMinimizeButton" // New, Window-specific
public let kAXFullScreenButtonAttribute = "AXFullScreenButton" // New, Window-specific
public let kAXDefaultButtonAttribute = "AXDefaultButton" // New, Window-specific
public let kAXCancelButtonAttribute = "AXCancelButton" // New, Window-specific
public let kAXGrowAreaAttribute = "AXGrowArea" // New, Window-specific
public let kAXModalAttribute = "AXModal" // New, Window-specific
public let kAXMenuBarAttribute = "AXMenuBar" // New, App-specific
public let kAXFrontmostAttribute = "AXFrontmost" // New, App-specific
public let kAXHiddenAttribute = "AXHidden" // New, App-specific
public let kAXPositionAttribute = "AXPosition"
public let kAXSizeAttribute = "AXSize"
// Value attributes
public let kAXMinValueAttribute = "AXMinValue" // New
public let kAXMaxValueAttribute = "AXMaxValue" // New
public let kAXValueIncrementAttribute = "AXValueIncrement" // New
public let kAXAllowedValuesAttribute = "AXAllowedValues" // New
// Text-specific attributes
public let kAXSelectedTextAttribute = "AXSelectedText" // New
public let kAXSelectedTextRangeAttribute = "AXSelectedTextRange" // New
public let kAXNumberOfCharactersAttribute = "AXNumberOfCharacters" // New
public let kAXVisibleCharacterRangeAttribute = "AXVisibleCharacterRange" // New
public let kAXInsertionPointLineNumberAttribute = "AXInsertionPointLineNumber" // New
// Actions - Values should match CFSTR defined in AXActionConstants.h
public let kAXActionsAttribute = "AXActions" // This is actually kAXActionNamesAttribute typically
public let kAXActionNamesAttribute = "AXActionNames" // Correct name for listing actions
public let kAXActionDescriptionAttribute = "AXActionDescription" // To get desc of an action (not in AXActionConstants.h but AXUIElement.h)
public let kAXIncrementAction = "AXIncrement" // New
public let kAXDecrementAction = "AXDecrement" // New
public let kAXConfirmAction = "AXConfirm" // New
public let kAXCancelAction = "AXCancel" // New
public let kAXShowMenuAction = "AXShowMenu"
public let kAXPickAction = "AXPick" // New (Obsolete in headers, but sometimes seen)
public let kAXPressAction = "AXPress" // New
// Specific action name for setting a value, used internally by performActionOnElement
public let kAXSetValueAction = "AXSetValue"
// Standard Accessibility Roles - Values should match CFSTR defined in AXRoleConstants.h (examples, add more as needed)
public let kAXApplicationRole = "AXApplication"
public let kAXSystemWideRole = "AXSystemWide" // New
public let kAXWindowRole = "AXWindow"
public let kAXSheetRole = "AXSheet" // New
public let kAXDrawerRole = "AXDrawer" // New
public let kAXGroupRole = "AXGroup"
public let kAXButtonRole = "AXButton"
public let kAXRadioButtonRole = "AXRadioButton" // New
public let kAXCheckBoxRole = "AXCheckBox"
public let kAXPopUpButtonRole = "AXPopUpButton" // New
public let kAXMenuButtonRole = "AXMenuButton" // New
public let kAXStaticTextRole = "AXStaticText"
public let kAXTextFieldRole = "AXTextField"
public let kAXTextAreaRole = "AXTextArea"
public let kAXScrollAreaRole = "AXScrollArea"
public let kAXScrollBarRole = "AXScrollBar" // New
public let kAXWebAreaRole = "AXWebArea"
public let kAXImageRole = "AXImage" // New
public let kAXListRole = "AXList" // New
public let kAXTableRole = "AXTable" // New
public let kAXOutlineRole = "AXOutline" // New
public let kAXColumnRole = "AXColumn" // New
public let kAXRowRole = "AXRow" // New
public let kAXToolbarRole = "AXToolbar"
public let kAXBusyIndicatorRole = "AXBusyIndicator" // New
public let kAXProgressIndicatorRole = "AXProgressIndicator" // New
public let kAXSliderRole = "AXSlider" // New
public let kAXIncrementorRole = "AXIncrementor" // New
public let kAXDisclosureTriangleRole = "AXDisclosureTriangle" // New
public let kAXMenuRole = "AXMenu" // New
public let kAXMenuItemRole = "AXMenuItem" // New
public let kAXSplitGroupRole = "AXSplitGroup" // New
public let kAXSplitterRole = "AXSplitter" // New
public let kAXColorWellRole = "AXColorWell" // New
public let kAXUnknownRole = "AXUnknown" // New
// Attributes for web content and tables/lists
public let kAXVisibleChildrenAttribute = "AXVisibleChildren"
public let kAXSelectedChildrenAttribute = "AXSelectedChildren"
public let kAXTabsAttribute = "AXTabs" // Often a kAXRadioGroup or kAXTabGroup role
public let kAXRowsAttribute = "AXRows"
public let kAXColumnsAttribute = "AXColumns"
public let kAXSelectedRowsAttribute = "AXSelectedRows" // New
public let kAXSelectedColumnsAttribute = "AXSelectedColumns" // New
public let kAXIndexAttribute = "AXIndex" // New (for rows/columns)
public let kAXDisclosingAttribute = "AXDisclosing" // New (for outlines)
// Custom or less standard attributes (verify usage and standard names)
public let kAXPathHintAttribute = "AXPathHint" // Our custom attribute for pathing
// String constant for "not available"
public let kAXNotAvailableString = "n/a"
// DOM specific attributes (these seem custom or web-specific, not standard Apple AX)
// Verify if these are actual attribute names exposed by web views or custom implementations.
public let kAXDOMIdentifierAttribute = "AXDOMIdentifier" // Example, might not be standard AX
public let kAXDOMClassListAttribute = "AXDOMClassList" // Example, might not be standard AX
public let kAXARIADOMResourceAttribute = "AXARIADOMResource" // Example
public let kAXARIADOMFunctionAttribute = "AXARIADOM-función" // Corrected identifier, kept original string value.
public let kAXARIADOMChildrenAttribute = "AXARIADOMChildren" // New
public let kAXDOMChildrenAttribute = "AXDOMChildren" // New
// New constants for missing attributes
public let kAXToolbarButtonAttribute = "AXToolbarButton"
public let kAXProxyAttribute = "AXProxy"
public let kAXSelectedCellsAttribute = "AXSelectedCells"
public let kAXHeaderAttribute = "AXHeader"
public let kAXHorizontalScrollBarAttribute = "AXHorizontalScrollBar"
public let kAXVerticalScrollBarAttribute = "AXVerticalScrollBar"
// Attributes used in child heuristic collection (often non-standard or specific)
public let kAXWebAreaChildrenAttribute = "AXWebAreaChildren"
public let kAXHTMLContentAttribute = "AXHTMLContent"
public let kAXApplicationNavigationAttribute = "AXApplicationNavigation"
public let kAXApplicationElementsAttribute = "AXApplicationElements"
public let kAXContentsAttribute = "AXContents"
public let kAXBodyAreaAttribute = "AXBodyArea"
public let kAXDocumentContentAttribute = "AXDocumentContent"
public let kAXWebPageContentAttribute = "AXWebPageContent"
public let kAXSplitGroupContentsAttribute = "AXSplitGroupContents"
public let kAXLayoutAreaChildrenAttribute = "AXLayoutAreaChildren"
public let kAXGroupChildrenAttribute = "AXGroupChildren"
// Helper function to convert AXError to a string
public func axErrorToString(_ error: AXError) -> String {
switch error {
case .success: return "success"
case .failure: return "failure"
case .apiDisabled: return "apiDisabled"
case .invalidUIElement: return "invalidUIElement"
case .invalidUIElementObserver: return "invalidUIElementObserver"
case .cannotComplete: return "cannotComplete"
case .attributeUnsupported: return "attributeUnsupported"
case .actionUnsupported: return "actionUnsupported"
case .notificationUnsupported: return "notificationUnsupported"
case .notImplemented: return "notImplemented"
case .notificationAlreadyRegistered: return "notificationAlreadyRegistered"
case .notificationNotRegistered: return "notificationNotRegistered"
case .noValue: return "noValue"
case .parameterizedAttributeUnsupported: return "parameterizedAttributeUnsupported"
case .notEnoughPrecision: return "notEnoughPrecision"
case .illegalArgument: return "illegalArgument"
@unknown default:
return "unknown AXError (code: \(error.rawValue))"
}
}
// MARK: - Custom Application/Computed Keys
public let focusedApplicationKey = "focused"
public let computedNameAttributeKey = "ComputedName"
public let isClickableAttributeKey = "IsClickable"
public let isIgnoredAttributeKey = "IsIgnored" // Used in AttributeMatcher
public let computedPathAttributeKey = "ComputedPath"

View File

@ -0,0 +1,108 @@
// AccessibilityError.swift - Defines custom error types for the accessibility tool.
import Foundation
import ApplicationServices // Import to make AXError visible
// Main error enum for the accessibility tool, incorporating parsing and operational errors.
public enum AccessibilityError: Error, CustomStringConvertible {
// Authorization & Setup Errors
case apiDisabled // Accessibility API is disabled.
case notAuthorized(String?) // Process is not authorized. Optional AXError for more detail.
// Command & Input Errors
case invalidCommand(String?) // Command is invalid or not recognized. Optional message.
case missingArgument(String) // A required argument is missing.
case invalidArgument(String) // An argument has an invalid value or format.
// Element & Search Errors
case appNotFound(String) // Application with specified bundle ID or name not found or not running.
case elementNotFound(String?) // Element matching criteria or path not found. Optional message.
case invalidElement // The AXUIElementRef is invalid or stale.
// Attribute Errors
case attributeUnsupported(String) // Attribute is not supported by the element.
case attributeNotReadable(String) // Attribute value cannot be read.
case attributeNotSettable(String) // Attribute is not settable.
case typeMismatch(expected: String, actual: String) // Value type does not match attribute's expected type.
case valueParsingFailed(details: String) // Failed to parse string into the required type for an attribute.
case valueNotAXValue(String) // Value is not an AXValue type when one is expected.
// Action Errors
case actionUnsupported(String) // Action is not supported by the element.
case actionFailed(String?, AXError?) // Action failed. Optional message and AXError.
// Generic & System Errors
case unknownAXError(AXError) // An unknown or unexpected AXError occurred.
case jsonEncodingFailed(Error?) // Failed to encode response to JSON.
case jsonDecodingFailed(Error?) // Failed to decode request from JSON.
case genericError(String) // A generic error with a custom message.
public var description: String {
switch self {
// Authorization & Setup
case .apiDisabled: return "Accessibility API is disabled. Please enable it in System Settings."
case .notAuthorized(let axErr):
let base = "Accessibility permissions are not granted for this process."
if let e = axErr { return "\(base) AXError: \(e)" }
return base
// Command & Input
case .invalidCommand(let msg):
let base = "Invalid command specified."
if let m = msg { return "\(base) \(m)" }
return base
case .missingArgument(let name): return "Missing required argument: \(name)."
case .invalidArgument(let details): return "Invalid argument: \(details)."
// Element & Search
case .appNotFound(let app): return "Application '\(app)' not found or not running."
case .elementNotFound(let msg):
let base = "No element matches the locator criteria or path."
if let m = msg { return "\(base) \(m)" }
return base
case .invalidElement: return "The specified UI element is invalid (possibly stale)."
// Attribute Errors
case .attributeUnsupported(let attr): return "Attribute '\(attr)' is not supported by this element."
case .attributeNotReadable(let attr): return "Attribute '\(attr)' is not readable."
case .attributeNotSettable(let attr): return "Attribute '\(attr)' is not settable."
case .typeMismatch(let expected, let actual): return "Type mismatch: Expected '\(expected)', got '\(actual)'."
case .valueParsingFailed(let details): return "Value parsing failed: \(details)."
case .valueNotAXValue(let attr): return "Value for attribute '\(attr)' is not an AXValue type as expected."
// Action Errors
case .actionUnsupported(let action): return "Action '\(action)' is not supported by this element."
case .actionFailed(let msg, let axErr):
var parts: [String] = ["Action failed."]
if let m = msg { parts.append(m) }
if let e = axErr { parts.append("AXError: \(e).") }
return parts.joined(separator: " ")
// Generic & System
case .unknownAXError(let e): return "An unexpected Accessibility Framework error occurred: \(e)."
case .jsonEncodingFailed(let err):
let base = "Failed to encode the response to JSON."
if let e = err { return "\(base) Error: \(e.localizedDescription)" }
return base
case .jsonDecodingFailed(let err):
let base = "Failed to decode the JSON command input."
if let e = err { return "\(base) Error: \(e.localizedDescription)" }
return base
case .genericError(let msg): return msg
}
}
// Helper to get a more specific exit code if needed, or a general one.
// This is just an example; actual exit codes might vary.
public var exitCode: Int32 {
switch self {
case .apiDisabled, .notAuthorized: return 10
case .invalidCommand, .missingArgument, .invalidArgument: return 20
case .appNotFound, .elementNotFound, .invalidElement: return 30
case .attributeUnsupported, .attributeNotReadable, .attributeNotSettable, .typeMismatch, .valueParsingFailed, .valueNotAXValue: return 40
case .actionUnsupported, .actionFailed: return 50
case .jsonEncodingFailed, .jsonDecodingFailed: return 60
case .unknownAXError, .genericError: return 1
}
}
}

View File

@ -0,0 +1,118 @@
// AccessibilityPermissions.swift - Utility for checking and managing accessibility permissions.
import Foundation
import ApplicationServices // For AXIsProcessTrusted(), AXUIElementCreateSystemWide(), etc.
import AppKit // For NSRunningApplication, NSAppleScript
private let kAXTrustedCheckOptionPromptKey = "AXTrustedCheckOptionPrompt"
// debug() is assumed to be globally available from Logging.swift
// getParentProcessName() is assumed to be globally available from ProcessUtils.swift
// kAXFocusedUIElementAttribute is assumed to be globally available from AccessibilityConstants.swift
// AccessibilityError is from AccessibilityError.swift
public struct AXPermissionsStatus {
public let isAccessibilityApiEnabled: Bool
public let isProcessTrustedForAccessibility: Bool
public var automationStatus: [String: Bool] = [:] // BundleID: Bool (true if permitted, false if denied, nil if not checked or app not running)
public var overallErrorMessages: [String] = []
public var canUseAccessibility: Bool {
isAccessibilityApiEnabled && isProcessTrustedForAccessibility
}
public func canAutomate(bundleID: String) -> Bool? {
return automationStatus[bundleID]
}
}
@MainActor
public func checkAccessibilityPermissions(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws {
// Define local dLog using passed-in parameters
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let trustedOptions = [kAXTrustedCheckOptionPromptKey: true] as CFDictionary
// tempLogs is already declared for getParentProcessName, which is good.
// var tempLogs: [String] = [] // This would be a re-declaration error if uncommented
if !AXIsProcessTrustedWithOptions(trustedOptions) {
// Use isDebugLoggingEnabled for the call to getParentProcessName
let parentName = getParentProcessName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let errorDetail = parentName != nil ? "Hint: Grant accessibility permissions to '\(parentName!)'." : "Hint: Ensure the application running this tool has Accessibility permissions."
dLog("Accessibility check failed (AXIsProcessTrustedWithOptions returned false). Details: \(errorDetail)")
throw AccessibilityError.notAuthorized(errorDetail)
} else {
dLog("Accessibility permissions are granted (AXIsProcessTrustedWithOptions returned true).")
}
}
// @MainActor // Removed again for pragmatic stability
public func getPermissionsStatus(checkAutomationFor bundleIDs: [String] = [], isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AXPermissionsStatus {
// Local dLog appends to currentDebugLogs
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("Starting full permission status check.")
// Check overall accessibility API status and process trust
let isProcessTrusted = AXIsProcessTrusted() // Non-prompting check
// let isApiEnabled = AXAPIEnabled() // System-wide check, REMOVED due to unavailability
if isDebugLoggingEnabled {
dLog("AXIsProcessTrusted() returned: \(isProcessTrusted)")
// dLog("AXAPIEnabled() returned: \(isApiEnabled) (Note: AXAPIEnabled is deprecated)") // Removed
if !isProcessTrusted {
let parentName = getParentProcessName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
let hint = parentName != nil ? "Hint: Grant accessibility permissions to '\(parentName!)'." : "Hint: Ensure the application running this tool has Accessibility permissions."
currentDebugLogs.append("Process is not trusted for Accessibility. \(hint)")
}
// Removed isApiEnabled check block
}
var automationStatus: [String: Bool] = [:]
if !bundleIDs.isEmpty && isProcessTrusted { // Only check automation if basic permissions seem okay (removed isApiEnabled from condition)
if isDebugLoggingEnabled { dLog("Checking automation permissions for bundle IDs: \(bundleIDs.joined(separator: ", "))") }
for bundleID in bundleIDs {
if NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).first != nil { // Changed from if let app = ...
let scriptSource = """
tell application id \"\(bundleID)\" to count windows
"""
var errorDict: NSDictionary? = nil
if let script = NSAppleScript(source: scriptSource) {
if isDebugLoggingEnabled { dLog("Executing AppleScript against \(bundleID) to check automation status.") }
let descriptor = script.executeAndReturnError(&errorDict) // descriptor is non-optional
if errorDict == nil && descriptor.descriptorType != typeNull {
// No error dictionary populated and descriptor is not typeNull, assume success for permissions.
automationStatus[bundleID] = true
if isDebugLoggingEnabled { dLog("AppleScript execution against \(bundleID) succeeded (no errorDict, descriptor type: \(descriptor.descriptorType.description)). Automation permitted.") }
} else {
automationStatus[bundleID] = false
if isDebugLoggingEnabled {
let errorCode = errorDict?[NSAppleScript.errorNumber] as? Int ?? 0
let errorMessage = errorDict?[NSAppleScript.errorMessage] as? String ?? "Unknown AppleScript error"
let descriptorDetails = errorDict == nil ? "Descriptor was typeNull (type: \(descriptor.descriptorType.description)) but no errorDict." : ""
currentDebugLogs.append("AppleScript execution against \(bundleID) failed. Automation likely denied. Code: \(errorCode), Msg: \(errorMessage). \(descriptorDetails)")
}
}
} else {
if isDebugLoggingEnabled { currentDebugLogs.append("Could not initialize AppleScript for bundle ID '\(bundleID)'.") }
}
} else {
if isDebugLoggingEnabled { currentDebugLogs.append("Application with bundle ID '\(bundleID)' is not running. Cannot check automation status.") }
// automationStatus[bundleID] remains nil (not checked)
}
}
} else if !bundleIDs.isEmpty {
if isDebugLoggingEnabled { dLog("Skipping automation permission checks because basic accessibility (isProcessTrusted: \(isProcessTrusted)) is not met.") }
}
let finalStatus = AXPermissionsStatus(
isAccessibilityApiEnabled: isProcessTrusted, // Base this on isProcessTrusted now
isProcessTrustedForAccessibility: isProcessTrusted,
automationStatus: automationStatus,
overallErrorMessages: currentDebugLogs // All logs collected so far become the messages
)
dLog("Finished permission status check. isAccessibilityApiEnabled: \(finalStatus.isAccessibilityApiEnabled), isProcessTrusted: \(finalStatus.isProcessTrustedForAccessibility)")
return finalStatus
}

View File

@ -0,0 +1,113 @@
// Attribute.swift - Defines a typed wrapper for Accessibility Attribute keys.
import Foundation
import ApplicationServices // Re-add for AXUIElement type
// import ApplicationServices // For kAX... constants - We will now use AccessibilityConstants.swift primarily
import CoreGraphics // For CGRect, CGPoint, CGSize, CFRange
// A struct to provide a type-safe way to refer to accessibility attributes.
// The generic type T represents the expected Swift type of the attribute's value.
// Note: For attributes returning AXValue (like CGPoint, CGRect), T might be the AXValue itself
// or the final unwrapped Swift type. For now, let's aim for the final Swift type where possible.
public struct Attribute<T> {
public let rawValue: String
// Internal initializer to allow creation within the module, e.g., for dynamic attribute strings.
internal init(_ rawValue: String) {
self.rawValue = rawValue
}
// MARK: - General Element Attributes
public static var role: Attribute<String> { Attribute<String>(kAXRoleAttribute) }
public static var subrole: Attribute<String> { Attribute<String>(kAXSubroleAttribute) }
public static var roleDescription: Attribute<String> { Attribute<String>(kAXRoleDescriptionAttribute) }
public static var title: Attribute<String> { Attribute<String>(kAXTitleAttribute) }
public static var description: Attribute<String> { Attribute<String>(kAXDescriptionAttribute) }
public static var help: Attribute<String> { Attribute<String>(kAXHelpAttribute) }
public static var identifier: Attribute<String> { Attribute<String>(kAXIdentifierAttribute) }
// MARK: - Value Attributes
// kAXValueAttribute can be many types. For a generic getter, Any might be appropriate,
// or specific versions if the context knows the type.
public static var value: Attribute<Any> { Attribute<Any>(kAXValueAttribute) }
// Example of a more specific value if known:
// static var stringValue: Attribute<String> { Attribute(kAXValueAttribute) }
// MARK: - State Attributes
public static var enabled: Attribute<Bool> { Attribute<Bool>(kAXEnabledAttribute) }
public static var focused: Attribute<Bool> { Attribute<Bool>(kAXFocusedAttribute) }
public static var busy: Attribute<Bool> { Attribute<Bool>(kAXElementBusyAttribute) }
public static var hidden: Attribute<Bool> { Attribute<Bool>(kAXHiddenAttribute) }
// MARK: - Hierarchy Attributes
public static var parent: Attribute<AXUIElement> { Attribute<AXUIElement>(kAXParentAttribute) }
// For children, the direct attribute often returns [AXUIElement].
// Element.children getter then wraps these.
public static var children: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXChildrenAttribute) }
public static var selectedChildren: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedChildrenAttribute) }
public static var visibleChildren: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleChildrenAttribute) }
public static var windows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXWindowsAttribute) }
public static var mainWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMainWindowAttribute) } // Can be nil
public static var focusedWindow: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedWindowAttribute) } // Can be nil
public static var focusedElement: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFocusedUIElementAttribute) } // Can be nil
// MARK: - Application Specific Attributes
// public static var enhancedUserInterface: Attribute<Bool> { Attribute<Bool>(kAXEnhancedUserInterfaceAttribute) } // Constant not found, commenting out
public static var frontmost: Attribute<Bool> { Attribute<Bool>(kAXFrontmostAttribute) }
public static var mainMenu: Attribute<AXUIElement> { Attribute<AXUIElement>(kAXMenuBarAttribute) }
// public static var hiddenApplication: Attribute<Bool> { Attribute(kAXHiddenAttribute) } // Same as element hidden, but for app. Covered by .hidden
// MARK: - Window Specific Attributes
public static var minimized: Attribute<Bool> { Attribute<Bool>(kAXMinimizedAttribute) }
public static var modal: Attribute<Bool> { Attribute<Bool>(kAXModalAttribute) }
public static var defaultButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXDefaultButtonAttribute) }
public static var cancelButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXCancelButtonAttribute) }
public static var closeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXCloseButtonAttribute) }
public static var zoomButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXZoomButtonAttribute) }
public static var minimizeButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXMinimizeButtonAttribute) }
public static var toolbarButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXToolbarButtonAttribute) }
public static var fullScreenButton: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXFullScreenButtonAttribute) }
public static var proxy: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXProxyAttribute) }
public static var growArea: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXGrowAreaAttribute) }
// MARK: - Table/List/Outline Attributes
public static var rows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXRowsAttribute) }
public static var columns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXColumnsAttribute) }
public static var selectedRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedRowsAttribute) }
public static var selectedColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedColumnsAttribute) }
public static var selectedCells: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXSelectedCellsAttribute) }
public static var visibleRows: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleRowsAttribute) }
public static var visibleColumns: Attribute<[AXUIElement]> { Attribute<[AXUIElement]>(kAXVisibleColumnsAttribute) }
public static var header: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXHeaderAttribute) }
public static var orientation: Attribute<String> { Attribute<String>(kAXOrientationAttribute) } // e.g., kAXVerticalOrientationValue
// MARK: - Text Attributes
public static var selectedText: Attribute<String> { Attribute<String>(kAXSelectedTextAttribute) }
public static var selectedTextRange: Attribute<CFRange> { Attribute<CFRange>(kAXSelectedTextRangeAttribute) }
public static var numberOfCharacters: Attribute<Int> { Attribute<Int>(kAXNumberOfCharactersAttribute) }
public static var visibleCharacterRange: Attribute<CFRange> { Attribute<CFRange>(kAXVisibleCharacterRangeAttribute) }
// Parameterized attributes are handled differently, often via functions.
// static var attributedStringForRange: Attribute<NSAttributedString> { Attribute(kAXAttributedStringForRangeParameterizedAttribute) }
// static var stringForRange: Attribute<String> { Attribute(kAXStringForRangeParameterizedAttribute) }
// MARK: - Scroll Area Attributes
public static var horizontalScrollBar: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXHorizontalScrollBarAttribute) }
public static var verticalScrollBar: Attribute<AXUIElement?> { Attribute<AXUIElement?>(kAXVerticalScrollBarAttribute) }
// MARK: - Action Related
// Action names are typically an array of strings.
public static var actionNames: Attribute<[String]> { Attribute<[String]>(kAXActionNamesAttribute) }
// Action description is parameterized by the action name, so a simple Attribute<String> isn't quite right.
// It would be kAXActionDescriptionAttribute, and you pass a parameter.
// For now, we will represent it as taking a string, and the usage site will need to handle parameterization.
public static var actionDescription: Attribute<String> { Attribute<String>(kAXActionDescriptionAttribute) }
// MARK: - AXValue holding attributes (expect these to return AXValueRef)
// These will typically be unwrapped by a helper function (like ValueParser or similar) into their Swift types.
public static var position: Attribute<CGPoint> { Attribute<CGPoint>(kAXPositionAttribute) }
public static var size: Attribute<CGSize> { Attribute<CGSize>(kAXSizeAttribute) }
// Note: CGRect for kAXBoundsAttribute is also common if available.
// For now, relying on position and size.
// Add more attributes as needed from ApplicationServices/HIServices Accessibility Attributes...
}

View File

@ -0,0 +1,87 @@
import Foundation
import ApplicationServices
// MARK: - Element Hierarchy Logic
extension Element {
@MainActor
public func children(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [Element]? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var collectedChildren: [Element] = []
var uniqueChildrenSet = Set<Element>()
var tempLogs: [String] = [] // For inner calls
dLog("Getting children for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
// Primary children attribute
tempLogs.removeAll()
if let directChildrenUI: [AXUIElement] = attribute(Attribute<[AXUIElement]>.children, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
currentDebugLogs.append(contentsOf: tempLogs)
for childUI in directChildrenUI {
let childAX = Element(childUI)
if !uniqueChildrenSet.contains(childAX) {
collectedChildren.append(childAX)
uniqueChildrenSet.insert(childAX)
}
}
} else {
currentDebugLogs.append(contentsOf: tempLogs) // Append logs even if nil
}
// Alternative children attributes
let alternativeAttributes: [String] = [
kAXVisibleChildrenAttribute, kAXWebAreaChildrenAttribute, kAXHTMLContentAttribute,
kAXARIADOMChildrenAttribute, kAXDOMChildrenAttribute, kAXApplicationNavigationAttribute,
kAXApplicationElementsAttribute, kAXContentsAttribute, kAXBodyAreaAttribute, kAXDocumentContentAttribute,
kAXWebPageContentAttribute, kAXSplitGroupContentsAttribute, kAXLayoutAreaChildrenAttribute,
kAXGroupChildrenAttribute, kAXSelectedChildrenAttribute, kAXRowsAttribute, kAXColumnsAttribute,
kAXTabsAttribute
]
for attrName in alternativeAttributes {
tempLogs.removeAll()
if let altChildrenUI: [AXUIElement] = attribute(Attribute<[AXUIElement]>(attrName), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
currentDebugLogs.append(contentsOf: tempLogs)
for childUI in altChildrenUI {
let childAX = Element(childUI)
if !uniqueChildrenSet.contains(childAX) {
collectedChildren.append(childAX)
uniqueChildrenSet.insert(childAX)
}
}
} else {
currentDebugLogs.append(contentsOf: tempLogs)
}
}
tempLogs.removeAll()
let currentRole = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
if currentRole == kAXApplicationRole as String {
tempLogs.removeAll()
if let windowElementsUI: [AXUIElement] = attribute(Attribute<[AXUIElement]>.windows, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
currentDebugLogs.append(contentsOf: tempLogs)
for childUI in windowElementsUI {
let childAX = Element(childUI)
if !uniqueChildrenSet.contains(childAX) {
collectedChildren.append(childAX)
uniqueChildrenSet.insert(childAX)
}
}
} else {
currentDebugLogs.append(contentsOf: tempLogs)
}
}
if collectedChildren.isEmpty {
dLog("No children found for element.")
return nil
} else {
dLog("Found \(collectedChildren.count) children.")
return collectedChildren
}
}
// generatePathString() is now fully implemented in Element.swift
}

View File

@ -0,0 +1,98 @@
import Foundation
import ApplicationServices
// MARK: - Element Common Attribute Getters & Status Properties
extension Element {
// Common Attribute Getters - now methods to accept logging parameters
@MainActor public func role(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.role, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func subrole(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.subrole, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func title(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.title, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func description(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.description, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isEnabled(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.enabled, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func value(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Any? {
attribute(Attribute<Any>.value, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func roleDescription(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.roleDescription, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func help(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.help, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func identifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
attribute(Attribute<String>.identifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
// Status Properties - now methods
@MainActor public func isFocused(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.focused, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isHidden(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.hidden, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isElementBusy(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
attribute(Attribute<Bool>.busy, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
@MainActor public func isIgnored(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
if attribute(Attribute<Bool>.hidden, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) == true {
return true
}
return false
}
@MainActor public func pid(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> pid_t? {
// This function doesn't call self.attribute, so its logging is self-contained if any.
// For now, assuming AXUIElementGetPid doesn't log through our system.
// If verbose logging of this specific call is needed, add dLog here.
var processID: pid_t = 0
let error = AXUIElementGetPid(self.underlyingElement, &processID)
if error == .success {
return processID
}
// Optional: dLog if error and isDebugLoggingEnabled
return nil
}
// Hierarchy and Relationship Getters - now methods
@MainActor public func parent(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element? {
guard let parentElementUI: AXUIElement = attribute(Attribute<AXUIElement>.parent, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else { return nil }
return Element(parentElementUI)
}
@MainActor public func windows(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [Element]? {
guard let windowElementsUI: [AXUIElement] = attribute(Attribute<[AXUIElement]>.windows, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else { return nil }
return windowElementsUI.map { Element($0) }
}
@MainActor public func mainWindow(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element? {
guard let windowElementUI: AXUIElement = attribute(Attribute<AXUIElement?>.mainWindow, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? nil else { return nil }
return Element(windowElementUI)
}
@MainActor public func focusedWindow(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element? {
guard let windowElementUI: AXUIElement = attribute(Attribute<AXUIElement?>.focusedWindow, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? nil else { return nil }
return Element(windowElementUI)
}
@MainActor public func focusedElement(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element? {
guard let elementUI: AXUIElement = attribute(Attribute<AXUIElement?>.focusedElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? nil else { return nil }
return Element(elementUI)
}
// Action-related - now a method
@MainActor
public func supportedActions(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [String]? {
return attribute(Attribute<[String]>.actionNames, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
}

View File

@ -0,0 +1,355 @@
// Element.swift - Wrapper for AXUIElement for a more Swift-idiomatic interface
import Foundation
import ApplicationServices // For AXUIElement and other C APIs
// We might need to import ValueHelpers or other local modules later
// Element struct is NOT @MainActor. Isolation is applied to members that need it.
public struct Element: Equatable, Hashable {
public let underlyingElement: AXUIElement
public init(_ element: AXUIElement) {
self.underlyingElement = element
}
// Implement Equatable - no longer needs nonisolated as struct is not @MainActor
public static func == (lhs: Element, rhs: Element) -> Bool {
return CFEqual(lhs.underlyingElement, rhs.underlyingElement)
}
// Implement Hashable - no longer needs nonisolated
public func hash(into hasher: inout Hasher) {
hasher.combine(CFHash(underlyingElement))
}
// Generic method to get an attribute's value (converted to Swift type T)
@MainActor
public func attribute<T>(_ attribute: Attribute<T>, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> T? {
// axValue is from ValueHelpers.swift and now expects logging parameters
return axValue(of: self.underlyingElement, attr: attribute.rawValue, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) as T?
}
// Method to get the raw CFTypeRef? for an attribute
// This is useful for functions like attributesMatch that do their own CFTypeID checking.
// This also needs to be @MainActor as AXUIElementCopyAttributeValue should be on main thread.
@MainActor
public func rawAttributeValue(named attributeName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> CFTypeRef? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
var value: CFTypeRef?
let error = AXUIElementCopyAttributeValue(self.underlyingElement, attributeName as CFString, &value)
if error == .success {
return value // Caller is responsible for CFRelease if it's a new object they own.
// For many get operations, this is a copy-get rule, but some are direct gets.
// Since we just return it, the caller should be aware or this function should manage it.
// Given AXSwift patterns, often the raw value isn't directly exposed like this,
// or it is clearly documented. For now, let's assume this is for internal use by attributesMatch
// which previously used copyAttributeValue which likely returned a +1 ref count object.
} else if error == .attributeUnsupported {
dLog("rawAttributeValue: Attribute \(attributeName) unsupported for element \(self.underlyingElement)")
} else if error == .noValue {
dLog("rawAttributeValue: Attribute \(attributeName) has no value for element \(self.underlyingElement)")
} else {
dLog("rawAttributeValue: Error getting attribute \(attributeName) for element \(self.underlyingElement): \(error.rawValue)")
}
return nil // Return nil if not success or if value was nil (though success should mean value is populated)
}
// MARK: - Common Attribute Getters (MOVED to Element+Properties.swift)
// MARK: - Status Properties (MOVED to Element+Properties.swift)
// MARK: - Hierarchy and Relationship Getters (Simpler ones MOVED to Element+Properties.swift)
// MARK: - Action-related (supportedActions MOVED to Element+Properties.swift)
// Remaining properties and methods will stay here for now
// (e.g., children, isActionSupported, performAction, parameterizedAttribute, briefDescription, generatePathString, static factories)
// MOVED to Element+Hierarchy.swift
// @MainActor public var children: [Element]? { ... }
// MARK: - Actions (supportedActions moved, other action methods remain)
@MainActor
public func isActionSupported(_ actionName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
if let actions: [String] = attribute(Attribute<[String]>.actionNames, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return actions.contains(actionName)
}
return false
}
@MainActor
@discardableResult
public func performAction(_ actionName: Attribute<String>, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> Element {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let error = AXUIElementPerformAction(self.underlyingElement, actionName.rawValue as CFString)
if error != .success {
// Now call the refactored briefDescription, passing the logs along.
let desc = self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("Action \(actionName.rawValue) failed on element \(desc). Error: \(error.rawValue)")
throw AccessibilityError.actionFailed("Action \(actionName.rawValue) failed on element \(desc)", error)
}
return self
}
@MainActor
@discardableResult
public func performAction(_ actionName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> Element {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let error = AXUIElementPerformAction(self.underlyingElement, actionName as CFString)
if error != .success {
// Now call the refactored briefDescription, passing the logs along.
let desc = self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("Action \(actionName) failed on element \(desc). Error: \(error.rawValue)")
throw AccessibilityError.actionFailed("Action \(actionName) failed on element \(desc)", error)
}
return self
}
// MARK: - Parameterized Attributes
@MainActor
public func parameterizedAttribute<T>(_ attribute: Attribute<T>, forParameter parameter: Any, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> T? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var cfParameter: CFTypeRef?
// Convert Swift parameter to CFTypeRef for the API
if var range = parameter as? CFRange {
cfParameter = AXValueCreate(.cfRange, &range)
} else if let string = parameter as? String {
cfParameter = string as CFString
} else if let number = parameter as? NSNumber {
cfParameter = number
} else if CFGetTypeID(parameter as CFTypeRef) != 0 { // Check if it's already a CFTypeRef-compatible type
cfParameter = (parameter as CFTypeRef)
} else {
dLog("parameterizedAttribute: Unsupported parameter type \(type(of: parameter))")
return nil
}
guard let actualCFParameter = cfParameter else {
dLog("parameterizedAttribute: Failed to convert parameter to CFTypeRef.")
return nil
}
var value: CFTypeRef?
let error = AXUIElementCopyParameterizedAttributeValue(underlyingElement, attribute.rawValue as CFString, actualCFParameter, &value)
if error != .success {
dLog("parameterizedAttribute: Error \(error.rawValue) getting attribute \(attribute.rawValue)")
return nil
}
guard let resultCFValue = value else { return nil }
// Use axValue's unwrapping and casting logic if possible, by temporarily creating an element and attribute
// This is a bit of a conceptual stretch, as axValue is designed for direct attributes.
// A more direct unwrap using ValueUnwrapper might be cleaner here.
let unwrappedValue = ValueUnwrapper.unwrap(resultCFValue, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
guard let finalValue = unwrappedValue else { return nil }
// Perform type casting similar to axValue
if T.self == String.self {
if let str = finalValue as? String { return str as? T }
else if let attrStr = finalValue as? NSAttributedString { return attrStr.string as? T }
return nil
}
if let castedValue = finalValue as? T {
return castedValue
}
dLog("parameterizedAttribute: Fallback cast attempt for attribute '\(attribute.rawValue)' to type \(T.self) FAILED. Unwrapped value was \(type(of: finalValue)): \(finalValue)")
return nil
}
// MOVED to Element+Hierarchy.swift
// @MainActor
// public func generatePathString() -> String { ... }
// MARK: - Attribute Accessors (Raw and Typed)
// ... existing attribute accessors ...
// MARK: - Computed Properties for Common Attributes & Heuristics
// ... existing properties like role, title, isEnabled ...
/// A computed name for the element, derived from common attributes like title, value, description, etc.
/// This provides a general-purpose, human-readable name.
@MainActor
// Convert from a computed property to a method to accept logging parameters
public func computedName(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
// Now uses the passed-in logging parameters for its internal calls
if let titleStr = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !titleStr.isEmpty, titleStr != kAXNotAvailableString { return titleStr }
if let valueStr: String = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) as? String, !valueStr.isEmpty, valueStr != kAXNotAvailableString { return valueStr }
if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !descStr.isEmpty, descStr != kAXNotAvailableString { return descStr }
if let helpStr: String = self.attribute(Attribute<String>(kAXHelpAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !helpStr.isEmpty, helpStr != kAXNotAvailableString { return helpStr }
if let phValueStr: String = self.attribute(Attribute<String>(kAXPlaceholderValueAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !phValueStr.isEmpty, phValueStr != kAXNotAvailableString { return phValueStr }
let roleNameStr: String = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "Element"
if let roleDescStr: String = self.roleDescription(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !roleDescStr.isEmpty, roleDescStr != kAXNotAvailableString {
return "\(roleDescStr) (\(roleNameStr))"
}
return nil
}
// MARK: - Path and Hierarchy
}
// Convenience factory for the application element - already @MainActor
@MainActor
public func applicationElement(for bundleIdOrName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
// Now call pid() with logging parameters
guard let pid = pid(forAppIdentifier: bundleIdOrName, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
// dLog for "Failed to find PID..." is now handled inside pid() itself or if it returns nil here, we can log the higher level failure.
// The message below is slightly redundant if pid() logs its own failure, but can be useful.
dLog("applicationElement: Failed to obtain PID for '\(bundleIdOrName)'. Check previous logs from pid().")
return nil
}
let appElement = AXUIElementCreateApplication(pid)
return Element(appElement)
}
// Convenience factory for the system-wide element - already @MainActor
@MainActor
public func systemWideElement(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element {
// This function doesn't do much logging itself, but consistent signature is good.
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("Creating system-wide element.")
return Element(AXUIElementCreateSystemWide())
}
// Extension to generate a descriptive path string
extension Element {
@MainActor
// Update signature to include logging parameters
public func generatePathString(upTo ancestor: Element? = nil, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var pathComponents: [String] = []
var currentElement: Element? = self
var depth = 0 // Safety break for very deep or circular hierarchies
let maxDepth = 25
var tempLogs: [String] = [] // Temporary logs for calls within the loop
dLog("generatePathString started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "nil")")
while let element = currentElement, depth < maxDepth {
tempLogs.removeAll() // Clear for each iteration
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
pathComponents.append(briefDesc)
currentDebugLogs.append(contentsOf: tempLogs) // Append logs from briefDescription
if let ancestor = ancestor, element == ancestor {
dLog("generatePathString: Reached specified ancestor: \(briefDesc)")
break // Reached the specified ancestor
}
// Check role to prevent going above application or a window if its parent is the app
tempLogs.removeAll()
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let parentElement = element.parent(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let parentRole = parentElement?.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
if role == kAXApplicationRole || (role == kAXWindowRole && parentRole == kAXApplicationRole && ancestor == nil) {
dLog("generatePathString: Stopping at \(role == kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)")
break
}
currentElement = parentElement
depth += 1
if currentElement == nil && role != kAXApplicationRole {
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
dLog("generatePathString: Unexpected orphan: \(orphanLog)")
pathComponents.append(orphanLog)
break
}
}
if depth >= maxDepth {
dLog("generatePathString: Reached max depth (\(maxDepth)). Path might be truncated.")
pathComponents.append("<...max_depth_reached...>")
}
let finalPath = pathComponents.reversed().joined(separator: " -> ")
dLog("generatePathString finished. Path: \(finalPath)")
return finalPath
}
// New function to return path components as an array
@MainActor
public func generatePathArray(upTo ancestor: Element? = nil, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [String] {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var pathComponents: [String] = []
var currentElement: Element? = self
var depth = 0
let maxDepth = 25
var tempLogs: [String] = []
dLog("generatePathArray started for element: \(self.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil")")
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
while let element = currentElement, depth < maxDepth {
tempLogs.removeAll()
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
pathComponents.append(briefDesc)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
if let ancestor = ancestor, element == ancestor {
dLog("generatePathArray: Reached specified ancestor: \(briefDesc)")
break
}
tempLogs.removeAll()
let role = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
tempLogs.removeAll()
let parentElement = element.parent(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
tempLogs.removeAll()
let parentRole = parentElement?.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs); tempLogs.removeAll()
if role == kAXApplicationRole || (role == kAXWindowRole && parentRole == kAXApplicationRole && ancestor == nil) {
dLog("generatePathArray: Stopping at \(role == kAXApplicationRole ? "Application" : "Window under App"): \(briefDesc)")
break
}
currentElement = parentElement
depth += 1
if currentElement == nil && role != kAXApplicationRole {
let orphanLog = "< Orphaned element path component: \(briefDesc) (role: \(role ?? "nil")) >"
dLog("generatePathArray: Unexpected orphan: \(orphanLog)")
pathComponents.append(orphanLog)
break
}
}
if depth >= maxDepth {
dLog("generatePathArray: Reached max depth (\(maxDepth)). Path might be truncated.")
pathComponents.append("<...max_depth_reached...>")
}
let reversedPathComponents = Array(pathComponents.reversed())
dLog("generatePathArray finished. Path components: \(reversedPathComponents.joined(separator: "/"))") // Log for debugging
return reversedPathComponents
}
}

View File

@ -0,0 +1,305 @@
// Models.swift - Contains Codable structs for command handling and responses
import Foundation
// Enum for output formatting options
public enum OutputFormat: String, Codable {
case smart // Default, tries to be concise and informative
case verbose // More detailed output, includes more attributes/info
case text_content // Primarily extracts textual content
case json_string // Returns the attributes as a JSON string (new)
}
// Define CommandType enum
public enum CommandType: String, Codable {
case query
case performAction = "performAction"
case getAttributes = "getAttributes"
case batch
case describeElement = "describeElement"
case getFocusedElement = "getFocusedElement"
case collectAll = "collectAll"
case extractText = "extractText"
case ping
// Add future commands here, ensuring case matches JSON or provide explicit raw value
}
// For encoding/decoding 'Any' type in JSON, especially for element attributes.
public struct AnyCodable: Codable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self.value = ()
} else if let bool = try? container.decode(Bool.self) {
self.value = bool
} else if let int = try? container.decode(Int.self) {
self.value = int
} else if let int32 = try? container.decode(Int32.self) {
self.value = int32
} else if let int64 = try? container.decode(Int64.self) {
self.value = int64
} else if let uint = try? container.decode(UInt.self) {
self.value = uint
} else if let uint32 = try? container.decode(UInt32.self) {
self.value = uint32
} else if let uint64 = try? container.decode(UInt64.self) {
self.value = uint64
} else if let double = try? container.decode(Double.self) {
self.value = double
} else if let float = try? container.decode(Float.self) {
self.value = float
} else if let string = try? container.decode(String.self) {
self.value = string
} else if let array = try? container.decode([AnyCodable].self) {
self.value = array.map { $0.value }
} else if let dictionary = try? container.decode([String: AnyCodable].self) {
self.value = dictionary.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
case is Void:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int32 as Int32:
try container.encode(Int(int32))
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let double as Double:
try container.encode(double)
case let float as Float:
try container.encode(float)
case let string as String:
try container.encode(string)
case let array as [AnyCodable]:
try container.encode(array)
case let array as [Any?]:
try container.encode(array.map { AnyCodable($0) })
case let dictionary as [String: AnyCodable]:
try container.encode(dictionary)
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyCodable($0) })
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyCodable value cannot be encoded")
throw EncodingError.invalidValue(value, context)
}
}
}
// Type alias for element attributes dictionary
public typealias ElementAttributes = [String: AnyCodable]
// Main command envelope - REPLACED with definition from axorc.swift for consistency
public struct CommandEnvelope: Codable {
public let command_id: String
public let command: CommandType // Uses CommandType from this file
public let application: String?
public let attributes: [String]?
public let payload: [String: String]? // For ping compatibility
public let debug_logging: Bool?
public let locator: Locator? // Locator from this file
public let path_hint: [String]?
public let max_elements: Int?
public let output_format: OutputFormat? // OutputFormat from this file
public let action_name: String? // For performAction
public let action_value: AnyCodable? // For performAction (AnyCodable from this file)
public let sub_commands: [CommandEnvelope]? // For batch command
// Added a public initializer for convenience, matching fields.
public init(command_id: String,
command: CommandType,
application: String? = nil,
attributes: [String]? = nil,
payload: [String : String]? = nil,
debug_logging: Bool? = nil,
locator: Locator? = nil,
path_hint: [String]? = nil,
max_elements: Int? = nil,
output_format: OutputFormat? = nil,
action_name: String? = nil,
action_value: AnyCodable? = nil,
sub_commands: [CommandEnvelope]? = nil
) {
self.command_id = command_id
self.command = command
self.application = application
self.attributes = attributes
self.payload = payload
self.debug_logging = debug_logging
self.locator = locator
self.path_hint = path_hint
self.max_elements = max_elements
self.output_format = output_format
self.action_name = action_name
self.action_value = action_value
self.sub_commands = sub_commands
}
}
// Locator for finding elements
public struct Locator: Codable {
public var match_all: Bool?
public var criteria: [String: String]
public var root_element_path_hint: [String]?
public var requireAction: String?
public var computed_name_contains: String?
enum CodingKeys: String, CodingKey {
case match_all
case criteria
case root_element_path_hint
case requireAction = "require_action"
case computed_name_contains
}
public init(match_all: Bool? = nil, criteria: [String: String] = [:], root_element_path_hint: [String]? = nil, requireAction: String? = nil, computed_name_contains: String? = nil) {
self.match_all = match_all
self.criteria = criteria
self.root_element_path_hint = root_element_path_hint
self.requireAction = requireAction
self.computed_name_contains = computed_name_contains
}
}
// Response for query command (single element)
public struct QueryResponse: Codable {
public var command_id: String
public var success: Bool
public var command: String
public var data: AXElement?
public var attributes: ElementAttributes?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, success: Bool = true, command: String = "getFocusedElement", data: AXElement? = nil, attributes: ElementAttributes? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.command = command
self.data = data
self.attributes = attributes
self.error = error
self.debug_logs = debug_logs
}
}
// Response for collect_all command (multiple elements)
public struct MultiQueryResponse: Codable {
public var command_id: String
public var elements: [ElementAttributes]?
public var count: Int?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, elements: [ElementAttributes]? = nil, count: Int? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.elements = elements
self.count = count ?? elements?.count
self.error = error
self.debug_logs = debug_logs
}
}
// Response for perform_action command
public struct PerformResponse: Codable {
public var command_id: String
public var success: Bool
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, success: Bool, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = success
self.error = error
self.debug_logs = debug_logs
}
}
// Response for extract_text command
public struct TextContentResponse: Codable {
public var command_id: String
public var text_content: String?
public var error: String?
public var debug_logs: [String]?
public init(command_id: String, text_content: String? = nil, error: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.text_content = text_content
self.error = error
self.debug_logs = debug_logs
}
}
// Generic error response
public struct ErrorResponse: Codable {
public var command_id: String
public var success: Bool
public var error: ErrorDetail
public var debug_logs: [String]?
public init(command_id: String, error: String, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = false
self.error = ErrorDetail(message: error)
self.debug_logs = debug_logs
}
}
public struct ErrorDetail: Codable {
public var message: String
public init(message: String) {
self.message = message
}
}
// Simple success response, e.g. for ping
public struct SimpleSuccessResponse: Codable, Equatable {
public var command_id: String
public var success: Bool
public var status: String
public var message: String
public var details: String?
public var debug_logs: [String]?
public init(command_id: String, status: String, message: String, details: String? = nil, debug_logs: [String]? = nil) {
self.command_id = command_id
self.success = true
self.status = status
self.message = message
self.details = details
self.debug_logs = debug_logs
}
}
// Placeholder for any additional models if needed
public struct AXElement: Codable {
public var attributes: ElementAttributes?
public var path: [String]?
public init(attributes: ElementAttributes?, path: [String]? = nil) {
self.attributes = attributes
self.path = path
}
}

View File

@ -0,0 +1,120 @@
// ProcessUtils.swift - Utilities for process and application inspection.
import Foundation
import AppKit // For NSRunningApplication, NSWorkspace
// debug() is assumed to be globally available from Logging.swift
@MainActor
public func pid(forAppIdentifier ident: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> pid_t? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
dLog("ProcessUtils: Attempting to find PID for identifier: '\(ident)'")
if ident == "focused" {
dLog("ProcessUtils: Identifier is 'focused'. Checking frontmost application.")
if let frontmostApp = NSWorkspace.shared.frontmostApplication {
dLog("ProcessUtils: Frontmost app is '\(frontmostApp.localizedName ?? "nil")' (PID: \(frontmostApp.processIdentifier), BundleID: \(frontmostApp.bundleIdentifier ?? "nil"), Terminated: \(frontmostApp.isTerminated))")
return frontmostApp.processIdentifier
} else {
dLog("ProcessUtils: NSWorkspace.shared.frontmostApplication returned nil.")
return nil
}
}
dLog("ProcessUtils: Trying by bundle identifier '\(ident)'.")
let appsByBundleID = NSRunningApplication.runningApplications(withBundleIdentifier: ident)
if !appsByBundleID.isEmpty {
dLog("ProcessUtils: Found \(appsByBundleID.count) app(s) by bundle ID '\(ident)'.")
for (index, app) in appsByBundleID.enumerated() {
dLog("ProcessUtils: App [\(index)] - Name: '\(app.localizedName ?? "nil")', PID: \(app.processIdentifier), BundleID: '\(app.bundleIdentifier ?? "nil")', Terminated: \(app.isTerminated)")
}
if let app = appsByBundleID.first(where: { !$0.isTerminated }) {
dLog("ProcessUtils: Using first non-terminated app found by bundle ID: '\(app.localizedName ?? "nil")' (PID: \(app.processIdentifier))")
return app.processIdentifier
} else {
dLog("ProcessUtils: All apps found by bundle ID '\(ident)' are terminated or list was empty initially but then non-empty (should not happen).")
}
} else {
dLog("ProcessUtils: No applications found for bundle identifier '\(ident)'.")
}
dLog("ProcessUtils: Trying by localized name (case-insensitive) '\(ident)'.")
let allApps = NSWorkspace.shared.runningApplications
if let appByName = allApps.first(where: { !$0.isTerminated && $0.localizedName?.lowercased() == ident.lowercased() }) {
dLog("ProcessUtils: Found non-terminated app by localized name: '\(appByName.localizedName ?? "nil")' (PID: \(appByName.processIdentifier), BundleID: '\(appByName.bundleIdentifier ?? "nil")')")
return appByName.processIdentifier
} else {
dLog("ProcessUtils: No non-terminated app found matching localized name '\(ident)'. Found \(allApps.filter { $0.localizedName?.lowercased() == ident.lowercased() }.count) terminated or non-matching apps by this name.")
}
dLog("ProcessUtils: Trying by path '\(ident)'.")
let potentialPath = (ident as NSString).expandingTildeInPath
if FileManager.default.fileExists(atPath: potentialPath),
let bundle = Bundle(path: potentialPath),
let bundleId = bundle.bundleIdentifier {
dLog("ProcessUtils: Path '\(potentialPath)' resolved to bundle '\(bundleId)'. Looking up running apps with this bundle ID.")
let appsByResolvedBundleID = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId)
if !appsByResolvedBundleID.isEmpty {
dLog("ProcessUtils: Found \(appsByResolvedBundleID.count) app(s) by resolved bundle ID '\(bundleId)'.")
for (index, app) in appsByResolvedBundleID.enumerated() {
dLog("ProcessUtils: App [\(index)] from path - Name: '\(app.localizedName ?? "nil")', PID: \(app.processIdentifier), BundleID: '\(app.bundleIdentifier ?? "nil")', Terminated: \(app.isTerminated)")
}
if let app = appsByResolvedBundleID.first(where: { !$0.isTerminated }) {
dLog("ProcessUtils: Using first non-terminated app found by path (via bundle ID '\(bundleId)'): '\(app.localizedName ?? "nil")' (PID: \(app.processIdentifier))")
return app.processIdentifier
} else {
dLog("ProcessUtils: All apps for bundle ID '\(bundleId)' (from path) are terminated.")
}
} else {
dLog("ProcessUtils: No running applications found for bundle identifier '\(bundleId)' derived from path '\(potentialPath)'.")
}
} else {
dLog("ProcessUtils: Identifier '\(ident)' is not a valid file path or bundle info could not be read.")
}
dLog("ProcessUtils: Trying by interpreting '\(ident)' as a PID string.")
if let pidInt = Int32(ident) {
if let appByPid = NSRunningApplication(processIdentifier: pidInt), !appByPid.isTerminated {
dLog("ProcessUtils: Found non-terminated app by PID string '\(ident)': '\(appByPid.localizedName ?? "nil")' (PID: \(appByPid.processIdentifier), BundleID: '\(appByPid.bundleIdentifier ?? "nil")')")
return pidInt
} else {
if NSRunningApplication(processIdentifier: pidInt)?.isTerminated == true {
dLog("ProcessUtils: String '\(ident)' is a PID, but the app is terminated.")
} else {
dLog("ProcessUtils: String '\(ident)' looked like a PID but no running application found for it.")
}
}
}
dLog("ProcessUtils: PID not found for identifier: '\(ident)'")
return nil
}
@MainActor
func findFrontmostApplicationPid(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> pid_t? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("ProcessUtils: findFrontmostApplicationPid called.")
if let frontmostApp = NSWorkspace.shared.frontmostApplication {
dLog("ProcessUtils: Frontmost app for findFrontmostApplicationPid is '\(frontmostApp.localizedName ?? "nil")' (PID: \(frontmostApp.processIdentifier), BundleID: '\(frontmostApp.bundleIdentifier ?? "nil")', Terminated: \(frontmostApp.isTerminated))")
return frontmostApp.processIdentifier
} else {
dLog("ProcessUtils: NSWorkspace.shared.frontmostApplication returned nil in findFrontmostApplicationPid.")
return nil
}
}
public func getParentProcessName(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let parentPid = getppid()
dLog("ProcessUtils: Parent PID is \(parentPid).")
if let parentApp = NSRunningApplication(processIdentifier: parentPid) {
dLog("ProcessUtils: Parent app is '\(parentApp.localizedName ?? "nil")' (BundleID: '\(parentApp.bundleIdentifier ?? "nil")')")
return parentApp.localizedName ?? parentApp.bundleIdentifier
}
dLog("ProcessUtils: Could not get NSRunningApplication for parent PID \(parentPid).")
return nil
}

View File

@ -0,0 +1,377 @@
// AttributeHelpers.swift - Contains functions for fetching and formatting element attributes
import Foundation
import ApplicationServices // For AXUIElement related types
import CoreGraphics // For potential future use with geometry types from attributes
// Note: This file assumes Models (for ElementAttributes, AnyCodable),
// Logging (for debug), AccessibilityConstants, and Utils (for axValue) are available in the same module.
// And now Element for the new element wrapper.
// Define AttributeData and AttributeSource here as they are not found by the compiler
public enum AttributeSource: String, Codable {
case direct // Directly from an AXAttribute
case computed // Derived by this tool
}
public struct AttributeData: Codable {
public let value: AnyCodable
public let source: AttributeSource
}
// MARK: - Element Summary Helpers
// Removed getSingleElementSummary as it was unused.
// MARK: - Internal Fetch Logic Helpers
// Approach using direct property access within a switch statement
@MainActor
private func extractDirectPropertyValue(for attributeName: String, from element: Element, outputFormat: OutputFormat, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> (value: Any?, handled: Bool) {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
var extractedValue: Any?
var handled = true
// Ensure logging parameters are passed to Element methods
switch attributeName {
case kAXPathHintAttribute:
extractedValue = element.attribute(Attribute<String>(kAXPathHintAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXRoleAttribute:
extractedValue = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXSubroleAttribute:
extractedValue = element.subrole(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXTitleAttribute:
extractedValue = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXDescriptionAttribute:
extractedValue = element.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXEnabledAttribute:
let val = element.isEnabled(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case kAXFocusedAttribute:
let val = element.isFocused(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case kAXHiddenAttribute:
let val = element.isHidden(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case isIgnoredAttributeKey:
let val = element.isIgnored(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val ? "true" : "false" }
case "PID":
let val = element.pid(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
case kAXElementBusyAttribute:
let val = element.isElementBusy(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
extractedValue = val
if outputFormat == .text_content { extractedValue = val?.description ?? kAXNotAvailableString }
default:
handled = false
}
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from Element method calls
return (extractedValue, handled)
}
@MainActor
private func determineAttributesToFetch(requestedAttributes: [String], forMultiDefault: Bool, targetRole: String?, element: Element, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [String] {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var attributesToFetch = requestedAttributes
if forMultiDefault {
attributesToFetch = [kAXRoleAttribute, kAXValueAttribute, kAXTitleAttribute, kAXIdentifierAttribute]
if let role = targetRole, role == kAXStaticTextRole {
attributesToFetch = [kAXRoleAttribute, kAXValueAttribute, kAXIdentifierAttribute]
}
} else if attributesToFetch.isEmpty {
var attrNames: CFArray?
if AXUIElementCopyAttributeNames(element.underlyingElement, &attrNames) == .success, let names = attrNames as? [String] {
attributesToFetch.append(contentsOf: names)
dLog("determineAttributesToFetch: No specific attributes requested, fetched all \(names.count) available: \(names.joined(separator: ", "))")
} else {
dLog("determineAttributesToFetch: No specific attributes requested and failed to fetch all available names.")
}
}
return attributesToFetch
}
// MARK: - Public Attribute Getters
@MainActor
public func getElementAttributes(_ element: Element, requestedAttributes: [String], forMultiDefault: Bool = false, targetRole: String? = nil, outputFormat: OutputFormat = .smart, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> ElementAttributes {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls, cleared and appended for each.
var result = ElementAttributes()
let valueFormatOption: ValueFormatOption = (outputFormat == .verbose) ? .verbose : .default
tempLogs.removeAll()
dLog("getElementAttributes starting for element: \(element.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)), format: \(outputFormat)")
currentDebugLogs.append(contentsOf: tempLogs)
let attributesToFetch = determineAttributesToFetch(requestedAttributes: requestedAttributes, forMultiDefault: forMultiDefault, targetRole: targetRole, element: element, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("Attributes to fetch: \(attributesToFetch.joined(separator: ", "))")
for attr in attributesToFetch {
var tempCallLogs: [String] = [] // Logs for a specific attribute fetching call
if attr == kAXParentAttribute {
tempCallLogs.removeAll()
let parent = element.parent(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[kAXParentAttribute] = formatParentAttribute(parent, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // formatParentAttribute will manage its own logs now
currentDebugLogs.append(contentsOf: tempCallLogs) // Collect logs from element.parent and formatParentAttribute
continue
} else if attr == kAXChildrenAttribute {
tempCallLogs.removeAll()
let children = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[attr] = formatChildrenAttribute(children, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs) // formatChildrenAttribute will manage its own logs
currentDebugLogs.append(contentsOf: tempCallLogs)
continue
} else if attr == kAXFocusedUIElementAttribute {
tempCallLogs.removeAll()
let focused = element.focusedElement(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
result[attr] = AnyCodable(formatFocusedUIElementAttribute(focused, outputFormat: outputFormat, valueFormatOption: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs))
currentDebugLogs.append(contentsOf: tempCallLogs)
continue
}
tempCallLogs.removeAll()
let (directValue, wasHandledDirectly) = extractDirectPropertyValue(for: attr, from: element, outputFormat: outputFormat, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
currentDebugLogs.append(contentsOf: tempCallLogs)
var finalValueToStore: Any?
if wasHandledDirectly {
finalValueToStore = directValue
dLog("Attribute '\(attr)' handled directly, value: \(String(describing: directValue))")
} else {
tempCallLogs.removeAll()
let rawCFValue: CFTypeRef? = element.rawAttributeValue(named: attr, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempCallLogs)
currentDebugLogs.append(contentsOf: tempCallLogs)
if outputFormat == .text_content {
finalValueToStore = formatRawCFValueForTextContent(rawCFValue)
} else {
finalValueToStore = formatCFTypeRef(rawCFValue, option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
dLog("Attribute '\(attr)' fetched via rawAttributeValue, formatted value: \(String(describing: finalValueToStore))")
}
if outputFormat == .smart {
if let strVal = finalValueToStore as? String,
(strVal.isEmpty || strVal == "<nil>" || strVal == "AXValue (Illegal)" || strVal.contains("Unknown CFType") || strVal == kAXNotAvailableString) {
dLog("Smart format: Skipping attribute '\(attr)' with unhelpful value: \(strVal)")
continue
}
}
result[attr] = AnyCodable(finalValueToStore)
}
tempLogs.removeAll()
if result[computedNameAttributeKey] == nil {
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
result[computedNameAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(name), source: .computed))
dLog("Added ComputedName: \(name)")
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if result[isClickableAttributeKey] == nil {
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if isButton || hasPressAction {
result[isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
dLog("Added IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if outputFormat == .verbose && result[computedPathAttributeKey] == nil {
let path = element.generatePathString(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
result[computedPathAttributeKey] = AnyCodable(path)
dLog("Added ComputedPath (verbose): \(path)")
}
currentDebugLogs.append(contentsOf: tempLogs)
populateActionNamesAttribute(for: element, result: &result, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
dLog("getElementAttributes finished. Result keys: \(result.keys.joined(separator: ", "))")
return result
}
@MainActor
private func populateActionNamesAttribute(for element: Element, result: inout ElementAttributes, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if result[kAXActionNamesAttribute] != nil {
dLog("populateActionNamesAttribute: Already present or explicitly requested, skipping.")
return
}
currentDebugLogs.append(contentsOf: tempLogs) // Appending potentially empty tempLogs, for consistency, though it does nothing here.
var actionsToStore: [String]?
tempLogs.removeAll()
if let currentActions = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !currentActions.isEmpty {
actionsToStore = currentActions
dLog("populateActionNamesAttribute: Got \(currentActions.count) from supportedActions.")
} else {
dLog("populateActionNamesAttribute: supportedActions was nil or empty. Trying kAXActionsAttribute.")
tempLogs.removeAll() // Clear before next call that uses it
if let fallbackActions: [String] = element.attribute(Attribute<[String]>(kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !fallbackActions.isEmpty {
actionsToStore = fallbackActions
dLog("populateActionNamesAttribute: Got \(fallbackActions.count) from kAXActionsAttribute fallback.")
}
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let pressActionSupported = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs)
dLog("populateActionNamesAttribute: kAXPressAction supported: \(pressActionSupported).")
if pressActionSupported {
if actionsToStore == nil { actionsToStore = [kAXPressAction] }
else if !actionsToStore!.contains(kAXPressAction) { actionsToStore!.append(kAXPressAction) }
}
if let finalActions = actionsToStore, !finalActions.isEmpty {
result[kAXActionNamesAttribute] = AnyCodable(finalActions)
dLog("populateActionNamesAttribute: Final actions: \(finalActions.joined(separator: ", ")).")
} else {
tempLogs.removeAll()
let primaryNil = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let fallbackNil = element.attribute(Attribute<[String]>(kAXActionsAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == nil
currentDebugLogs.append(contentsOf: tempLogs)
if primaryNil && fallbackNil && !pressActionSupported {
result[kAXActionNamesAttribute] = AnyCodable(kAXNotAvailableString)
dLog("populateActionNamesAttribute: All action sources nil/unsupported. Set to kAXNotAvailableString.")
} else {
result[kAXActionNamesAttribute] = AnyCodable("\(kAXNotAvailableString) (no specific actions found or list empty)")
dLog("populateActionNamesAttribute: Some action source present but list empty. Set to verbose kAXNotAvailableString.")
}
}
}
// MARK: - Attribute Formatting Helpers
// Helper function to format the parent attribute
@MainActor
private func formatParentAttribute(_ parent: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let parentElement = parent else { return AnyCodable(nil as String?) }
if outputFormat == .text_content {
return AnyCodable("Element: \(parentElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
} else {
return AnyCodable(parentElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
}
// Helper function to format the children attribute
@MainActor
private func formatChildrenAttribute(_ children: [Element]?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let actualChildren = children, !actualChildren.isEmpty else { return AnyCodable("[]") }
if outputFormat == .text_content {
return AnyCodable("Array of \(actualChildren.count) Element(s)")
} else if outputFormat == .verbose {
var childrenSummaries: [String] = []
for childElement in actualChildren {
childrenSummaries.append(childElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
return AnyCodable("[\(childrenSummaries.joined(separator: ", "))]")
} else { // .smart output
return AnyCodable("Array of \(actualChildren.count) children")
}
}
// Helper function to format the focused UI element attribute
@MainActor
private func formatFocusedUIElementAttribute(_ focusedElement: Element?, outputFormat: OutputFormat, valueFormatOption: ValueFormatOption, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AnyCodable {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let actualFocusedElement = focusedElement else { return AnyCodable(nil as String?) }
if outputFormat == .text_content {
return AnyCodable("Element: \(actualFocusedElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "?Role")")
} else {
return AnyCodable(actualFocusedElement.briefDescription(option: valueFormatOption, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))
}
}
/// Encodes the given ElementAttributes dictionary into a new dictionary containing
/// a single key "json_representation" with the JSON string as its value.
/// If encoding fails, returns a dictionary with an error message.
@MainActor
public func encodeAttributesToJSONStringRepresentation(_ attributes: ElementAttributes) -> ElementAttributes {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // Or .sortedKeys for deterministic output if needed
do {
let jsonData = try encoder.encode(attributes) // attributes is [String: AnyCodable]
if let jsonString = String(data: jsonData, encoding: .utf8) {
return ["json_representation": AnyCodable(jsonString)]
} else {
return ["error": AnyCodable("Failed to convert encoded JSON data to string")]
}
} catch {
return ["error": AnyCodable("Failed to encode attributes to JSON: \(error.localizedDescription)")]
}
}
// MARK: - Computed Attributes
// New helper function to get only computed/heuristic attributes for matching
@MainActor
public func getComputedAttributes(for element: Element, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> ElementAttributes {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
var attributes: ElementAttributes = [:]
tempLogs.removeAll()
dLog("getComputedAttributes for element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs))")
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
if let name = element.computedName(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
attributes[computedNameAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(name), source: .computed))
dLog("ComputedName: \(name)")
}
currentDebugLogs.append(contentsOf: tempLogs)
tempLogs.removeAll()
let isButton = (element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) == kAXButtonRole)
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from role call
tempLogs.removeAll()
let hasPressAction = element.isActionSupported(kAXPressAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from isActionSupported call
if isButton || hasPressAction {
attributes[isClickableAttributeKey] = AnyCodable(AttributeData(value: AnyCodable(true), source: .computed))
dLog("IsClickable: true (button: \(isButton), pressAction: \(hasPressAction))")
}
// Ensure other computed attributes like ComputedPath also use methods with logging if they exist.
// For now, this focuses on the direct errors.
return attributes
}
// MARK: - Attribute Formatting Helpers (Additional)
// Helper function to format a raw CFTypeRef for .text_content output
@MainActor
private func formatRawCFValueForTextContent(_ rawValue: CFTypeRef?) -> String {
guard let value = rawValue else { return kAXNotAvailableString }
let typeID = CFGetTypeID(value)
if typeID == CFStringGetTypeID() { return (value as! String) }
else if typeID == CFAttributedStringGetTypeID() { return (value as! NSAttributedString).string }
else if typeID == AXValueGetTypeID() {
let axVal = value as! AXValue
return formatAXValue(axVal, option: .default) // Assumes formatAXValue returns String
} else if typeID == CFNumberGetTypeID() { return (value as! NSNumber).stringValue }
else if typeID == CFBooleanGetTypeID() { return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false" }
else { return "<\(CFCopyTypeIDDescription(typeID) as String? ?? "ComplexType")>" }
}
// Any other attribute-specific helper functions could go here in the future.

View File

@ -0,0 +1,173 @@
import Foundation
import ApplicationServices // For AXUIElement, CFTypeRef etc.
// debug() is assumed to be globally available from Logging.swift
// DEBUG_LOGGING_ENABLED is a global public var from Logging.swift
@MainActor
internal func attributesMatch(element: Element, matchDetails: [String: String], depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
let criteriaDesc = matchDetails.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
let roleForLog = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil"
let titleForLog = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil"
dLog("attributesMatch [D\(depth)]: Check. Role=\(roleForLog), Title=\(titleForLog). Criteria: [\(criteriaDesc)]")
if !matchComputedNameAttributes(element: element, computedNameEquals: matchDetails[computedNameAttributeKey + "_equals"], computedNameContains: matchDetails[computedNameAttributeKey + "_contains"], depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return false
}
for (key, expectedValue) in matchDetails {
if key == computedNameAttributeKey + "_equals" || key == computedNameAttributeKey + "_contains" { continue }
if key == kAXRoleAttribute { continue } // Already handled by ElementSearch's role check or not a primary filter here
if key == kAXEnabledAttribute || key == kAXFocusedAttribute || key == kAXHiddenAttribute || key == kAXElementBusyAttribute || key == isIgnoredAttributeKey || key == kAXMainAttribute {
if !matchBooleanAttribute(element: element, key: key, expectedValueString: expectedValue, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return false
}
continue
}
if key == kAXActionNamesAttribute || key == kAXAllowedValuesAttribute || key == kAXChildrenAttribute {
if !matchArrayAttribute(element: element, key: key, expectedValueString: expectedValue, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return false
}
continue
}
if !matchStringAttribute(element: element, key: key, expectedValueString: expectedValue, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return false
}
}
dLog("attributesMatch [D\(depth)]: All attributes MATCHED criteria.")
return true
}
@MainActor
internal func matchStringAttribute(element: Element, key: String, expectedValueString: String, depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if let currentValue = element.attribute(Attribute<String>(key), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
if currentValue != expectedValueString {
dLog("attributesMatch [D\(depth)]: Attribute '\(key)' expected '\(expectedValueString)', but found '\(currentValue)'. No match.")
return false
}
return true
} else {
if expectedValueString.lowercased() == "nil" || expectedValueString == kAXNotAvailableString || expectedValueString.isEmpty {
dLog("attributesMatch [D\(depth)]: Attribute '\(key)' not found, but expected value ('\(expectedValueString)') suggests absence is OK. Match for this key.")
return true
} else {
dLog("attributesMatch [D\(depth)]: Attribute '\(key)' (expected '\(expectedValueString)') not found or not convertible to String. No match.")
return false
}
}
}
@MainActor
internal func matchArrayAttribute(element: Element, key: String, expectedValueString: String, depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
guard let expectedArray = decodeExpectedArray(fromString: expectedValueString, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
dLog("matchArrayAttribute [D\(depth)]: Could not decode expected array string '\(expectedValueString)' for attribute '\(key)'. No match.")
return false
}
var actualArray: [String]? = nil
if key == kAXActionNamesAttribute {
actualArray = element.supportedActions(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
} else if key == kAXAllowedValuesAttribute {
actualArray = element.attribute(Attribute<[String]>(key), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
} else if key == kAXChildrenAttribute {
actualArray = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)?.map { childElement -> String in
var childLogs: [String] = []
return childElement.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &childLogs) ?? "UnknownRole"
}
} else {
dLog("matchArrayAttribute [D\(depth)]: Unknown array key '\(key)'. This function needs to be extended for this key.")
return false
}
if let actual = actualArray {
if Set(actual) != Set(expectedArray) {
dLog("matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' expected '\(expectedArray)', but found '\(actual)'. Sets differ. No match.")
return false
}
return true
} else {
if expectedArray.isEmpty {
dLog("matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' not found, but expected array was empty. Match for this key.")
return true
}
dLog("matchArrayAttribute [D\(depth)]: Array Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match.")
return false
}
}
@MainActor
internal func matchBooleanAttribute(element: Element, key: String, expectedValueString: String, depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
var currentBoolValue: Bool?
switch key {
case kAXEnabledAttribute: currentBoolValue = element.isEnabled(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXFocusedAttribute: currentBoolValue = element.isFocused(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXHiddenAttribute: currentBoolValue = element.isHidden(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXElementBusyAttribute: currentBoolValue = element.isElementBusy(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case isIgnoredAttributeKey: currentBoolValue = element.isIgnored(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
case kAXMainAttribute: currentBoolValue = element.attribute(Attribute<Bool>(key), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
default:
dLog("matchBooleanAttribute [D\(depth)]: Unknown boolean key '\(key)'. This should not happen.")
return false
}
if let actualBool = currentBoolValue {
let expectedBool = expectedValueString.lowercased() == "true"
if actualBool != expectedBool {
dLog("attributesMatch [D\(depth)]: Boolean Attribute '\(key)' expected '\(expectedBool)', but found '\(actualBool)'. No match.")
return false
}
return true
} else {
dLog("attributesMatch [D\(depth)]: Boolean Attribute '\(key)' (expected '\(expectedValueString)') not found in element. No match.")
return false
}
}
@MainActor
internal func matchComputedNameAttributes(element: Element, computedNameEquals: String?, computedNameContains: String?, depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For Element method calls
if computedNameEquals == nil && computedNameContains == nil {
return true
}
// getComputedAttributes will need logging parameters
let computedAttrs = getComputedAttributes(for: element, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if let currentComputedNameAny = computedAttrs[computedNameAttributeKey]?.value, // Assuming .value is how you get it from the AttributeData struct
let currentComputedName = currentComputedNameAny as? String {
if let equals = computedNameEquals {
if currentComputedName != equals {
dLog("matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' != '\(equals)'. No match.")
return false
}
}
if let contains = computedNameContains {
if !currentComputedName.localizedCaseInsensitiveContains(contains) {
dLog("matchComputedNameAttributes [D\(depth)]: ComputedName '\(currentComputedName)' does not contain '\(contains)'. No match.")
return false
}
}
return true
} else {
dLog("matchComputedNameAttributes [D\(depth)]: Locator requires ComputedName (equals: \(computedNameEquals ?? "nil"), contains: \(computedNameContains ?? "nil")), but element has none. No match.")
return false
}
}

View File

@ -0,0 +1,200 @@
// ElementSearch.swift - Contains search and element collection logic
import Foundation
import ApplicationServices
// Variable DEBUG_LOGGING_ENABLED is expected to be globally available from Logging.swift
// Element is now the primary type for UI elements.
// decodeExpectedArray MOVED to Utils/GeneralParsingUtils.swift
enum ElementMatchStatus {
case fullMatch // Role, attributes, and (if specified) action all match
case partialMatch_actionMissing // Role and attributes match, but a required action is missing
case noMatch // Role or attributes do not match
}
@MainActor
internal func evaluateElementAgainstCriteria(element: Element, locator: Locator, actionToVerify: String?, depth: Int, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> ElementMatchStatus {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For calls to Element methods that need their own log scope temporarily
let currentElementRoleForLog: String? = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
let wantedRoleFromCriteria = locator.criteria[kAXRoleAttribute]
var roleMatchesCriteria = false
if let currentRole = currentElementRoleForLog, let roleToMatch = wantedRoleFromCriteria, !roleToMatch.isEmpty, roleToMatch != "*" {
roleMatchesCriteria = (currentRole == roleToMatch)
} else {
roleMatchesCriteria = true // Wildcard/empty/nil role in criteria is a match
let wantedRoleStr = wantedRoleFromCriteria ?? "any"
let currentRoleStr = currentElementRoleForLog ?? "nil"
dLog("evaluateElementAgainstCriteria [D\(depth)]: Wildcard/empty/nil role in criteria ('\(wantedRoleStr)') considered a match for element role \(currentRoleStr).")
}
if !roleMatchesCriteria {
dLog("evaluateElementAgainstCriteria [D\(depth)]: Role mismatch. Element role: \(currentElementRoleForLog ?? "nil"), Expected: \(wantedRoleFromCriteria ?? "any"). No match.")
return .noMatch
}
// Role matches, now check other attributes
// attributesMatch will also need isDebugLoggingEnabled, currentDebugLogs
if !attributesMatch(element: element, matchDetails: locator.criteria, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
// attributesMatch itself will log the specific mismatch reason
dLog("evaluateElementAgainstCriteria [D\(depth)]: attributesMatch returned false. No match.")
return .noMatch
}
// Role and attributes match. Now check for required action.
if let requiredAction = actionToVerify, !requiredAction.isEmpty {
if !element.isActionSupported(requiredAction, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) {
dLog("evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched, but required action '\(requiredAction)' is MISSING.")
return .partialMatch_actionMissing
}
dLog("evaluateElementAgainstCriteria [D\(depth)]: Role, Attributes, and Required Action '\(requiredAction)' all MATCH.")
} else {
dLog("evaluateElementAgainstCriteria [D\(depth)]: Role & Attributes matched. No action to verify or action already included in locator.criteria for attributesMatch.")
}
return .fullMatch
}
@MainActor
public func search(element: Element,
locator: Locator,
requireAction: String?,
depth: Int = 0,
maxDepth: Int = DEFAULT_MAX_DEPTH_SEARCH,
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String]) -> Element? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For calls to Element methods
let criteriaDesc = locator.criteria.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
let roleStr = element.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "nil"
let titleStr = element.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? "N/A"
dLog("search [D\(depth)]: Visiting. Role: \(roleStr), Title: \(titleStr). Locator Criteria: [\(criteriaDesc)], Action: \(requireAction ?? "none")")
if depth > maxDepth {
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
dLog("search [D\(depth)]: Max depth \(maxDepth) reached for element \(briefDesc).")
return nil
}
let matchStatus = evaluateElementAgainstCriteria(element: element,
locator: locator,
actionToVerify: requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) // Pass through logs
if matchStatus == .fullMatch {
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
dLog("search [D\(depth)]: evaluateElementAgainstCriteria returned .fullMatch for \(briefDesc). Returning element.")
return element
}
let briefDesc = element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if matchStatus == .partialMatch_actionMissing {
dLog("search [D\(depth)]: Element \(briefDesc) matched criteria but missed action '\(requireAction ?? "")'. Continuing child search.")
}
if matchStatus == .noMatch {
dLog("search [D\(depth)]: Element \(briefDesc) did not match criteria. Continuing child search.")
}
let childrenToSearch: [Element] = element.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
if !childrenToSearch.isEmpty {
for childElement in childrenToSearch {
if let found = search(element: childElement, locator: locator, requireAction: requireAction, depth: depth + 1, maxDepth: maxDepth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) {
return found
}
}
}
return nil
}
@MainActor
public func collectAll(
appElement: Element,
locator: Locator,
currentElement: Element,
depth: Int,
maxDepth: Int,
maxElements: Int,
currentPath: [Element],
elementsBeingProcessed: inout Set<Element>,
foundElements: inout [Element],
isDebugLoggingEnabled: Bool,
currentDebugLogs: inout [String] // Added logging parameter
) {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var tempLogs: [String] = [] // For calls to Element methods
let briefDescCurrent = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs)
if elementsBeingProcessed.contains(currentElement) || currentPath.contains(currentElement) {
dLog("collectAll [D\(depth)]: Cycle detected or element \(briefDescCurrent) already processed/in path.")
return
}
elementsBeingProcessed.insert(currentElement)
if foundElements.count >= maxElements {
dLog("collectAll [D\(depth)]: Max elements limit of \(maxElements) reached before processing \(briefDescCurrent).")
elementsBeingProcessed.remove(currentElement)
return
}
if depth > maxDepth {
dLog("collectAll [D\(depth)]: Max depth \(maxDepth) reached for \(briefDescCurrent).")
elementsBeingProcessed.remove(currentElement)
return
}
let criteriaDesc = locator.criteria.map { "\($0.key)=\($0.value)" }.joined(separator: ", ")
dLog("collectAll [D\(depth)]: Visiting \(briefDescCurrent). Criteria: [\(criteriaDesc)], Action: \(locator.requireAction ?? "none")")
let matchStatus = evaluateElementAgainstCriteria(element: currentElement,
locator: locator,
actionToVerify: locator.requireAction,
depth: depth,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs) // Pass through logs
if matchStatus == .fullMatch {
if foundElements.count < maxElements {
if !foundElements.contains(currentElement) {
foundElements.append(currentElement)
dLog("collectAll [D\(depth)]: Added \(briefDescCurrent). Hits: \(foundElements.count)/\(maxElements)")
} else {
dLog("collectAll [D\(depth)]: Element \(briefDescCurrent) was a full match but already in foundElements.")
}
} else {
dLog("collectAll [D\(depth)]: Element \(briefDescCurrent) was a full match but maxElements (\(maxElements)) already reached.")
}
}
let childrenToExplore: [Element] = currentElement.children(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs) ?? []
elementsBeingProcessed.remove(currentElement)
let newPath = currentPath + [currentElement]
for child in childrenToExplore {
if foundElements.count >= maxElements {
dLog("collectAll [D\(depth)]: Max elements (\(maxElements)) reached during child traversal of \(briefDescCurrent). Stopping further exploration for this branch.")
break
}
collectAll(
appElement: appElement,
locator: locator,
currentElement: child,
depth: depth + 1,
maxDepth: maxDepth,
maxElements: maxElements,
currentPath: newPath,
elementsBeingProcessed: &elementsBeingProcessed,
foundElements: &foundElements,
isDebugLoggingEnabled: isDebugLoggingEnabled,
currentDebugLogs: &currentDebugLogs // Pass through logs
)
}
}

View File

@ -0,0 +1,81 @@
// PathUtils.swift - Utilities for parsing paths and navigating element hierarchies.
import Foundation
import ApplicationServices // For Element, AXUIElement and kAX...Attribute constants
// Assumes Element is defined (likely via AXSwift an extension or typealias)
// debug() is assumed to be globally available from Logging.swift
// axValue<T>() is assumed to be globally available from ValueHelpers.swift
// kAXWindowRole, kAXWindowsAttribute, kAXChildrenAttribute, kAXRoleAttribute from AccessibilityConstants.swift
public func parsePathComponent(_ path: String) -> (role: String, index: Int)? {
let pattern = #"(\w+)\[(\d+)\]"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
let range = NSRange(path.startIndex..<path.endIndex, in: path)
guard let match = regex.firstMatch(in: path, range: range) else { return nil }
let role = (path as NSString).substring(with: match.range(at: 1))
guard let index = Int((path as NSString).substring(with: match.range(at: 2))) else { return nil }
return (role: role, index: index - 1) // Return 0-based index
}
@MainActor
public func navigateToElement(from rootElement: Element, pathHint: [String], isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Element? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
var currentElement = rootElement
for pathComponent in pathHint {
guard let (role, index) = parsePathComponent(pathComponent) else {
dLog("Failed to parse path component: \(pathComponent)")
return nil
}
var tempBriefDescLogs: [String] = [] // Placeholder for briefDescription logs
if role.lowercased() == "window" || role.lowercased() == kAXWindowRole.lowercased() {
guard let windowUIElements: [AXUIElement] = axValue(of: currentElement.underlyingElement, attr: kAXWindowsAttribute, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
dLog("PathUtils: AXWindows attribute could not be fetched as [AXUIElement].")
return nil
}
dLog("PathUtils: Fetched \(windowUIElements.count) AXUIElements for AXWindows.")
let windows: [Element] = windowUIElements.map { Element($0) }
dLog("PathUtils: Mapped to \(windows.count) Elements.")
guard index < windows.count else {
dLog("PathUtils: Index \(index) is out of bounds for windows array (count: \(windows.count)). Component: \(pathComponent).")
return nil
}
currentElement = windows[index]
} else {
let currentElementDesc = currentElement.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempBriefDescLogs) // Placeholder call
guard let allChildrenUIElements: [AXUIElement] = axValue(of: currentElement.underlyingElement, attr: kAXChildrenAttribute, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
dLog("PathUtils: AXChildren attribute could not be fetched as [AXUIElement] for element \(currentElementDesc) while processing \(pathComponent).")
return nil
}
dLog("PathUtils: Fetched \(allChildrenUIElements.count) AXUIElements for AXChildren of \(currentElementDesc) for \(pathComponent).")
let allChildren: [Element] = allChildrenUIElements.map { Element($0) }
dLog("PathUtils: Mapped to \(allChildren.count) Elements for children of \(currentElementDesc) for \(pathComponent).")
guard !allChildren.isEmpty else {
dLog("No children found for element \(currentElementDesc) while processing component: \(pathComponent)")
return nil
}
let matchingChildren = allChildren.filter {
guard let childRole: String = axValue(of: $0.underlyingElement, attr: kAXRoleAttribute, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else { return false }
return childRole.lowercased() == role.lowercased()
}
guard index < matchingChildren.count else {
dLog("Child not found for component: \(pathComponent) at index \(index). Role: \(role). For element \(currentElementDesc). Matching children count: \(matchingChildren.count)")
return nil
}
currentElement = matchingChildren[index]
}
}
return currentElement
}

View File

@ -0,0 +1,42 @@
import Foundation
// CustomCharacterSet struct from Scanner
public struct CustomCharacterSet {
private var characters: Set<Character>
public init(characters: Set<Character>) {
self.characters = characters
}
public init(charactersInString: String) {
self.characters = Set(charactersInString.map { $0 })
}
public func contains(_ character: Character) -> Bool {
return self.characters.contains(character)
}
public mutating func add(_ characters: Set<Character>) {
self.characters.formUnion(characters)
}
public func adding(_ characters: Set<Character>) -> CustomCharacterSet {
return CustomCharacterSet(characters: self.characters.union(characters))
}
public mutating func remove(_ characters: Set<Character>) {
self.characters.subtract(characters)
}
public func removing(_ characters: Set<Character>) -> CustomCharacterSet {
return CustomCharacterSet(characters: self.characters.subtracting(characters))
}
// Add some common character sets that might be useful, similar to Foundation.CharacterSet
public static var whitespacesAndNewlines: CustomCharacterSet {
return CustomCharacterSet(charactersInString: " \t\n\r")
}
public static var decimalDigits: CustomCharacterSet {
return CustomCharacterSet(charactersInString: "0123456789")
}
public static func punctuationAndSymbols() -> CustomCharacterSet { // Example
// This would need a more comprehensive list based on actual needs
return CustomCharacterSet(charactersInString: ".,:;?!()[]{}-_=+") // Simplified set
}
public static func characters(in string: String) -> CustomCharacterSet {
return CustomCharacterSet(charactersInString: string)
}
}

View File

@ -0,0 +1,84 @@
// GeneralParsingUtils.swift - General parsing utilities
import Foundation
// TODO: Consider if this should be public or internal depending on usage across modules if this were a larger project.
// For AXHelper, internal or public within the module is fine.
/// Decodes a string representation of an array into an array of strings.
/// The input string can be JSON-style (e.g., "["item1", "item2"]")
/// or a simple comma-separated list (e.g., "item1, item2", with or without brackets).
public func decodeExpectedArray(fromString: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> [String]? {
// This function itself does not log, but takes the parameters as it's called by functions that do.
// func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
let trimmedString = fromString.trimmingCharacters(in: .whitespacesAndNewlines)
// Try JSON deserialization first for robustness with escaped characters, etc.
if trimmedString.hasPrefix("[") && trimmedString.hasSuffix("]") {
if let jsonData = trimmedString.data(using: .utf8) {
do {
// Attempt to decode as [String]
if let array = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String] {
return array
}
// Fallback: if it decodes as [Any], convert elements to String
else if let anyArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [Any] {
return anyArray.compactMap { item -> String? in
if let strItem = item as? String {
return strItem
} else {
// For non-string items, convert to string representation
// This handles numbers, booleans if they were in the JSON array
return String(describing: item)
}
}
}
} catch {
// dLog("JSON decoding failed for string: \(trimmedString). Error: \(error.localizedDescription)")
}
}
}
// Fallback to comma-separated parsing if JSON fails or string isn't JSON-like
// Remove brackets first if they exist for comma parsing
var stringToSplit = trimmedString
if stringToSplit.hasPrefix("[") && stringToSplit.hasSuffix("]") {
stringToSplit = String(stringToSplit.dropFirst().dropLast())
}
// If the string (after removing brackets) is empty, it represents an empty array.
if stringToSplit.isEmpty && trimmedString.hasPrefix("[") && trimmedString.hasSuffix("]") {
return []
}
// If the original string was just "[]" or "", and after stripping it's empty, it's an empty array.
// If it was empty to begin with, or just spaces, it's not a valid array string by this func's def.
if stringToSplit.isEmpty && !trimmedString.isEmpty && !(trimmedString.hasPrefix("[") && trimmedString.hasSuffix("]")) {
// e.g. input was " " which became "", not a valid array representation
// or input was "item" which is not an array string
// However, if original was "[]", stringToSplit is empty, should return []
// If original was "", stringToSplit is empty, should return nil (or based on stricter needs)
// This function is lenient: if after stripping brackets it's empty, it's an empty array.
// If the original was non-empty but not bracketed, and became empty after trimming, it's not an array.
}
// Handle case where stringToSplit might be empty, meaning an empty array if brackets were present.
if stringToSplit.isEmpty {
// If original string was "[]", then stringToSplit is empty, return []
// If original was "", then stringToSplit is empty, return nil (not an array format)
return (trimmedString.hasPrefix("[") && trimmedString.hasSuffix("]")) ? [] : nil
}
return stringToSplit.components(separatedBy: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
// Do not filter out empty strings if they are explicitly part of the list e.g. "a,,b"
// The original did .filter { !$0.isEmpty }, which might be too aggressive.
// For now, let's keep all components and let caller decide if empty strings are valid.
// Re-evaluating: if a component is empty after trimming, it usually means an empty element.
// Example: "[a, ,b]" -> ["a", "", "b"]. Example "a," -> ["a", ""].
// The original .filter { !$0.isEmpty } would turn "a,," into ["a"]
// Let's retain the original filtering of completely empty strings after trim,
// as "[a,,b]" usually implies "[a,b]" in lenient contexts.
// If explicit empty strings like `["a", "", "b"]` are needed, JSON is better.
.filter { !$0.isEmpty }
}

View File

@ -0,0 +1,323 @@
// Scanner.swift - Custom scanner implementation (Scanner)
import Foundation
// String extension MOVED to String+HelperExtensions.swift
// CustomCharacterSet struct MOVED to CustomCharacterSet.swift
// Scanner class from Scanner
class Scanner {
// MARK: - Properties and Initialization
let string: String
var location: Int = 0
init(string: String) {
self.string = string
}
var isAtEnd: Bool {
return self.location >= self.string.count
}
// MARK: - Character Set Scanning
// A more conventional scanUpTo (scans until a character in the set is found)
@discardableResult func scanUpToCharacters(in charSet: CustomCharacterSet) -> String? {
let initialLocation = self.location
var scannedCharacters = String()
while self.location < self.string.count {
let currentChar = self.string[self.location]
if charSet.contains(currentChar) { break }
scannedCharacters.append(currentChar)
self.location += 1
}
return scannedCharacters.isEmpty && self.location == initialLocation ? nil : scannedCharacters
}
// Scans characters that ARE in the provided set (like original Scanner's scanUpTo/scan(characterSet:))
@discardableResult func scanCharacters(in charSet: CustomCharacterSet) -> String? {
let initialLocation = self.location
var characters = String()
while self.location < self.string.count, charSet.contains(self.string[self.location]) {
characters.append(self.string[self.location])
self.location += 1
}
if characters.isEmpty {
self.location = initialLocation // Revert if nothing was scanned
return nil
}
return characters
}
@discardableResult func scan(characterSet: CustomCharacterSet) -> Character? {
guard self.location < self.string.count else { return nil }
let character = self.string[self.location]
guard characterSet.contains(character) else { return nil }
self.location += 1
return character
}
@discardableResult func scan(characterSet: CustomCharacterSet) -> String? {
var characters = String()
while let character: Character = self.scan(characterSet: characterSet) {
characters.append(character)
}
return characters.isEmpty ? nil : characters
}
// MARK: - Specific Character and String Scanning
@discardableResult func scan(character: Character, options: NSString.CompareOptions = []) -> Character? {
guard self.location < self.string.count else { return nil }
let characterString = String(character)
if characterString.compare(String(self.string[self.location]), options: options, range: nil, locale: nil) == .orderedSame {
self.location += 1
return character
}
return nil
}
@discardableResult func scan(string: String, options: NSString.CompareOptions = []) -> String? {
let savepoint = self.location
var characters = String()
for character in string {
if let charScanned = self.scan(character: character, options: options) {
characters.append(charScanned)
} else {
self.location = savepoint // Revert on failure
return nil
}
}
// If we scanned the whole string, it's a match.
return characters.count == string.count ? characters : { self.location = savepoint; return nil }()
}
func scan(token: String, options: NSString.CompareOptions = []) -> String? {
self.scanWhitespaces()
return self.scan(string: token, options: options)
}
func scan(strings: [String], options: NSString.CompareOptions = []) -> String? {
for stringEntry in strings {
if let scannedString = self.scan(string: stringEntry, options: options) {
return scannedString
}
}
return nil
}
func scan(tokens: [String], options: NSString.CompareOptions = []) -> String? {
self.scanWhitespaces()
return self.scan(strings: tokens, options: options)
}
// MARK: - Integer Scanning
func scanSign() -> Int? {
return self.scan(dictionary: ["+": 1, "-": -1])
}
// Private helper that scans and returns a string of digits
private func scanDigits() -> String? {
return self.scanCharacters(in: .decimalDigits)
}
// Calculate integer value from digit string with given base
private func integerValue<T: BinaryInteger>(from digitString: String, base: T = 10) -> T {
return digitString.reduce(T(0)) { result, char in
result * base + T(Int(String(char))!)
}
}
func scanUnsignedInteger<T: UnsignedInteger>() -> T? {
self.scanWhitespaces()
guard let digitString = self.scanDigits() else { return nil }
return integerValue(from: digitString)
}
func scanInteger<T: SignedInteger>() -> T? {
let savepoint = self.location
self.scanWhitespaces()
// Parse sign if present
let sign = self.scanSign() ?? 1
// Parse digits
guard let digitString = self.scanDigits() else {
// If we found a sign but no digits, revert and return nil
if sign != 1 {
self.location = savepoint
}
return nil
}
// Calculate final value with sign applied
return T(sign) * integerValue(from: digitString)
}
// MARK: - Floating Point Scanning
// Attempt to parse Double with a compact implementation
func scanDouble() -> Double? {
scanWhitespaces()
let initialLocation = self.location
// Parse sign
let sign: Double = (scan(character: "-") != nil) ? -1.0 : { _ = scan(character: "+"); return 1.0 }()
// Buffer to build the numeric string
var numberStr = ""
var hasDigits = false
// Parse integer part
if let digits = scanCharacters(in: .decimalDigits) {
numberStr += digits
hasDigits = true
}
// Parse fractional part
let dotLocation = location
if scan(character: ".") != nil {
if let fractionDigits = scanCharacters(in: .decimalDigits) {
numberStr += "."
numberStr += fractionDigits
hasDigits = true
} else {
// Revert dot scan if not followed by digits
location = dotLocation
}
}
// If no digits found in either integer or fractional part, revert and return nil
if !hasDigits {
location = initialLocation
return nil
}
// Parse exponent
var exponent = 0
let expLocation = location
if scan(character: "e", options: .caseInsensitive) != nil {
let expSign: Double = (scan(character: "-") != nil) ? -1.0 : { _ = scan(character: "+"); return 1.0 }()
if let expDigits = scanCharacters(in: .decimalDigits), let expValue = Int(expDigits) {
exponent = Int(expSign) * expValue
} else {
// Revert exponent scan if not followed by valid digits
location = expLocation
}
}
// Convert to final double value
if var value = Double(numberStr) {
value *= sign
if exponent != 0 {
value *= pow(10.0, Double(exponent))
}
return value
}
// If conversion fails, revert everything
location = initialLocation
return nil
}
// Mapping hex characters to their integer values
private static let hexValues: [Character: Int] = [
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9,
"a": 10, "b": 11, "c": 12, "d": 13, "e": 14, "f": 15,
"A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
]
func scanHexadecimalInteger<T: UnsignedInteger>() -> T? {
let initialLoc = location
let hexCharSet = CustomCharacterSet(charactersInString: Self.characterSets.hexDigits)
var value: T = 0
var digitCount = 0
while let char: Character = scan(characterSet: hexCharSet),
let digit = Self.hexValues[char] {
value = value * 16 + T(digit)
digitCount += 1
}
if digitCount == 0 {
location = initialLoc // Revert if nothing was scanned
return nil
}
return value
}
// Helper function for power calculation with FloatingPoint types
private func scannerPower<T: FloatingPoint>(base: T, exponent: Int) -> T {
if exponent == 0 { return T(1) }
if exponent < 0 { return T(1) / scannerPower(base: base, exponent: -exponent) }
var result = T(1)
for _ in 0..<exponent {
result *= base
}
return result
}
// MARK: - Identifier Scanning
// Character sets for identifier scanning
static private let characterSets = (
lowercaseLetters: "abcdefghijklmnopqrstuvwxyz",
uppercaseLetters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
hexDigits: "0123456789abcdefABCDEF"
)
static var identifierFirstCharSet: CustomCharacterSet {
CustomCharacterSet(charactersInString: characterSets.lowercaseLetters + characterSets.uppercaseLetters + "_")
}
static var identifierFollowingCharSet: CustomCharacterSet {
CustomCharacterSet(charactersInString: characterSets.lowercaseLetters + characterSets.uppercaseLetters + characterSets.digits + "_")
}
func scanIdentifier() -> String? {
scanWhitespaces()
let savepoint = location
// Scan first character (must be letter or underscore)
guard let firstChar: Character = scan(characterSet: Self.identifierFirstCharSet) else {
location = savepoint
return nil
}
// Begin with the first character
var identifier = String(firstChar)
// Scan remaining characters (can include digits)
while let nextChar: Character = scan(characterSet: Self.identifierFollowingCharSet) {
identifier.append(nextChar)
}
return identifier
}
// MARK: - Whitespace Scanning
func scanWhitespaces() {
_ = self.scanCharacters(in: .whitespacesAndNewlines)
}
// MARK: - Dictionary-based Scanning
func scan<T>(dictionary: [String: T], options: NSString.CompareOptions = []) -> T? {
for (key, value) in dictionary {
if self.scan(string: key, options: options) != nil {
// Original Scanner asserts string == key, which is true if scan(string:) returns non-nil.
return value
}
}
return nil
}
// Helper to get the remaining string
var remainingString: String {
if isAtEnd { return "" }
let startIndex = string.index(string.startIndex, offsetBy: location)
return String(string[startIndex...])
}
}

View File

@ -0,0 +1,31 @@
import Foundation
// String extension from Scanner
extension String {
subscript (i: Int) -> Character {
return self[index(startIndex, offsetBy: i)]
}
func range(from range: NSRange) -> Range<String.Index>? {
return Range(range, in: self)
}
func range(from range: Range<String.Index>) -> NSRange {
return NSRange(range, in: self)
}
var firstLine: String? {
var line: String?
self.enumerateLines {
line = $0
$1 = true
}
return line
}
}
extension Optional {
var orNilString: String {
switch self {
case .some(let value): return "\(value)"
case .none: return "nil"
}
}
}

View File

@ -0,0 +1,42 @@
// TextExtraction.swift - Utilities for extracting textual content from Elements.
import Foundation
import ApplicationServices // For Element and kAX...Attribute constants
// Assumes Element is defined and has an `attribute(String) -> String?` method.
// Constants like kAXValueAttribute are expected to be available (e.g., from AccessibilityConstants.swift)
// axValue<T>() is assumed to be globally available from ValueHelpers.swift
@MainActor
public func extractTextContent(element: Element, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
dLog("Extracting text content for element: \(element.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))")
var texts: [String] = []
let textualAttributes = [
kAXValueAttribute, kAXTitleAttribute, kAXDescriptionAttribute, kAXHelpAttribute,
kAXPlaceholderValueAttribute, kAXLabelValueAttribute, kAXRoleDescriptionAttribute,
// Consider adding kAXStringForRangeParameterizedAttribute if dealing with large text views for performance
// kAXSelectedTextAttribute could also be relevant depending on use case
]
for attrName in textualAttributes {
var tempLogs: [String] = [] // For the axValue call
// Pass the received logging parameters to axValue
if let strValue: String = axValue(of: element.underlyingElement, attr: attrName, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &tempLogs), !strValue.isEmpty, strValue.lowercased() != kAXNotAvailableString.lowercased() {
texts.append(strValue)
currentDebugLogs.append(contentsOf: tempLogs) // Collect logs from axValue
} else {
currentDebugLogs.append(contentsOf: tempLogs) // Still collect logs if value was nil/empty
}
}
// Deduplicate while preserving order
var uniqueTexts: [String] = []
var seenTexts = Set<String>()
for text in texts {
if !seenTexts.contains(text) {
uniqueTexts.append(text)
seenTexts.insert(text)
}
}
return uniqueTexts.joined(separator: "\n")
}

View File

@ -0,0 +1,44 @@
import Foundation
// MARK: - Scannable Protocol
protocol Scannable {
init?(_ scanner: Scanner)
}
// MARK: - Scannable Conformance
extension Int: Scannable {
init?(_ scanner: Scanner) {
if let value: Int = scanner.scanInteger() { self = value }
else { return nil }
}
}
extension UInt: Scannable {
init?(_ scanner: Scanner) {
if let value: UInt = scanner.scanUnsignedInteger() { self = value }
else { return nil }
}
}
extension Float: Scannable {
init?(_ scanner: Scanner) {
// Using the custom scanDouble and casting
if let value = scanner.scanDouble() { self = Float(value) }
else { return nil }
}
}
extension Double: Scannable {
init?(_ scanner: Scanner) {
if let value = scanner.scanDouble() { self = value }
else { return nil }
}
}
extension Bool: Scannable {
init?(_ scanner: Scanner) {
scanner.scanWhitespaces()
if let value: Bool = scanner.scan(dictionary: ["true": true, "false": false], options: [.caseInsensitive]) { self = value }
else { return nil }
}
}

View File

@ -0,0 +1,174 @@
// ValueFormatter.swift - Utilities for formatting AX values into human-readable strings
import Foundation
import ApplicationServices
import CoreGraphics // For CGPoint, CGSize, CGRect, CFRange
// debug() is assumed to be globally available from Logging.swift
// stringFromAXValueType() is assumed to be available from ValueHelpers.swift
// axErrorToString() is assumed to be available from AccessibilityConstants.swift
@MainActor
public enum ValueFormatOption {
case `default` // Concise, suitable for lists or brief views
case verbose // More detailed, suitable for focused inspection
}
@MainActor
public func formatAXValue(_ axValue: AXValue, option: ValueFormatOption = .default) -> String {
let type = AXValueGetType(axValue)
var result = "AXValue (\(stringFromAXValueType(type)))"
switch type {
case .cgPoint:
var point = CGPoint.zero
if AXValueGetValue(axValue, .cgPoint, &point) {
result = "x=\(point.x) y=\(point.y)"
if option == .verbose { result = "<CGPoint: \(result)>" }
}
case .cgSize:
var size = CGSize.zero
if AXValueGetValue(axValue, .cgSize, &size) {
result = "w=\(size.width) h=\(size.height)"
if option == .verbose { result = "<CGSize: \(result)>" }
}
case .cgRect:
var rect = CGRect.zero
if AXValueGetValue(axValue, .cgRect, &rect) {
result = "x=\(rect.origin.x) y=\(rect.origin.y) w=\(rect.size.width) h=\(rect.size.height)"
if option == .verbose { result = "<CGRect: \(result)>" }
}
case .cfRange:
var range = CFRange()
if AXValueGetValue(axValue, .cfRange, &range) {
result = "pos=\(range.location) len=\(range.length)"
if option == .verbose { result = "<CFRange: \(result)>" }
}
case .axError:
var error = AXError.success
if AXValueGetValue(axValue, .axError, &error) {
result = axErrorToString(error)
if option == .verbose { result = "<AXError: \(result)>" }
}
case .illegal:
result = "Illegal AXValue"
default:
// For boolean type (rawValue 4)
if type.rawValue == 4 {
var boolResult: DarwinBoolean = false
if AXValueGetValue(axValue, type, &boolResult) {
result = boolResult.boolValue ? "true" : "false"
if option == .verbose { result = "<Boolean: \(result)>"}
}
}
// Other types: return generic description.
// Consider if other specific AXValueTypes need custom formatting.
break
}
return result
}
// Helper to escape strings for display (e.g. in logs or formatted output that isn't strict JSON)
private func escapeStringForDisplay(_ input: String) -> String {
var escaped = input
// More comprehensive escaping might be needed depending on the exact output context
// For now, handle common cases for human-readable display.
escaped = escaped.replacingOccurrences(of: "\\", with: "\\\\") // Escape backslashes first
escaped = escaped.replacingOccurrences(of: "\"", with: "\\\"") // Escape double quotes
escaped = escaped.replacingOccurrences(of: "\n", with: "\\n") // Escape newlines
escaped = escaped.replacingOccurrences(of: "\t", with: "\\t") // Escape tabs
escaped = escaped.replacingOccurrences(of: "\r", with: "\\r") // Escape carriage returns
return escaped
}
@MainActor
// Update signature to accept logging parameters
public func formatCFTypeRef(_ cfValue: CFTypeRef?, option: ValueFormatOption = .default, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String {
guard let value = cfValue else { return "<nil>" }
let typeID = CFGetTypeID(value)
// var tempLogs: [String] = [] // Removed as it was unused
switch typeID {
case AXUIElementGetTypeID():
let element = Element(value as! AXUIElement)
// Pass the received logging parameters to briefDescription
return element.briefDescription(option: option, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
case AXValueGetTypeID():
return formatAXValue(value as! AXValue, option: option)
case CFStringGetTypeID():
return "\"\(escapeStringForDisplay(value as! String))\"" // Used helper
case CFAttributedStringGetTypeID():
return "\"\(escapeStringForDisplay((value as! NSAttributedString).string ))\"" // Used helper
case CFBooleanGetTypeID():
return CFBooleanGetValue((value as! CFBoolean)) ? "true" : "false"
case CFNumberGetTypeID():
return (value as! NSNumber).stringValue
case CFArrayGetTypeID():
let cfArray = value as! CFArray
let count = CFArrayGetCount(cfArray)
if option == .verbose || count <= 5 { // Show contents for small arrays or if verbose
var swiftArray: [String] = []
for i in 0..<count {
guard let elementPtr = CFArrayGetValueAtIndex(cfArray, i) else {
swiftArray.append("<nil_in_array>")
continue
}
// Pass logging parameters to recursive call
swiftArray.append(formatCFTypeRef(Unmanaged<CFTypeRef>.fromOpaque(elementPtr).takeUnretainedValue(), option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))
}
return "[\(swiftArray.joined(separator: ","))]"
} else {
return "<Array of size \(count)>"
}
case CFDictionaryGetTypeID():
let cfDict = value as! CFDictionary
let count = CFDictionaryGetCount(cfDict)
if option == .verbose || count <= 3 { // Show contents for small dicts or if verbose
var swiftDict: [String: String] = [:]
if let nsDict = cfDict as? [String: AnyObject] {
for (key, val) in nsDict {
// Pass logging parameters to recursive call
swiftDict[key] = formatCFTypeRef(val, option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
}
// Sort by key for consistent output
let sortedItems = swiftDict.sorted { $0.key < $1.key }
.map { "\"\(escapeStringForDisplay($0.key))\": \($0.value)" } // Used helper for key, value is already formatted
return "{\(sortedItems.joined(separator: ","))}"
} else {
return "<Dictionary (bridging failed), size \(count)>"
}
} else {
return "<Dictionary of size \(count)>"
}
case CFURLGetTypeID():
return (value as! URL).absoluteString
default:
let typeDescription = CFCopyTypeIDDescription(typeID) as String? ?? "Unknown CFType"
return "<CFType: \(typeDescription)>"
}
}
// Add a helper to Element for a brief description
extension Element {
@MainActor
// Now a method to accept logging parameters
public func briefDescription(option: ValueFormatOption = .default, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String {
// Call the new method versions of title, identifier, value, description, role
if let titleStr = self.title(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !titleStr.isEmpty {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr): \"\(escapeStringForDisplay(titleStr))\">"
}
else if let identifierStr = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !identifierStr.isEmpty {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr) id: \"\(escapeStringForDisplay(identifierStr))\">"
} else if let valueAny = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), let valueStr = valueAny as? String, !valueStr.isEmpty, valueStr.count < 50 {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr) val: \"\(escapeStringForDisplay(valueStr))\">"
} else if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs), !descStr.isEmpty, descStr.count < 50 {
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr) desc: \"\(escapeStringForDisplay(descStr))\">"
}
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) ?? "UnknownRole"
return "<\(roleStr)>"
}
}

View File

@ -0,0 +1,165 @@
import Foundation
import ApplicationServices
import CoreGraphics // For CGPoint, CGSize etc.
// debug() is assumed to be globally available from Logging.swift
// Constants like kAXPositionAttribute are assumed to be globally available from AccessibilityConstants.swift
// ValueUnwrapper has been moved to its own file: ValueUnwrapper.swift
// MARK: - Attribute Value Accessors
@MainActor
public func copyAttributeValue(element: AXUIElement, attribute: String) -> CFTypeRef? {
var value: CFTypeRef?
// This function is low-level, avoid extensive logging here unless specifically for this function.
// Logging for attribute success/failure is better handled by the caller (axValue).
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success else {
return nil
}
return value
}
@MainActor
public func axValue<T>(of element: AXUIElement, attr: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> T? {
func dLog(_ message: String) {
if isDebugLoggingEnabled {
currentDebugLogs.append(message)
}
}
// copyAttributeValue doesn't log, so no need to pass log params to it.
let rawCFValue = copyAttributeValue(element: element, attribute: attr)
// ValueUnwrapper.unwrap also needs to be audited for logging. For now, assume it doesn't log or its logs are separate.
let unwrappedValue = ValueUnwrapper.unwrap(rawCFValue, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
guard let value = unwrappedValue else {
// It's common for attributes to be missing or have no value.
// Only log if in debug mode and something was expected but not found,
// or if rawCFValue was non-nil but unwrapped to nil (which ValueUnwrapper might handle).
// For now, let's not log here, as Element.swift's rawAttributeValue also has checks.
return nil
}
if T.self == String.self {
if let str = value as? String { return str as? T }
else if let attrStr = value as? NSAttributedString { return attrStr.string as? T }
dLog("axValue: Expected String for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == Bool.self {
if let boolVal = value as? Bool { return boolVal as? T }
else if let numVal = value as? NSNumber { return numVal.boolValue as? T }
dLog("axValue: Expected Bool for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == Int.self {
if let intVal = value as? Int { return intVal as? T }
else if let numVal = value as? NSNumber { return numVal.intValue as? T }
dLog("axValue: Expected Int for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == Double.self {
if let doubleVal = value as? Double { return doubleVal as? T }
else if let numVal = value as? NSNumber { return numVal.doubleValue as? T }
dLog("axValue: Expected Double for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == [AXUIElement].self {
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> AXUIElement? in
guard let cfItem = item else { return nil }
// Ensure correct comparison for CFTypeRef type ID
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() { // Directly use AXUIElementGetTypeID()
return (cfItem as! AXUIElement)
}
return nil
}
return result as? T
}
dLog("axValue: Expected [AXUIElement] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == [Element].self { // Assuming Element is a struct wrapping AXUIElement
if let anyArray = value as? [Any?] {
let result = anyArray.compactMap { item -> Element? in
guard let cfItem = item else { return nil }
if CFGetTypeID(cfItem as CFTypeRef) == AXUIElementGetTypeID() { // Check underlying type
return Element(cfItem as! AXUIElement)
}
return nil
}
return result as? T
}
dLog("axValue: Expected [Element] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == [String].self {
if let stringArray = value as? [Any?] {
let result = stringArray.compactMap { $0 as? String }
// Ensure all elements were successfully cast, otherwise it's not a homogenous [String] array
if result.count == stringArray.count { return result as? T }
}
dLog("axValue: Expected [String] for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
// CGPoint and CGSize are expected to be directly unwrapped by ValueUnwrapper to these types.
if T.self == CGPoint.self {
if let pointVal = value as? CGPoint { return pointVal as? T }
dLog("axValue: Expected CGPoint for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == CGSize.self {
if let sizeVal = value as? CGSize { return sizeVal as? T }
dLog("axValue: Expected CGSize for attribute '\(attr)', but got \(type(of: value)): \(value)")
return nil
}
if T.self == AXUIElement.self {
if let cfValue = value as CFTypeRef?, CFGetTypeID(cfValue) == AXUIElementGetTypeID() {
return (cfValue as! AXUIElement) as? T
}
let typeDescription = String(describing: type(of: value))
let valueDescription = String(describing: value)
dLog("axValue: Expected AXUIElement for attribute '\(attr)', but got \(typeDescription): \(valueDescription)")
return nil
}
if let castedValue = value as? T {
return castedValue
}
dLog("axValue: Fallback cast attempt for attribute '\(attr)' to type \(T.self) FAILED. Unwrapped value was \(type(of: value)): \(value)")
return nil
}
// MARK: - AXValueType String Helper
public func stringFromAXValueType(_ type: AXValueType) -> String {
switch type {
case .cgPoint: return "CGPoint (kAXValueCGPointType)"
case .cgSize: return "CGSize (kAXValueCGSizeType)"
case .cgRect: return "CGRect (kAXValueCGRectType)"
case .cfRange: return "CFRange (kAXValueCFRangeType)"
case .axError: return "AXError (kAXValueAXErrorType)"
case .illegal: return "Illegal (kAXValueIllegalType)"
default:
// AXValueType is not exhaustive in Swift's AXValueType enum from ApplicationServices.
// Common missing ones include Boolean (4), Number (5), Array (6), Dictionary (7), String (8), URL (9), etc.
// We rely on ValueUnwrapper to handle these based on CFGetTypeID.
// This function is mostly for AXValue encoded types.
if type.rawValue == 4 { // kAXValueBooleanType is often 4 but not in the public enum
return "Boolean (rawValue 4, contextually kAXValueBooleanType)"
}
return "Unknown AXValueType (rawValue: \(type.rawValue))"
}
}

View File

@ -0,0 +1,236 @@
// AXValueParser.swift - Utilities for parsing string inputs into AX-compatible values
import Foundation
import ApplicationServices
import CoreGraphics // For CGPoint, CGSize, CGRect, CFRange
// debug() is assumed to be globally available from Logging.swift
// Constants are assumed to be globally available from AccessibilityConstants.swift
// Scanner and CustomCharacterSet are from Scanner.swift
// AccessibilityError is from AccessibilityError.swift
// Inspired by UIElementInspector's UIElementUtilities.m
// AXValueParseError enum has been removed and its cases merged into AccessibilityError.
@MainActor
public func getCFTypeIDForAttribute(element: Element, attributeName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> CFTypeID? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
guard let rawValue = element.rawAttributeValue(named: attributeName, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
dLog("getCFTypeIDForAttribute: Failed to get raw attribute value for '\(attributeName)'")
return nil
}
return CFGetTypeID(rawValue)
}
@MainActor
public func getAXValueTypeForAttribute(element: Element, attributeName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> AXValueType? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
guard let rawValue = element.rawAttributeValue(named: attributeName, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
dLog("getAXValueTypeForAttribute: Failed to get raw attribute value for '\(attributeName)'")
return nil
}
guard CFGetTypeID(rawValue) == AXValueGetTypeID() else {
dLog("getAXValueTypeForAttribute: Attribute '\(attributeName)' is not an AXValue. TypeID: \(CFGetTypeID(rawValue))")
return nil
}
let axValue = rawValue as! AXValue
return AXValueGetType(axValue)
}
// Main function to create CFTypeRef for setting an attribute
// It determines the type of the attribute and then calls the appropriate parser.
@MainActor
public func createCFTypeRefFromString(stringValue: String, forElement element: Element, attributeName: String, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> CFTypeRef? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
guard let currentRawValue = element.rawAttributeValue(named: attributeName, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) else {
throw AccessibilityError.attributeNotReadable("Could not read current value for attribute '\(attributeName)' to determine type.")
}
let typeID = CFGetTypeID(currentRawValue)
if typeID == AXValueGetTypeID() {
let axValue = currentRawValue as! AXValue
let axValueType = AXValueGetType(axValue)
dLog("Attribute '\(attributeName)' is AXValue of type: \(stringFromAXValueType(axValueType))")
return try parseStringToAXValue(stringValue: stringValue, targetAXValueType: axValueType, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs)
} else if typeID == CFStringGetTypeID() {
dLog("Attribute '\(attributeName)' is CFString. Returning stringValue as CFString.")
return stringValue as CFString
} else if typeID == CFNumberGetTypeID() {
dLog("Attribute '\(attributeName)' is CFNumber. Attempting to parse stringValue as Double then create CFNumber.")
if let doubleValue = Double(stringValue) {
return NSNumber(value: doubleValue) // CFNumber is toll-free bridged to NSNumber
} else if let intValue = Int(stringValue) {
return NSNumber(value: intValue)
} else {
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' as Double or Int for CFNumber attribute '\(attributeName)'")
}
} else if typeID == CFBooleanGetTypeID() {
dLog("Attribute '\(attributeName)' is CFBoolean. Attempting to parse stringValue as Bool.")
if stringValue.lowercased() == "true" {
return kCFBooleanTrue
} else if stringValue.lowercased() == "false" {
return kCFBooleanFalse
} else {
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' as Bool (true/false) for CFBoolean attribute '\(attributeName)'")
}
}
// TODO: Handle other CFTypeIDs like CFArray, CFDictionary if necessary for set-value.
// For now, focus on types directly convertible from string or AXValue structs.
let typeDescription = CFCopyTypeIDDescription(typeID) as String? ?? "Unknown CFType"
throw AccessibilityError.attributeUnsupported("Setting attribute '\(attributeName)' of CFTypeID \(typeID) (\(typeDescription)) from string is not supported yet.")
}
// Parses a string into an AXValue for struct types like CGPoint, CGSize, CGRect, CFRange
@MainActor
private func parseStringToAXValue(stringValue: String, targetAXValueType: AXValueType, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) throws -> AXValue? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
var valueRef: AXValue?
switch targetAXValueType {
case .cgPoint:
var x: Double = 0, y: Double = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 2,
let xValStr = components[0].split(separator: "=").last, let xVal = Double(xValStr),
let yValStr = components[1].split(separator: "=").last, let yVal = Double(yValStr) {
x = xVal; y = yVal
} else if components.count == 2, let xVal = Double(components[0]), let yVal = Double(components[1]) {
x = xVal; y = yVal
} else {
let scanner = Scanner(string: stringValue)
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xy:, \t\n"))
let xScanned = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xy:, \t\n"))
let yScanned = scanner.scanDouble()
if let xVal = xScanned, let yVal = yScanned {
x = xVal; y = yVal
} else {
dLog("parseStringToAXValue: CGPoint parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CGPoint. Expected format like 'x=10,y=20' or '10,20'.")
}
}
var point = CGPoint(x: x, y: y)
valueRef = AXValueCreate(targetAXValueType, &point)
case .cgSize:
var w: Double = 0, h: Double = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 2,
let wValStr = components[0].split(separator: "=").last, let wVal = Double(wValStr),
let hValStr = components[1].split(separator: "=").last, let hVal = Double(hValStr) {
w = wVal; h = hVal
} else if components.count == 2, let wVal = Double(components[0]), let hVal = Double(components[1]) {
w = wVal; h = hVal
} else {
let scanner = Scanner(string: stringValue)
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "wh:, \t\n"))
let wScanned = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "wh:, \t\n"))
let hScanned = scanner.scanDouble()
if let wVal = wScanned, let hVal = hScanned {
w = wVal; h = hVal
} else {
dLog("parseStringToAXValue: CGSize parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CGSize. Expected format like 'w=100,h=50' or '100,50'.")
}
}
var size = CGSize(width: w, height: h)
valueRef = AXValueCreate(targetAXValueType, &size)
case .cgRect:
var x: Double = 0, y: Double = 0, w: Double = 0, h: Double = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 4,
let xStr = components[0].split(separator: "=").last, let xVal = Double(xStr),
let yStr = components[1].split(separator: "=").last, let yVal = Double(yStr),
let wStr = components[2].split(separator: "=").last, let wVal = Double(wStr),
let hStr = components[3].split(separator: "=").last, let hVal = Double(hStr) {
x = xVal; y = yVal; w = wVal; h = hVal
} else if components.count == 4,
let xVal = Double(components[0]), let yVal = Double(components[1]),
let wVal = Double(components[2]), let hVal = Double(components[3]) {
x = xVal; y = yVal; w = wVal; h = hVal
} else {
let scanner = Scanner(string: stringValue)
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xywh:, \t\n"))
let xS_opt = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xywh:, \t\n"))
let yS_opt = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xywh:, \t\n"))
let wS_opt = scanner.scanDouble()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "xywh:, \t\n"))
let hS_opt = scanner.scanDouble()
if let xS = xS_opt, let yS = yS_opt, let wS = wS_opt, let hS = hS_opt {
x = xS; y = yS; w = wS; h = hS
} else {
dLog("parseStringToAXValue: CGRect parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CGRect. Expected format like 'x=0,y=0,w=100,h=50' or '0,0,100,50'.")
}
}
var rect = CGRect(x: x, y: y, width: w, height: h)
valueRef = AXValueCreate(targetAXValueType, &rect)
case .cfRange:
var loc: Int = 0, len: Int = 0
let components = stringValue.replacingOccurrences(of: " ", with: "").split(separator: ",")
if components.count == 2,
let locStr = components[0].split(separator: "=").last, let locVal = Int(locStr),
let lenStr = components[1].split(separator: "=").last, let lenVal = Int(lenStr) {
loc = locVal; len = lenVal
} else if components.count == 2, let locVal = Int(components[0]), let lenVal = Int(components[1]) {
loc = locVal; len = lenVal
} else {
let scanner = Scanner(string: stringValue)
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "loclen:, \t\n"))
let locScanned: Int? = scanner.scanInteger()
_ = scanner.scanCharacters(in: CustomCharacterSet(charactersInString: "loclen:, \t\n"))
let lenScanned: Int? = scanner.scanInteger()
if let locV = locScanned, let lenV = lenScanned {
loc = locV
len = lenV
} else {
dLog("parseStringToAXValue: CFRange parsing failed for '\(stringValue)' via scanner.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' into CFRange. Expected format like 'loc=0,len=10' or '0,10'.")
}
}
var range = CFRangeMake(loc, len)
valueRef = AXValueCreate(targetAXValueType, &range)
case .illegal:
dLog("parseStringToAXValue: Attempted to parse for .illegal AXValueType.")
throw AccessibilityError.attributeUnsupported("Cannot parse value for AXValueType .illegal")
case .axError:
dLog("parseStringToAXValue: Attempted to parse for .axError AXValueType.")
throw AccessibilityError.attributeUnsupported("Cannot set an attribute of AXValueType .axError")
default:
if targetAXValueType.rawValue == 4 {
var boolVal: DarwinBoolean
if stringValue.lowercased() == "true" { boolVal = true }
else if stringValue.lowercased() == "false" { boolVal = false }
else {
dLog("parseStringToAXValue: Boolean parsing failed for '\(stringValue)' for AXValue.")
throw AccessibilityError.valueParsingFailed(details: "Could not parse '\(stringValue)' as boolean for AXValue.")
}
valueRef = AXValueCreate(targetAXValueType, &boolVal)
} else {
dLog("parseStringToAXValue: Unsupported AXValueType '\(stringFromAXValueType(targetAXValueType))' (rawValue: \(targetAXValueType.rawValue)).")
throw AccessibilityError.attributeUnsupported("Parsing for AXValueType '\(stringFromAXValueType(targetAXValueType))' (rawValue: \(targetAXValueType.rawValue)) from string is not supported yet.")
}
}
if valueRef == nil {
dLog("parseStringToAXValue: AXValueCreate failed for type \(stringFromAXValueType(targetAXValueType)) with input '\(stringValue)'")
throw AccessibilityError.valueParsingFailed(details: "AXValueCreate failed for type \(stringFromAXValueType(targetAXValueType)) with input '\(stringValue)'")
}
return valueRef
}

View File

@ -0,0 +1,92 @@
import Foundation
import ApplicationServices
import CoreGraphics // For CGPoint, CGSize etc.
// debug() is assumed to be globally available from Logging.swift
// Constants like kAXPositionAttribute are assumed to be globally available from AccessibilityConstants.swift
// MARK: - ValueUnwrapper Utility
struct ValueUnwrapper {
@MainActor
static func unwrap(_ cfValue: CFTypeRef?, isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Any? {
func dLog(_ message: String) { if isDebugLoggingEnabled { currentDebugLogs.append(message) } }
guard let value = cfValue else { return nil }
let typeID = CFGetTypeID(value)
switch typeID {
case ApplicationServices.AXUIElementGetTypeID():
return value as! AXUIElement
case ApplicationServices.AXValueGetTypeID():
let axVal = value as! AXValue
let axValueType = AXValueGetType(axVal)
if axValueType.rawValue == 4 { // kAXValueBooleanType (private)
var boolResult: DarwinBoolean = false
if AXValueGetValue(axVal, axValueType, &boolResult) {
return boolResult.boolValue
}
}
switch axValueType {
case .cgPoint:
var point = CGPoint.zero
return AXValueGetValue(axVal, .cgPoint, &point) ? point : nil
case .cgSize:
var size = CGSize.zero
return AXValueGetValue(axVal, .cgSize, &size) ? size : nil
case .cgRect:
var rect = CGRect.zero
return AXValueGetValue(axVal, .cgRect, &rect) ? rect : nil
case .cfRange:
var cfRange = CFRange()
return AXValueGetValue(axVal, .cfRange, &cfRange) ? cfRange : nil
case .axError:
var axErrorValue: AXError = .success
return AXValueGetValue(axVal, .axError, &axErrorValue) ? axErrorValue : nil
case .illegal:
dLog("ValueUnwrapper: Encountered AXValue with type .illegal")
return nil
@unknown default: // Added @unknown default to handle potential new AXValueType cases
dLog("ValueUnwrapper: AXValue with unhandled AXValueType: \(stringFromAXValueType(axValueType)).")
return axVal // Return the original AXValue if type is unknown
}
case CFStringGetTypeID():
return (value as! CFString) as String
case CFAttributedStringGetTypeID():
return (value as! NSAttributedString).string
case CFBooleanGetTypeID():
return CFBooleanGetValue((value as! CFBoolean))
case CFNumberGetTypeID():
return value as! NSNumber
case CFArrayGetTypeID():
let cfArray = value as! CFArray
var swiftArray: [Any?] = []
for i in 0..<CFArrayGetCount(cfArray) {
guard let elementPtr = CFArrayGetValueAtIndex(cfArray, i) else {
swiftArray.append(nil)
continue
}
swiftArray.append(unwrap(Unmanaged<CFTypeRef>.fromOpaque(elementPtr).takeUnretainedValue(), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs))
}
return swiftArray
case CFDictionaryGetTypeID():
let cfDict = value as! CFDictionary
var swiftDict: [String: Any?] = [:]
// Attempt to bridge to Swift dictionary directly if possible
if let nsDict = cfDict as? [String: AnyObject] { // Use AnyObject for broader compatibility
for (key, val) in nsDict {
swiftDict[key] = unwrap(val, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: &currentDebugLogs) // Unwrap the value
}
} else {
// Fallback for more complex CFDictionary structures if direct bridging fails
// This part requires careful handling of CFDictionary keys and values
// For now, we'll log if direct bridging fails, as full CFDictionary iteration is complex.
dLog("ValueUnwrapper: Failed to bridge CFDictionary to [String: AnyObject]. Full CFDictionary iteration not yet implemented here.")
}
return swiftDict
default:
dLog("ValueUnwrapper: Unhandled CFTypeID: \(typeID) - \(CFCopyTypeIDDescription(typeID) as String? ?? "Unknown"). Returning raw value.")
return value // Return the original value if CFType is not handled
}
}
}

773
Sources/axorc/axorc.swift Normal file
View File

@ -0,0 +1,773 @@
import Foundation
import AXorcist
import ArgumentParser
let AXORC_VERSION = "0.1.2a-config_fix"
@main // Add @main if this is the executable's entry point
struct AXORCCommand: AsyncParsableCommand { // Changed to AsyncParsableCommand
static let configuration = CommandConfiguration(
commandName: "axorc", // commandName must come before abstract
abstract: "AXORC CLI - Handles JSON commands via various input methods. Version \\(AXORC_VERSION)"
)
@Flag(name: .long, help: "Enable debug logging for the command execution.")
var debug: Bool = false
@Flag(name: .long, help: "Read JSON payload from STDIN.")
var stdin: Bool = false
@Option(name: .long, help: "Read JSON payload from the specified file path.")
var file: String?
@Argument(help: "Read JSON payload directly from this string argument. If other input flags (--stdin, --file) are used, this argument is ignored.")
var directPayload: String? = nil
mutating func run() async throws {
var localDebugLogs: [String] = []
if debug {
localDebugLogs.append("Debug logging enabled by --debug flag.")
}
var receivedJsonString: String? = nil
var inputSourceDescription: String = "Unspecified"
var detailedInputError: String? = nil
let activeInputFlags = (stdin ? 1 : 0) + (file != nil ? 1 : 0)
let positionalPayloadProvided = directPayload != nil && !(directPayload?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
if activeInputFlags > 1 {
detailedInputError = "Error: Multiple input flags specified (--stdin, --file). Only one is allowed."
inputSourceDescription = detailedInputError!
} else if stdin {
inputSourceDescription = "STDIN"
let stdInputHandle = FileHandle.standardInput
let stdinData = stdInputHandle.readDataToEndOfFile()
if let str = String(data: stdinData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !str.isEmpty {
receivedJsonString = str
localDebugLogs.append("Successfully read \(str.count) chars from STDIN.")
} else {
detailedInputError = "Warning: STDIN flag specified, but no data or empty data received."
localDebugLogs.append(detailedInputError!)
}
} else if let filePath = file {
inputSourceDescription = "File: \(filePath)"
do {
let fileContent = try String(contentsOfFile: filePath, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
if fileContent.isEmpty {
detailedInputError = "Error: File '\(filePath)' is empty."
} else {
receivedJsonString = fileContent
localDebugLogs.append("Successfully read from file: \(filePath)")
}
} catch {
detailedInputError = "Error: Failed to read from file '\(filePath)': \(error.localizedDescription)"
}
if detailedInputError != nil { localDebugLogs.append(detailedInputError!) }
} else if let payload = directPayload, positionalPayloadProvided {
inputSourceDescription = "Direct Argument Payload"
receivedJsonString = payload.trimmingCharacters(in: .whitespacesAndNewlines)
localDebugLogs.append("Using direct argument payload. Length: \(receivedJsonString?.count ?? 0)")
} else if directPayload != nil && !positionalPayloadProvided {
detailedInputError = "Error: Direct argument payload was provided but was an empty string."
inputSourceDescription = detailedInputError!
localDebugLogs.append(detailedInputError!)
} else {
detailedInputError = "No JSON input method specified or chosen method yielded no data."
inputSourceDescription = detailedInputError!
localDebugLogs.append(detailedInputError!)
}
if detailedInputError != nil { localDebugLogs.append(detailedInputError!) }
print("AXORC_JSON_OUTPUT_PREFIX:::")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let errorToReport = detailedInputError, receivedJsonString == nil {
let errResponse = ErrorResponse(command_id: "input_error", error: ErrorResponse.ErrorDetail(message: errorToReport), debug_logs: debug ? localDebugLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
guard let jsonToProcess = receivedJsonString, !jsonToProcess.isEmpty else {
let finalErrorMsg = detailedInputError ?? "No JSON data successfully processed. Last input state: \(inputSourceDescription)."
var errorLogs = localDebugLogs; errorLogs.append(finalErrorMsg)
let errResponse = ErrorResponse(command_id: "no_json_data", error: ErrorResponse.ErrorDetail(message: finalErrorMsg), debug_logs: debug ? errorLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
do {
let commandEnvelope = try JSONDecoder().decode(CommandEnvelope.self, from: Data(jsonToProcess.utf8))
var currentLogs = localDebugLogs
currentLogs.append("Decoded CommandEnvelope. Type: \(commandEnvelope.command), ID: \(commandEnvelope.command_id)")
switch commandEnvelope.command {
case .ping:
let prefix = "Ping handled by AXORCCommand. Input source: "
let messageValue = inputSourceDescription
let successMessage = prefix + messageValue
currentLogs.append(successMessage)
let details: String?
if let payloadData = jsonToProcess.data(using: .utf8),
let payload = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
let payloadDict = payload["payload"] as? [String: Any],
let payloadMessage = payloadDict["message"] as? String {
details = payloadMessage
} else {
details = nil
}
let successResponse = SimpleSuccessResponse(
command_id: commandEnvelope.command_id,
success: true, // Explicitly true
status: "pong",
message: successMessage,
details: details,
debug_logs: debug ? currentLogs : nil
)
if let data = try? encoder.encode(successResponse), let str = String(data: data, encoding: .utf8) { print(str) }
case .getFocusedElement:
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
// Directly await the MainActor function. operationResult is non-optional.
let operationResult: HandlerResponse = await axInstance.handleGetFocusedElement(
for: appIdentifierForHandler,
requestedAttributes: requestedAttributesForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
// No semaphore needed
// operationResult is now non-optional, so we can use it directly.
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .getAttributes:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "getAttributes command requires a locator but none was provided"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let outputFormatForHandler = commandEnvelope.output_format
// Call the new handleGetAttributes method
let operationResult: HandlerResponse = await axInstance.handleGetAttributes(
for: appIdentifierForHandler,
locator: locatorForHandler,
requestedAttributes: requestedAttributesForHandler,
pathHint: pathHintForHandler,
maxDepth: maxDepthForHandler,
outputFormat: outputFormatForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for getAttributes...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding for getAttributes: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .query:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "query command requires a locator but none was provided"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let requestedAttributesForHandler = commandEnvelope.attributes
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let outputFormatForHandler = commandEnvelope.output_format
// Call the new handleQuery method
let operationResult: HandlerResponse = await axInstance.handleQuery(
for: appIdentifierForHandler,
locator: locatorForHandler,
pathHint: pathHintForHandler,
maxDepth: maxDepthForHandler,
requestedAttributes: requestedAttributesForHandler,
outputFormat: outputFormatForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for query...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding for query: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .describeElement:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "describeElement command requires a locator but none was provided"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let outputFormatForHandler = commandEnvelope.output_format
// Call the new handleDescribeElement method
let operationResult: HandlerResponse = await axInstance.handleDescribeElement(
for: appIdentifierForHandler,
locator: locatorForHandler,
pathHint: pathHintForHandler,
maxDepth: maxDepthForHandler,
outputFormat: outputFormatForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for describeElement...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding for describeElement: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .performAction:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "performAction command requires a locator but none was provided"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
guard let actionNameForHandler = commandEnvelope.action_name else {
let errorMsg = "performAction command requires an action_name but none was provided"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let pathHintForHandler = commandEnvelope.path_hint
let actionValueForHandler = commandEnvelope.action_value // This is AnyCodable?
// Call the new handlePerformAction method
let operationResult: HandlerResponse = await axInstance.handlePerformAction(
for: appIdentifierForHandler,
locator: locatorForHandler,
pathHint: pathHintForHandler,
actionName: actionNameForHandler,
actionValue: actionValueForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for performAction...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding for performAction: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .extractText:
guard let locatorForHandler = commandEnvelope.locator else {
let errorMsg = "extractText command requires a locator but none was provided"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
let axInstance = AXorcist()
var handlerLogs = currentLogs
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let pathHintForHandler = commandEnvelope.path_hint
let operationResult: HandlerResponse = await axInstance.handleExtractText(
for: appIdentifierForHandler,
locator: locatorForHandler,
pathHint: pathHintForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: &handlerLogs
)
let actualResponse = operationResult
let finalDebugLogs = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for extractText...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: actualResponse.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: actualResponse,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding for extractText: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .batch:
// The main commandEnvelope is for the batch itself.
// Sub-commands are now directly in commandEnvelope.sub_commands.
guard let subCommands = commandEnvelope.sub_commands, !subCommands.isEmpty else {
let errorMsg = "Batch command received, but 'sub_commands' array is missing or empty."
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
return
}
currentLogs.append("Processing batch command. Batch ID: \(commandEnvelope.command_id), Number of sub-commands: \(subCommands.count)")
let axInstance = AXorcist()
var handlerLogs = currentLogs // batch handler will append to this
// Call the handleBatchCommands method
let batchHandlerResponses: [HandlerResponse] = await axInstance.handleBatchCommands(
batchCommandID: commandEnvelope.command_id, // Use the main command's ID for the batch
subCommands: subCommands, // Pass the array of CommandEnvelopes
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug, // Use overall debug flag
currentDebugLogs: &handlerLogs
)
// Convert each HandlerResponse into a QueryResponse
var batchQueryResponses: [QueryResponse] = []
var overallSuccess = true
for (index, subHandlerResponse) in batchHandlerResponses.enumerated() {
// The subCommandEnvelope for ID and type.
// Make sure subCommands array is not empty and index is valid.
guard index < subCommands.count else {
// This should not happen if batchHandlerResponses lines up with subCommands
let errorMsg = "Mismatch between subCommands and batchHandlerResponses count."
currentLogs.append(errorMsg)
// Consider how to report this internal error
continue
}
let subCommandEnvelope = subCommands[index]
let subQueryResponse = QueryResponse(
command_id: subCommandEnvelope.command_id, // Use sub-command's ID
success: subHandlerResponse.error == nil,
command: subCommandEnvelope.command.rawValue, // Use sub-command's type
handlerResponse: subHandlerResponse,
debug_logs: nil // Individual sub-command logs are part of HandlerResponse.
// QueryResponse's init handles this for its 'error' or 'data'.
// The overall batch debug log will be separate.
)
batchQueryResponses.append(subQueryResponse)
if subHandlerResponse.error != nil {
overallSuccess = false
}
}
let finalDebugLogsForBatch = debug || (commandEnvelope.debug_logging ?? false) ? handlerLogs : nil
let batchOperationResponse = BatchOperationResponse(
command_id: commandEnvelope.command_id, // ID of the overall batch from the main envelope
success: overallSuccess,
results: batchQueryResponses,
debug_logs: finalDebugLogsForBatch
)
do {
let data = try encoder.encode(batchOperationResponse)
if let str = String(data: data, encoding: .utf8) {
print(str)
} else {
let errorMsg = "Failed to convert BatchOperationResponse to UTF8 string."
currentLogs.append(errorMsg) // Log to main logs
fputs("[axorc DEBUG] \(errorMsg)\n", stderr)
// Fallback to a simple error if top-level encoding fails
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: finalDebugLogsForBatch)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
let errorMsg = "Failed to encode BatchOperationResponse: \(error.localizedDescription)"
currentLogs.append(errorMsg) // Log to main logs
fputs("[axorc DEBUG] \(errorMsg) - Error: \(error)\n", stderr)
// Fallback to a simple error
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: finalDebugLogsForBatch)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
case .collectAll:
let axInstance = AXorcist()
let handlerLogs = currentLogs // Changed var to let
let commandIDForResponse = commandEnvelope.command_id
let appIdentifierForHandler = commandEnvelope.application
let locatorForHandler = commandEnvelope.locator // Optional for collectAll
let pathHintForHandler = commandEnvelope.path_hint
let maxDepthForHandler = commandEnvelope.max_elements
let requestedAttributesForHandler = commandEnvelope.attributes
let outputFormatForHandler = commandEnvelope.output_format
// Call handleCollectAll, passing handlerLogs as non-inout
let operationResult: HandlerResponse = await axInstance.handleCollectAll(
for: appIdentifierForHandler,
locator: locatorForHandler,
pathHint: pathHintForHandler,
maxDepth: maxDepthForHandler,
requestedAttributes: requestedAttributesForHandler,
outputFormat: outputFormatForHandler,
isDebugLoggingEnabled: commandEnvelope.debug_logging ?? debug,
currentDebugLogs: handlerLogs // Pass as [String]
)
// operationResult.debug_logs now contains all logs from the handler
// including the initial handlerLogs plus anything new from handleCollectAll.
let finalDebugLogs = (debug || (commandEnvelope.debug_logging ?? false)) ? operationResult.debug_logs : nil
fputs("[axorc DEBUG] Attempting to encode QueryResponse for collectAll...\n", stderr)
let queryResponse = QueryResponse(
command_id: commandIDForResponse,
success: operationResult.error == nil,
command: commandEnvelope.command.rawValue,
handlerResponse: operationResult,
debug_logs: finalDebugLogs
)
do {
let data = try encoder.encode(queryResponse)
fputs("[axorc DEBUG] QueryResponse encoded to data. Size: \(data.count)\n", stderr)
if let str = String(data: data, encoding: .utf8) {
fputs("[axorc DEBUG] QueryResponse data converted to string. Length: \(str.count). Printing to stdout.\n", stderr)
print(str) // STDOUT
} else {
fputs("[axorc DEBUG] Failed to convert QueryResponse data to UTF8 string.\n", stderr)
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Failed to convert QueryResponse data to string (UTF8)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let errData = try? encoder.encode(errResponse), let errStr = String(data: errData, encoding: .utf8) { print(errStr) }
}
} catch {
fputs("[axorc DEBUG] Explicitly CAUGHT error during QueryResponse encoding for collectAll: \(error)\n", stderr)
fputs("[axorc DEBUG] Error localizedDescription: \(error.localizedDescription)\n", stderr)
if let encodingError = error as? EncodingError {
fputs("[axorc DEBUG] EncodingError context: \(encodingError)\n", stderr)
}
let errorDetailForResponse = ErrorResponse.ErrorDetail(message: "Caught error during QueryResponse encoding: \(error.localizedDescription)")
let errResponse = ErrorResponse(command_id: commandIDForResponse, error: errorDetailForResponse, debug_logs: finalDebugLogs)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
default:
let errorMsg = "Unhandled command type: \(commandEnvelope.command)"
currentLogs.append(errorMsg)
let errResponse = ErrorResponse(command_id: commandEnvelope.command_id, error: ErrorResponse.ErrorDetail(message: errorMsg), debug_logs: debug ? currentLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
} catch {
var errorLogs = localDebugLogs
let basicErrorMessage = "JSON decoding error: \(error.localizedDescription)"
errorLogs.append(basicErrorMessage)
let detailedErrorMessage: String
if let decodingError = error as? DecodingError {
errorLogs.append("Decoding error details: \(decodingError.humanReadableDescription)")
detailedErrorMessage = "Failed to decode JSON command (DecodingError): \(decodingError.humanReadableDescription)"
} else {
detailedErrorMessage = "Failed to decode JSON command: \(error.localizedDescription)"
}
let errResponse = ErrorResponse(command_id: "decode_error", error: ErrorResponse.ErrorDetail(message: detailedErrorMessage), debug_logs: debug ? errorLogs : nil)
if let data = try? encoder.encode(errResponse), let str = String(data: data, encoding: .utf8) { print(str) }
}
}
}
// MARK: - Codable Structs for axorc responses and CommandEnvelope
// These should align with structs in AXorcistIntegrationTests.swift
struct SimpleSuccessResponse: Codable {
let command_id: String
let success: Bool
let status: String? // e.g., "pong"
let message: String
let details: String?
let debug_logs: [String]?
}
struct ErrorResponse: Codable {
let command_id: String
var success: Bool = false // Default to false for errors
struct ErrorDetail: Codable {
let message: String
}
let error: ErrorDetail
let debug_logs: [String]?
}
// AXElement as received from AXorcist library and to be encoded in QueryResponse
// This is a pass-through structure. AXorcist.AXElement should be Codable itself.
// If AXorcist.AXElement is not Codable, then this needs to be manually constructed.
// For now, assume AXorcist.AXElement is Codable or can be easily made so.
// The properties (attributes, path) must match what AXorcist.AXElement provides.
struct AXElementForEncoding: Codable {
let attributes: [String: AnyCodable]? // This will now use AXorcist.AnyCodable
let path: [String]?
init(from axElement: AXElement) { // axElement is AXorcist.AXElement
self.attributes = axElement.attributes // Directly assign
self.path = axElement.path
}
}
struct QueryResponse: Codable {
let command_id: String
let success: Bool
let command: String // Name of the command, e.g., "getFocusedElement"
let data: AXElementForEncoding? // Contains the AX element's data, adapted for encoding
let error: ErrorResponse.ErrorDetail?
let debug_logs: [String]?
// Custom initializer to bridge from HandlerResponse (from AXorcist module)
init(command_id: String, success: Bool, command: String, handlerResponse: HandlerResponse, debug_logs: [String]?) {
self.command_id = command_id
self.success = success
self.command = command
if let axElement = handlerResponse.data {
self.data = AXElementForEncoding(from: axElement) // Convert here
} else {
self.data = nil
}
if let errorMsg = handlerResponse.error {
self.error = ErrorResponse.ErrorDetail(message: errorMsg)
} else {
self.error = nil
}
self.debug_logs = debug_logs
}
}
struct BatchOperationResponse: Codable {
let command_id: String
let success: Bool
let results: [QueryResponse]
let debug_logs: [String]?
}
// Helper for DecodingError display
extension DecodingError {
var humanReadableDescription: String {
switch self {
case .typeMismatch(let type, let context): return "Type mismatch for \(type): \(context.debugDescription) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))"
case .valueNotFound(let type, let context): return "Value not found for \(type): \(context.debugDescription) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))"
case .keyNotFound(let key, let context): return "Key not found: \(key.stringValue) at \(context.codingPath.map { $0.stringValue }.joined(separator: ".")) - \(context.debugDescription)"
case .dataCorrupted(let context): return "Data corrupted: \(context.debugDescription) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))"
@unknown default: return self.localizedDescription
}
}
}
/*
struct AXORC: ParsableCommand { ... old content ... }
*/

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import XCTest
class SimpleXCTest: XCTestCase {
func testExample() throws {
XCTAssertEqual(1, 1, "Simple assertion should pass")
}
func testAnotherExample() {
XCTAssertTrue(true, "Another simple assertion")
}
}

BIN
axorc Executable file

Binary file not shown.

11
run_tests.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
echo "=== AXorcist Test Runner ==="
echo "Killing any existing SwiftPM processes..."
# Kill any existing swift processes
pkill -f "swift" || true
pkill -f "SourceKitService" || true
echo "Starting swift test (without git clean to preserve dependencies)..."
swift test