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:
parent
b3920f883d
commit
13d0e93369
@ -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 */;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -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>
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 it’s 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
39
Makefile
Normal 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
32
Package.resolved
Normal 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
44
Package.swift
Normal 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
|
||||
)
|
||||
]
|
||||
)
|
||||
960
Sources/AXorcist/AXorcist.swift
Normal file
960
Sources/AXorcist/AXorcist.swift
Normal 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: ¤tDebugLogs) 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: ¤tDebugLogs)) for application \(appIdentifier)")
|
||||
|
||||
let fetchedAttributes = getElementAttributes(
|
||||
focusedElement,
|
||||
requestedAttributes: requestedAttributes ?? [],
|
||||
forMultiDefault: false,
|
||||
targetRole: nil,
|
||||
outputFormat: .smart,
|
||||
isDebugLoggingEnabled: isDebugLoggingEnabled,
|
||||
currentDebugLogs: ¤tDebugLogs
|
||||
)
|
||||
|
||||
let elementPathArray = focusedElement.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
|
||||
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: ¤tDebugLogs) 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: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
if let elementToQuery = foundElement {
|
||||
let elementDescription = elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs
|
||||
)
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
let elementPathArray = elementToQuery.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
let axElement = AXElement(attributes: attributes, path: elementPathArray)
|
||||
|
||||
dLog("[AXorcist.handleGetAttributes] Successfully fetched attributes for element \(elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)).")
|
||||
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: ¤tDebugLogs) 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: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs) 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: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs
|
||||
)
|
||||
}
|
||||
|
||||
if let elementToQuery = foundElement {
|
||||
let elementDescription = elementToQuery.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
let elementPathArray = elementToQuery.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
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: ¤tDebugLogs) 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: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
if let elementToDescribe = foundElement {
|
||||
let elementDescription = elementToDescribe.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
_ = 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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
if outputFormat == .json_string {
|
||||
attributes = encodeAttributesToJSONStringRepresentation(attributes)
|
||||
}
|
||||
|
||||
let elementPathArray = elementToDescribe.generatePathArray(upTo: appElement, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
let axElement = AXElement(attributes: attributes, path: elementPathArray)
|
||||
|
||||
dLog("[AXorcist.handleDescribeElement] Successfully described element \(elementToDescribe.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)).")
|
||||
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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs))")
|
||||
guard let foundElement = search(element: effectiveElement, locator: locator, requireAction: locator.requireAction, maxDepth: maxDepth ?? DEFAULT_MAX_DEPTH_SEARCH, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) 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: ¤tDebugLogs))")
|
||||
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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs)), 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: ¤tDebugLogs)
|
||||
// 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: ¤tDebugLogs // 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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
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: ¤tDebugLogs
|
||||
)
|
||||
|
||||
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)", ¤tDebugLogs)
|
||||
|
||||
// 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)", ¤tDebugLogs)
|
||||
}
|
||||
|
||||
// Skip all other attribute processing for this debug step
|
||||
dLog("TEMPORARY DEBUG: Skipped all other attribute processing.", ¤tDebugLogs)
|
||||
|
||||
// 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: \", \"))", ¤tDebugLogs)
|
||||
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 ...
|
||||
}
|
||||
71
Sources/AXorcist/Commands/GetAttributesCommandHandler.swift
Normal file
71
Sources/AXorcist/Commands/GetAttributesCommandHandler.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
92
Sources/AXorcist/Commands/QueryCommandHandler.swift
Normal file
92
Sources/AXorcist/Commands/QueryCommandHandler.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
201
Sources/AXorcist/Core/AccessibilityConstants.swift
Normal file
201
Sources/AXorcist/Core/AccessibilityConstants.swift
Normal 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"
|
||||
108
Sources/AXorcist/Core/AccessibilityError.swift
Normal file
108
Sources/AXorcist/Core/AccessibilityError.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Sources/AXorcist/Core/AccessibilityPermissions.swift
Normal file
118
Sources/AXorcist/Core/AccessibilityPermissions.swift
Normal 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: ¤tDebugLogs)
|
||||
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: ¤tDebugLogs)
|
||||
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
|
||||
}
|
||||
113
Sources/AXorcist/Core/Attribute.swift
Normal file
113
Sources/AXorcist/Core/Attribute.swift
Normal 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...
|
||||
}
|
||||
87
Sources/AXorcist/Core/Element+Hierarchy.swift
Normal file
87
Sources/AXorcist/Core/Element+Hierarchy.swift
Normal 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: ¤tDebugLogs))")
|
||||
|
||||
// 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
|
||||
}
|
||||
98
Sources/AXorcist/Core/Element+Properties.swift
Normal file
98
Sources/AXorcist/Core/Element+Properties.swift
Normal 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: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func subrole(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
attribute(Attribute<String>.subrole, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func title(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
attribute(Attribute<String>.title, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func description(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
attribute(Attribute<String>.description, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func isEnabled(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
|
||||
attribute(Attribute<Bool>.enabled, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func value(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Any? {
|
||||
attribute(Attribute<Any>.value, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func roleDescription(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
attribute(Attribute<String>.roleDescription, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func help(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
attribute(Attribute<String>.help, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func identifier(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> String? {
|
||||
attribute(Attribute<String>.identifier, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
|
||||
// Status Properties - now methods
|
||||
@MainActor public func isFocused(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
|
||||
attribute(Attribute<Bool>.focused, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func isHidden(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
|
||||
attribute(Attribute<Bool>.hidden, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
@MainActor public func isElementBusy(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool? {
|
||||
attribute(Attribute<Bool>.busy, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs)
|
||||
}
|
||||
|
||||
@MainActor public func isIgnored(isDebugLoggingEnabled: Bool, currentDebugLogs: inout [String]) -> Bool {
|
||||
if attribute(Attribute<Bool>.hidden, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) == 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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs) ?? 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: ¤tDebugLogs) ?? 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: ¤tDebugLogs) ?? 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: ¤tDebugLogs)
|
||||
}
|
||||
}
|
||||
355
Sources/AXorcist/Core/Element.swift
Normal file
355
Sources/AXorcist/Core/Element.swift
Normal 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: ¤tDebugLogs) 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: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs)
|
||||
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: ¤tDebugLogs)
|
||||
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: ¤tDebugLogs)
|
||||
|
||||
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: ¤tDebugLogs), !titleStr.isEmpty, titleStr != kAXNotAvailableString { return titleStr }
|
||||
|
||||
if let valueStr: String = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) as? String, !valueStr.isEmpty, valueStr != kAXNotAvailableString { return valueStr }
|
||||
|
||||
if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), !descStr.isEmpty, descStr != kAXNotAvailableString { return descStr }
|
||||
|
||||
if let helpStr: String = self.attribute(Attribute<String>(kAXHelpAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), !helpStr.isEmpty, helpStr != kAXNotAvailableString { return helpStr }
|
||||
if let phValueStr: String = self.attribute(Attribute<String>(kAXPlaceholderValueAttribute), isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), !phValueStr.isEmpty, phValueStr != kAXNotAvailableString { return phValueStr }
|
||||
|
||||
let roleNameStr: String = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "Element"
|
||||
|
||||
if let roleDescStr: String = self.roleDescription(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), !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: ¤tDebugLogs) 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: ¤tDebugLogs)) upTo: \(ancestor?.briefDescription(option: .default, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "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
|
||||
}
|
||||
}
|
||||
305
Sources/AXorcist/Core/Models.swift
Normal file
305
Sources/AXorcist/Core/Models.swift
Normal 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
|
||||
}
|
||||
}
|
||||
120
Sources/AXorcist/Core/ProcessUtils.swift
Normal file
120
Sources/AXorcist/Core/ProcessUtils.swift
Normal 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
|
||||
}
|
||||
377
Sources/AXorcist/Search/AttributeHelpers.swift
Normal file
377
Sources/AXorcist/Search/AttributeHelpers.swift
Normal 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: ¤tDebugLogs)
|
||||
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: ¤tDebugLogs)
|
||||
}
|
||||
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: ¤tDebugLogs)
|
||||
|
||||
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.
|
||||
173
Sources/AXorcist/Search/AttributeMatcher.swift
Normal file
173
Sources/AXorcist/Search/AttributeMatcher.swift
Normal 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: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if key == kAXActionNamesAttribute || key == kAXAllowedValuesAttribute || key == kAXChildrenAttribute {
|
||||
if !matchArrayAttribute(element: element, key: key, expectedValueString: expectedValue, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchStringAttribute(element: element, key: key, expectedValueString: expectedValue, depth: depth, isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs) 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
|
||||
}
|
||||
}
|
||||
|
||||
200
Sources/AXorcist/Search/ElementSearch.swift
Normal file
200
Sources/AXorcist/Search/ElementSearch.swift
Normal 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: ¤tDebugLogs) {
|
||||
// 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: ¤tDebugLogs) // 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: ¤tDebugLogs) {
|
||||
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: ¤tDebugLogs) // 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: ¤tDebugLogs // Pass through logs
|
||||
)
|
||||
}
|
||||
}
|
||||
81
Sources/AXorcist/Search/PathUtils.swift
Normal file
81
Sources/AXorcist/Search/PathUtils.swift
Normal 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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs) 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
|
||||
}
|
||||
42
Sources/AXorcist/Utils/CustomCharacterSet.swift
Normal file
42
Sources/AXorcist/Utils/CustomCharacterSet.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
84
Sources/AXorcist/Utils/GeneralParsingUtils.swift
Normal file
84
Sources/AXorcist/Utils/GeneralParsingUtils.swift
Normal 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 }
|
||||
}
|
||||
323
Sources/AXorcist/Utils/Scanner.swift
Normal file
323
Sources/AXorcist/Utils/Scanner.swift
Normal 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...])
|
||||
}
|
||||
}
|
||||
31
Sources/AXorcist/Utils/String+HelperExtensions.swift
Normal file
31
Sources/AXorcist/Utils/String+HelperExtensions.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Sources/AXorcist/Utils/TextExtraction.swift
Normal file
42
Sources/AXorcist/Utils/TextExtraction.swift
Normal 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: ¤tDebugLogs))")
|
||||
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")
|
||||
}
|
||||
44
Sources/AXorcist/Values/Scannable.swift
Normal file
44
Sources/AXorcist/Values/Scannable.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
174
Sources/AXorcist/Values/ValueFormatter.swift
Normal file
174
Sources/AXorcist/Values/ValueFormatter.swift
Normal 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: ¤tDebugLogs)
|
||||
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: ¤tDebugLogs))
|
||||
}
|
||||
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: ¤tDebugLogs)
|
||||
}
|
||||
// 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: ¤tDebugLogs), !titleStr.isEmpty {
|
||||
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "UnknownRole"
|
||||
return "<\(roleStr): \"\(escapeStringForDisplay(titleStr))\">"
|
||||
}
|
||||
else if let identifierStr = self.identifier(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), !identifierStr.isEmpty {
|
||||
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "UnknownRole"
|
||||
return "<\(roleStr) id: \"\(escapeStringForDisplay(identifierStr))\">"
|
||||
} else if let valueAny = self.value(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), let valueStr = valueAny as? String, !valueStr.isEmpty, valueStr.count < 50 {
|
||||
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "UnknownRole"
|
||||
return "<\(roleStr) val: \"\(escapeStringForDisplay(valueStr))\">"
|
||||
} else if let descStr = self.description(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs), !descStr.isEmpty, descStr.count < 50 {
|
||||
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "UnknownRole"
|
||||
return "<\(roleStr) desc: \"\(escapeStringForDisplay(descStr))\">"
|
||||
}
|
||||
let roleStr = self.role(isDebugLoggingEnabled: isDebugLoggingEnabled, currentDebugLogs: ¤tDebugLogs) ?? "UnknownRole"
|
||||
return "<\(roleStr)>"
|
||||
}
|
||||
}
|
||||
165
Sources/AXorcist/Values/ValueHelpers.swift
Normal file
165
Sources/AXorcist/Values/ValueHelpers.swift
Normal 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: ¤tDebugLogs)
|
||||
|
||||
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))"
|
||||
}
|
||||
}
|
||||
236
Sources/AXorcist/Values/ValueParser.swift
Normal file
236
Sources/AXorcist/Values/ValueParser.swift
Normal 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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs) 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: ¤tDebugLogs)
|
||||
} 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
|
||||
}
|
||||
92
Sources/AXorcist/Values/ValueUnwrapper.swift
Normal file
92
Sources/AXorcist/Values/ValueUnwrapper.swift
Normal 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: ¤tDebugLogs))
|
||||
}
|
||||
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: ¤tDebugLogs) // 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
773
Sources/axorc/axorc.swift
Normal 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 ... }
|
||||
*/
|
||||
|
||||
1252
Tests/AXorcistTests/AXorcistIntegrationTests.swift
Normal file
1252
Tests/AXorcistTests/AXorcistIntegrationTests.swift
Normal file
File diff suppressed because it is too large
Load Diff
11
Tests/AXorcistTests/SimpleXCTest.swift
Normal file
11
Tests/AXorcistTests/SimpleXCTest.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
11
run_tests.sh
Executable file
11
run_tests.sh
Executable 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
|
||||
Loading…
Reference in New Issue
Block a user