Unify ManeshTraderMac into main repository

This commit is contained in:
Matt Bruce 2026-02-14 10:43:21 -06:00
parent cfe417a9e6
commit 895eac9cbf
14 changed files with 1609 additions and 1 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ dist-mac/
dist-standalone/
ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/ManeshTraderBackend
**/xcuserdata/

@ -1 +0,0 @@
Subproject commit 82f326ea935cde47e1bff98face941499d8d0a98

View File

@ -0,0 +1,582 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXContainerItemProxy section */
EAB6607A2F3FD5C100ED41BA /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EAB660642F3FD5C000ED41BA /* Project object */;
proxyType = 1;
remoteGlobalIDString = EAB6606B2F3FD5C000ED41BA;
remoteInfo = ManeshTraderMac;
};
EAB660842F3FD5C100ED41BA /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EAB660642F3FD5C000ED41BA /* Project object */;
proxyType = 1;
remoteGlobalIDString = EAB6606B2F3FD5C000ED41BA;
remoteInfo = ManeshTraderMac;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMac.app; sourceTree = BUILT_PRODUCTS_DIR; };
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ManeshTraderMac;
sourceTree = "<group>";
};
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ManeshTraderMacTests;
sourceTree = "<group>";
};
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = ManeshTraderMacUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
EAB660692F3FD5C000ED41BA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660762F3FD5C100ED41BA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660802F3FD5C100ED41BA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
EAB660632F3FD5C000ED41BA = {
isa = PBXGroup;
children = (
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */,
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */,
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */,
EAB6606D2F3FD5C000ED41BA /* Products */,
);
sourceTree = "<group>";
};
EAB6606D2F3FD5C000ED41BA /* Products */ = {
isa = PBXGroup;
children = (
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */,
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */,
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */ = {
isa = PBXNativeTarget;
buildConfigurationList = EAB6608D2F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMac" */;
buildPhases = (
EAB660682F3FD5C000ED41BA /* Sources */,
EAB660692F3FD5C000ED41BA /* Frameworks */,
EAB6606A2F3FD5C000ED41BA /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */,
);
name = ManeshTraderMac;
packageProductDependencies = (
);
productName = ManeshTraderMac;
productReference = EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */;
productType = "com.apple.product-type.application";
};
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */;
buildPhases = (
EAB660752F3FD5C100ED41BA /* Sources */,
EAB660762F3FD5C100ED41BA /* Frameworks */,
EAB660772F3FD5C100ED41BA /* Resources */,
);
buildRules = (
);
dependencies = (
EAB6607B2F3FD5C100ED41BA /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */,
);
name = ManeshTraderMacTests;
packageProductDependencies = (
);
productName = ManeshTraderMacTests;
productReference = EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EAB660932F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacUITests" */;
buildPhases = (
EAB6607F2F3FD5C100ED41BA /* Sources */,
EAB660802F3FD5C100ED41BA /* Frameworks */,
EAB660812F3FD5C100ED41BA /* Resources */,
);
buildRules = (
);
dependencies = (
EAB660852F3FD5C100ED41BA /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */,
);
name = ManeshTraderMacUITests;
packageProductDependencies = (
);
productName = ManeshTraderMacUITests;
productReference = EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
EAB660642F3FD5C000ED41BA /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2630;
LastUpgradeCheck = 2630;
TargetAttributes = {
EAB6606B2F3FD5C000ED41BA = {
CreatedOnToolsVersion = 26.3;
};
EAB660782F3FD5C100ED41BA = {
CreatedOnToolsVersion = 26.3;
TestTargetID = EAB6606B2F3FD5C000ED41BA;
};
EAB660822F3FD5C100ED41BA = {
CreatedOnToolsVersion = 26.3;
TestTargetID = EAB6606B2F3FD5C000ED41BA;
};
};
};
buildConfigurationList = EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "ManeshTraderMac" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = EAB660632F3FD5C000ED41BA;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = EAB6606D2F3FD5C000ED41BA /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */,
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */,
EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
EAB6606A2F3FD5C000ED41BA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660772F3FD5C100ED41BA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660812F3FD5C100ED41BA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
EAB660682F3FD5C000ED41BA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660752F3FD5C100ED41BA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB6607F2F3FD5C100ED41BA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
EAB6607B2F3FD5C100ED41BA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */;
targetProxy = EAB6607A2F3FD5C100ED41BA /* PBXContainerItemProxy */;
};
EAB660852F3FD5C100ED41BA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */;
targetProxy = EAB660842F3FD5C100ED41BA /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
EAB6608B2F3FD5C100ED41BA /* 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 = 6R7KLBPBLZ;
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 = 26.2;
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;
};
EAB6608C2F3FD5C100ED41BA /* 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 = 6R7KLBPBLZ;
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 = 26.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
EAB6608E2F3FD5C100ED41BA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMac;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
EAB6608F2F3FD5C100ED41BA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMac;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
EAB660912F3FD5C100ED41BA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ManeshTraderMac.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ManeshTraderMac";
};
name = Debug;
};
EAB660922F3FD5C100ED41BA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ManeshTraderMac.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ManeshTraderMac";
};
name = Release;
};
EAB660942F3FD5C100ED41BA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = ManeshTraderMac;
};
name = Debug;
};
EAB660952F3FD5C100ED41BA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TEST_TARGET_NAME = ManeshTraderMac;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "ManeshTraderMac" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EAB6608B2F3FD5C100ED41BA /* Debug */,
EAB6608C2F3FD5C100ED41BA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EAB6608D2F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMac" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EAB6608E2F3FD5C100ED41BA /* Debug */,
EAB6608F2F3FD5C100ED41BA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EAB660912F3FD5C100ED41BA /* Debug */,
EAB660922F3FD5C100ED41BA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EAB660932F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EAB660942F3FD5C100ED41BA /* Debug */,
EAB660952F3FD5C100ED41BA /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = EAB660642F3FD5C000ED41BA /* Project object */;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,799 @@
import SwiftUI
import WebKit
import Foundation
import Observation
struct ContentView: View {
@State private var host = TraderHost()
@State private var didAutostart = false
@AppStorage("mt_setup_completed") private var setupCompleted = false
@AppStorage("mt_symbol") private var storedSymbol = "AAPL"
@AppStorage("mt_interval") private var storedInterval = "1d"
@AppStorage("mt_period") private var storedPeriod = "6mo"
@AppStorage("mt_max_bars") private var storedMaxBars = 500
@AppStorage("mt_drop_live") private var storedDropLive = true
@AppStorage("mt_use_body_range") private var storedUseBodyRange = false
@AppStorage("mt_volume_filter_enabled") private var storedVolumeFilterEnabled = false
@AppStorage("mt_volume_sma_window") private var storedVolumeSMAWindow = 20
@AppStorage("mt_volume_multiplier") private var storedVolumeMultiplier = 1.0
@AppStorage("mt_gray_fake") private var storedGrayFake = true
@AppStorage("mt_hide_market_closed_gaps") private var storedHideMarketClosedGaps = true
@AppStorage("mt_enable_auto_refresh") private var storedEnableAutoRefresh = false
@AppStorage("mt_refresh_sec") private var storedRefreshSeconds = 60
@State private var showSetupSheet = false
@State private var setupDraft = UserSetupPreferences.default
#if DEBUG
@State private var showDebugPanel = false
#endif
var body: some View {
VStack(spacing: 0) {
if host.isRunning {
LocalWebView(url: host.serverURL, reloadToken: host.reloadToken)
} else {
launchView
}
#if DEBUG
debugPanel
#endif
}
.frame(minWidth: 1100, minHeight: 760)
.onAppear {
guard !didAutostart else { return }
didAutostart = true
let normalized = syncHostPreferencesFromSharedSettings()
persistSharedSettingsFile(normalized)
host.start()
if !setupCompleted {
setupDraft = normalized.setupDefaults
showSetupSheet = true
}
}
.onDisappear { host.stop() }
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Setup") {
syncStateFromSharedSettingsFileIfAvailable()
setupDraft = storedWebPreferences.normalized().setupDefaults
showSetupSheet = true
}
Button("Reload") {
_ = syncHostPreferencesFromSharedSettings()
host.reloadWebView()
}
.disabled(!host.isRunning)
Button("Restart") {
_ = syncHostPreferencesFromSharedSettings()
host.restart()
}
.disabled(host.isStarting)
}
}
.sheet(isPresented: $showSetupSheet) {
setupSheet
}
}
private var sharedSettingsURL: URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".manesh_trader", isDirectory: true)
.appendingPathComponent("settings.json", isDirectory: false)
}
private var storedWebPreferences: WebPreferences {
WebPreferences(
symbol: storedSymbol,
interval: storedInterval,
period: storedPeriod,
maxBars: storedMaxBars,
dropLive: storedDropLive,
useBodyRange: storedUseBodyRange,
volumeFilterEnabled: storedVolumeFilterEnabled,
volumeSMAWindow: storedVolumeSMAWindow,
volumeMultiplier: storedVolumeMultiplier,
grayFake: storedGrayFake,
hideMarketClosedGaps: storedHideMarketClosedGaps,
enableAutoRefresh: storedEnableAutoRefresh,
refreshSeconds: storedRefreshSeconds
)
}
private var setupSheet: some View {
NavigationStack {
Form {
Section {
Text("Choose your default market settings so you see useful data immediately on launch.")
.foregroundStyle(.secondary)
}
Section("Data Defaults") {
TextField("Symbol", text: $setupDraft.symbol)
Text("Ticker or pair, e.g. AAPL, MSFT, BTC-USD.")
.font(.caption)
.foregroundStyle(.secondary)
Picker("Timeframe", selection: $setupDraft.timeframe) {
ForEach(UserSetupPreferences.timeframeOptions, id: \.self) { option in
Text(option).tag(option)
}
}
Text("Bar size for each candle. `1d` is a good starting point.")
.font(.caption)
.foregroundStyle(.secondary)
Picker("Period", selection: $setupDraft.period) {
ForEach(UserSetupPreferences.periodOptions, id: \.self) { option in
Text(option).tag(option)
}
}
Text("How much history to load for analysis.")
.font(.caption)
.foregroundStyle(.secondary)
Stepper(value: $setupDraft.maxBars, in: 20...5000, step: 10) {
Text("Max bars: \(setupDraft.maxBars)")
}
Text("Limits candles loaded for speed and readability.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.navigationTitle("Welcome to ManeshTrader")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Use Defaults") {
let normalized = setupDraft.normalized()
applySetup(normalized, markCompleted: true)
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save and Continue") {
let normalized = setupDraft.normalized()
applySetup(normalized, markCompleted: true)
}
}
}
}
.frame(minWidth: 560, minHeight: 460)
}
private func applySetup(_ preferences: UserSetupPreferences, markCompleted: Bool) {
let normalizedSetup = preferences.normalized()
let mergedPreferences = storedWebPreferences
.applyingSetup(normalizedSetup)
.normalized()
writeWebPreferencesToStorage(mergedPreferences)
persistSharedSettingsFile(mergedPreferences)
if markCompleted {
setupCompleted = true
}
host.applyPreferences(mergedPreferences)
host.reloadWebView()
showSetupSheet = false
}
private func syncStateFromSharedSettingsFileIfAvailable() {
guard
let data = try? Data(contentsOf: sharedSettingsURL),
let decoded = try? JSONDecoder().decode(WebPreferences.self, from: data)
else {
return
}
writeWebPreferencesToStorage(decoded.normalized())
}
@discardableResult
private func syncHostPreferencesFromSharedSettings() -> WebPreferences {
syncStateFromSharedSettingsFileIfAvailable()
let normalized = storedWebPreferences.normalized()
writeWebPreferencesToStorage(normalized)
host.applyPreferences(normalized)
return normalized
}
private func writeWebPreferencesToStorage(_ preferences: WebPreferences) {
storedSymbol = preferences.symbol
storedInterval = preferences.interval
storedPeriod = preferences.period
storedMaxBars = preferences.maxBars
storedDropLive = preferences.dropLive
storedUseBodyRange = preferences.useBodyRange
storedVolumeFilterEnabled = preferences.volumeFilterEnabled
storedVolumeSMAWindow = preferences.volumeSMAWindow
storedVolumeMultiplier = preferences.volumeMultiplier
storedGrayFake = preferences.grayFake
storedHideMarketClosedGaps = preferences.hideMarketClosedGaps
storedEnableAutoRefresh = preferences.enableAutoRefresh
storedRefreshSeconds = preferences.refreshSeconds
}
private func persistSharedSettingsFile(_ preferences: WebPreferences) {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
do {
let normalized = preferences.normalized()
let directory = sharedSettingsURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let data = try encoder.encode(normalized)
try data.write(to: sharedSettingsURL, options: .atomic)
} catch {
#if DEBUG
print("Failed to persist shared settings: \(error.localizedDescription)")
#endif
}
}
private var launchView: some View {
VStack(spacing: 14) {
launchCard
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
LinearGradient(
colors: [
Color(nsColor: .windowBackgroundColor),
Color(nsColor: .underPageBackgroundColor),
],
startPoint: .top,
endPoint: .bottom
)
)
}
@ViewBuilder
private var launchCard: some View {
let card = VStack(spacing: 14) {
Image(systemName: host.launchError == nil ? "chart.line.uptrend.xyaxis.circle.fill" : "exclamationmark.triangle.fill")
.font(.system(size: 42))
.foregroundStyle(host.launchError == nil ? .green : .orange)
Text(host.launchError == nil ? "Starting ManeshTrader" : "Couldnt Start ManeshTrader")
.font(.title3.weight(.semibold))
if let launchError = host.launchError {
Text(launchError)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 700)
if #available(macOS 26.0, *) {
Button("Try Again") { host.start() }
.buttonStyle(.glassProminent)
} else {
Button("Try Again") { host.start() }
.buttonStyle(.borderedProminent)
}
} else {
ProgressView()
.controlSize(.large)
Text("Loading local engine...")
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 26)
.padding(.vertical, 22)
if #available(macOS 26.0, *) {
card.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 24))
} else {
card.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24, style: .continuous))
}
}
#if DEBUG
private var debugPanel: some View {
VStack(spacing: 0) {
Divider()
DisclosureGroup("Developer Tools", isExpanded: $showDebugPanel) {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Button("Start") {
_ = syncHostPreferencesFromSharedSettings()
host.start()
}
.disabled(host.isRunning || host.isStarting)
Button("Stop") { host.stop() }
.disabled(!host.isRunning && !host.isStarting)
Button("Reload") {
_ = syncHostPreferencesFromSharedSettings()
host.reloadWebView()
}
.disabled(!host.isRunning)
}
Text(host.status)
.font(.callout)
.foregroundStyle(.secondary)
Text("Local URL: \(host.serverURL.absoluteString)")
.font(.caption)
.foregroundStyle(.secondary)
Text("Embedded backend: \(host.backendExecutablePath)")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
.padding(.top, 6)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
}
}
#endif
}
#Preview {
ContentView()
}
private struct UserSetupPreferences {
static let timeframeOptions = ["1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo"]
static let periodOptions = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "max"]
static let `default` = UserSetupPreferences(symbol: "AAPL", timeframe: "1d", period: "6mo", maxBars: 500)
var symbol: String
var timeframe: String
var period: String
var maxBars: Int
func normalized() -> UserSetupPreferences {
let normalizedSymbol = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
let safeSymbol = normalizedSymbol.isEmpty ? "AAPL" : normalizedSymbol
let safeTimeframe = Self.timeframeOptions.contains(timeframe) ? timeframe : "1d"
let safePeriod = Self.periodOptions.contains(period) ? period : "6mo"
let safeMaxBars = min(5000, max(20, maxBars))
return UserSetupPreferences(
symbol: safeSymbol,
timeframe: safeTimeframe,
period: safePeriod,
maxBars: safeMaxBars
)
}
}
private struct WebPreferences: Codable {
var symbol: String
var interval: String
var period: String
var maxBars: Int
var dropLive: Bool
var useBodyRange: Bool
var volumeFilterEnabled: Bool
var volumeSMAWindow: Int
var volumeMultiplier: Double
var grayFake: Bool
var hideMarketClosedGaps: Bool
var enableAutoRefresh: Bool
var refreshSeconds: Int
init(
symbol: String,
interval: String,
period: String,
maxBars: Int,
dropLive: Bool,
useBodyRange: Bool,
volumeFilterEnabled: Bool,
volumeSMAWindow: Int,
volumeMultiplier: Double,
grayFake: Bool,
hideMarketClosedGaps: Bool,
enableAutoRefresh: Bool,
refreshSeconds: Int
) {
self.symbol = symbol
self.interval = interval
self.period = period
self.maxBars = maxBars
self.dropLive = dropLive
self.useBodyRange = useBodyRange
self.volumeFilterEnabled = volumeFilterEnabled
self.volumeSMAWindow = volumeSMAWindow
self.volumeMultiplier = volumeMultiplier
self.grayFake = grayFake
self.hideMarketClosedGaps = hideMarketClosedGaps
self.enableAutoRefresh = enableAutoRefresh
self.refreshSeconds = refreshSeconds
}
static let `default` = WebPreferences(
symbol: "AAPL",
interval: "1d",
period: "6mo",
maxBars: 500,
dropLive: true,
useBodyRange: false,
volumeFilterEnabled: false,
volumeSMAWindow: 20,
volumeMultiplier: 1.0,
grayFake: true,
hideMarketClosedGaps: true,
enableAutoRefresh: false,
refreshSeconds: 60
)
private enum CodingKeys: String, CodingKey {
case symbol
case interval
case period
case maxBars = "max_bars"
case dropLive = "drop_live"
case useBodyRange = "use_body_range"
case volumeFilterEnabled = "volume_filter_enabled"
case volumeSMAWindow = "volume_sma_window"
case volumeMultiplier = "volume_multiplier"
case grayFake = "gray_fake"
case hideMarketClosedGaps = "hide_market_closed_gaps"
case enableAutoRefresh = "enable_auto_refresh"
case refreshSeconds = "refresh_sec"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let defaults = Self.default
symbol = try container.decodeIfPresent(String.self, forKey: .symbol) ?? defaults.symbol
interval = try container.decodeIfPresent(String.self, forKey: .interval) ?? defaults.interval
period = try container.decodeIfPresent(String.self, forKey: .period) ?? defaults.period
maxBars = try container.decodeIfPresent(Int.self, forKey: .maxBars) ?? defaults.maxBars
dropLive = try container.decodeIfPresent(Bool.self, forKey: .dropLive) ?? defaults.dropLive
useBodyRange = try container.decodeIfPresent(Bool.self, forKey: .useBodyRange) ?? defaults.useBodyRange
volumeFilterEnabled = try container.decodeIfPresent(Bool.self, forKey: .volumeFilterEnabled) ?? defaults.volumeFilterEnabled
volumeSMAWindow = try container.decodeIfPresent(Int.self, forKey: .volumeSMAWindow) ?? defaults.volumeSMAWindow
volumeMultiplier = try container.decodeIfPresent(Double.self, forKey: .volumeMultiplier) ?? defaults.volumeMultiplier
grayFake = try container.decodeIfPresent(Bool.self, forKey: .grayFake) ?? defaults.grayFake
hideMarketClosedGaps = try container.decodeIfPresent(Bool.self, forKey: .hideMarketClosedGaps) ?? defaults.hideMarketClosedGaps
enableAutoRefresh = try container.decodeIfPresent(Bool.self, forKey: .enableAutoRefresh) ?? defaults.enableAutoRefresh
refreshSeconds = try container.decodeIfPresent(Int.self, forKey: .refreshSeconds) ?? defaults.refreshSeconds
}
func normalized() -> WebPreferences {
let safeSymbol = {
let candidate = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
return candidate.isEmpty ? "AAPL" : candidate
}()
let safeInterval = UserSetupPreferences.timeframeOptions.contains(interval) ? interval : "1d"
let safePeriod = UserSetupPreferences.periodOptions.contains(period) ? period : "6mo"
let safeMaxBars = min(5000, max(20, maxBars))
let safeVolumeSMAWindow = min(100, max(2, volumeSMAWindow))
let clampedMultiplier = min(3.0, max(0.1, volumeMultiplier))
let safeVolumeMultiplier = (clampedMultiplier * 10).rounded() / 10
let safeRefreshSeconds = min(600, max(10, refreshSeconds))
return WebPreferences(
symbol: safeSymbol,
interval: safeInterval,
period: safePeriod,
maxBars: safeMaxBars,
dropLive: dropLive,
useBodyRange: useBodyRange,
volumeFilterEnabled: volumeFilterEnabled,
volumeSMAWindow: safeVolumeSMAWindow,
volumeMultiplier: safeVolumeMultiplier,
grayFake: grayFake,
hideMarketClosedGaps: hideMarketClosedGaps,
enableAutoRefresh: enableAutoRefresh,
refreshSeconds: safeRefreshSeconds
)
}
var setupDefaults: UserSetupPreferences {
UserSetupPreferences(
symbol: symbol,
timeframe: interval,
period: period,
maxBars: maxBars
).normalized()
}
func applyingSetup(_ setup: UserSetupPreferences) -> WebPreferences {
let normalizedSetup = setup.normalized()
return WebPreferences(
symbol: normalizedSetup.symbol,
interval: normalizedSetup.timeframe,
period: normalizedSetup.period,
maxBars: normalizedSetup.maxBars,
dropLive: dropLive,
useBodyRange: useBodyRange,
volumeFilterEnabled: volumeFilterEnabled,
volumeSMAWindow: volumeSMAWindow,
volumeMultiplier: volumeMultiplier,
grayFake: grayFake,
hideMarketClosedGaps: hideMarketClosedGaps,
enableAutoRefresh: enableAutoRefresh,
refreshSeconds: refreshSeconds
)
}
var queryItems: [URLQueryItem] {
[
URLQueryItem(name: "symbol", value: symbol),
URLQueryItem(name: "interval", value: interval),
URLQueryItem(name: "period", value: period),
URLQueryItem(name: "max_bars", value: String(maxBars)),
URLQueryItem(name: "drop_live", value: String(dropLive)),
URLQueryItem(name: "use_body_range", value: String(useBodyRange)),
URLQueryItem(name: "volume_filter_enabled", value: String(volumeFilterEnabled)),
URLQueryItem(name: "volume_sma_window", value: String(volumeSMAWindow)),
URLQueryItem(name: "volume_multiplier", value: String(volumeMultiplier)),
URLQueryItem(name: "gray_fake", value: String(grayFake)),
URLQueryItem(name: "hide_market_closed_gaps", value: String(hideMarketClosedGaps)),
URLQueryItem(name: "enable_auto_refresh", value: String(enableAutoRefresh)),
URLQueryItem(name: "refresh_sec", value: String(refreshSeconds)),
]
}
}
private struct LocalWebView: NSViewRepresentable {
let url: URL
let reloadToken: Int
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero)
webView.customUserAgent = "ManeshTraderMac"
webView.allowsBackForwardNavigationGestures = false
webView.navigationDelegate = context.coordinator
context.coordinator.attach(webView)
webView.load(URLRequest(url: url))
return webView
}
func updateNSView(_ webView: WKWebView, context: Context) {
if context.coordinator.lastReloadToken != reloadToken || webView.url != url {
context.coordinator.lastReloadToken = reloadToken
webView.load(URLRequest(url: url))
}
}
func makeCoordinator() -> Coordinator {
Coordinator(url: url, reloadToken: reloadToken)
}
final class Coordinator: NSObject, WKNavigationDelegate {
private let url: URL
private weak var webView: WKWebView?
private var pendingRetry: DispatchWorkItem?
var lastReloadToken: Int
init(url: URL, reloadToken: Int) {
self.url = url
self.lastReloadToken = reloadToken
}
func attach(_ webView: WKWebView) {
self.webView = webView
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
pendingRetry?.cancel()
pendingRetry = nil
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
scheduleRetry()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
scheduleRetry()
}
private func scheduleRetry() {
pendingRetry?.cancel()
let retry = DispatchWorkItem { [weak self] in
guard let self, let webView = self.webView else { return }
webView.load(URLRequest(url: self.url))
}
pendingRetry = retry
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: retry)
}
}
}
@Observable
@MainActor
private final class TraderHost {
var isRunning = false
var isStarting = false
var launchError: String?
var status = "Preparing backend..."
var backendExecutablePath = ""
var reloadToken = 0
var serverPort = 8501
var webQueryItems: [URLQueryItem] = []
var serverURL: URL {
var components = URLComponents()
components.scheme = "http"
components.host = "127.0.0.1"
components.port = serverPort
components.queryItems = webQueryItems.isEmpty ? nil : webQueryItems
return components.url!
}
private var process: Process?
private var outputPipe: Pipe?
private var latestBackendLogLine = ""
private var lastSignificantBackendLogLine = ""
private var didRequestStop = false
func start() {
guard process == nil else { return }
isStarting = true
launchError = nil
status = "Starting local engine..."
didRequestStop = false
guard let executableURL = bundledBackendExecutableURL() else {
isStarting = false
launchError = "Required backend files were not found in the app bundle."
status = "Bundled backend executable not found."
return
}
backendExecutablePath = executableURL.path
do {
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executableURL.path)
} catch {
status = "Could not set executable permissions: \(error.localizedDescription)"
}
let p = Process()
latestBackendLogLine = ""
lastSignificantBackendLogLine = ""
serverPort = nextAvailablePort(preferred: 8501)
p.currentDirectoryURL = executableURL.deletingLastPathComponent()
p.executableURL = executableURL
var environment = ProcessInfo.processInfo.environment
environment["MANESH_TRADER_PORT"] = "\(serverPort)"
p.environment = environment
let pipe = Pipe()
outputPipe = pipe
p.standardOutput = pipe
p.standardError = pipe
pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
let data = handle.availableData
guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return }
let lastLine = text
.split(whereSeparator: \.isNewline)
.last
.map(String.init)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !lastLine.isEmpty else { return }
DispatchQueue.main.async {
self?.latestBackendLogLine = lastLine
if !lastLine.contains("LOADER: failed to destroy sync semaphore") {
self?.lastSignificantBackendLogLine = lastLine
}
}
}
p.terminationHandler = { [weak self] proc in
DispatchQueue.main.async {
guard let self else { return }
self.isRunning = false
self.isStarting = false
self.process = nil
self.outputPipe?.fileHandleForReading.readabilityHandler = nil
self.outputPipe = nil
if self.didRequestStop {
self.status = "Stopped."
return
}
if let line = self.lastSignificantBackendLogLine.isEmpty ? nil : self.lastSignificantBackendLogLine {
self.status = "Stopped (exit \(proc.terminationStatus)): \(line)"
} else if let line = self.latestBackendLogLine.isEmpty ? nil : self.latestBackendLogLine {
self.status = "Stopped (exit \(proc.terminationStatus)): \(line)"
} else {
self.status = "Stopped (exit \(proc.terminationStatus))."
}
if proc.terminationStatus != 0 {
self.launchError = "The local engine stopped unexpectedly. Please try again."
}
}
}
do {
try p.run()
process = p
isRunning = true
isStarting = false
reloadToken += 1
status = "Running locally in-app at \(serverURL.absoluteString)"
} catch {
status = "Failed to start backend: \(error.localizedDescription)"
process = nil
outputPipe?.fileHandleForReading.readabilityHandler = nil
outputPipe = nil
isRunning = false
isStarting = false
launchError = "Unable to start the local engine. Please try again."
}
}
func stop() {
guard let process else { return }
didRequestStop = true
isStarting = false
if process.isRunning {
process.terminate()
}
outputPipe?.fileHandleForReading.readabilityHandler = nil
outputPipe = nil
self.process = nil
isRunning = false
status = "Stopped."
}
func reloadWebView() {
guard isRunning else { return }
reloadToken += 1
}
func applyPreferences(_ preferences: WebPreferences) {
webQueryItems = preferences.normalized().queryItems
}
func restart() {
stop()
start()
}
private func bundledBackendExecutableURL() -> URL? {
let fm = FileManager.default
let candidates = [
Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil, subdirectory: "EmbeddedBackend"),
Bundle.main.resourceURL?.appendingPathComponent("EmbeddedBackend/ManeshTraderBackend"),
Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil),
].compactMap { $0 }
return candidates.first(where: { fm.fileExists(atPath: $0.path) })
}
private func nextAvailablePort(preferred: Int) -> Int {
if canBindToLocalPort(preferred) {
return preferred
}
for candidate in 8502...8600 where canBindToLocalPort(candidate) {
return candidate
}
return preferred
}
private func canBindToLocalPort(_ port: Int) -> Bool {
let fd = socket(AF_INET, SOCK_STREAM, 0)
guard fd >= 0 else { return false }
defer { close(fd) }
var value: Int32 = 1
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout<Int32>.size))
var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.stride)
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = in_port_t(UInt16(port).bigEndian)
addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
return withUnsafePointer(to: &addr) { pointer in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
bind(fd, sockPtr, socklen_t(MemoryLayout<sockaddr_in>.stride)) == 0
}
}
}
}

View File

@ -0,0 +1,6 @@
This folder is populated by `scripts/build_embedded_backend.sh`.
- Output binary: `ManeshTraderBackend`
- Source of truth for backend code: project root (`app.py`, `manesh_trader/`)
Do not hand-edit generated binaries in this folder.

View File

@ -0,0 +1,18 @@
//
// ManeshTraderMacApp.swift
// ManeshTraderMac
//
// Created by Matt Bruce on 2/13/26.
//
import SwiftUI
@main
struct ManeshTraderMacApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.navigationTitle("ManeshTrader")
}
}
}

View File

@ -0,0 +1,17 @@
//
///Users/mattbruce/Documents/Web/ManeshTrader/ManeshTraderMac/ManeshTraderMac/ContentView.swift ManeshTraderMacTests.swift
// ManeshTraderMacTests
//
// Created by Matt Bruce on 2/13/26.
//
import Testing
@testable import ManeshTraderMac
struct ManeshTraderMacTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
}

View File

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

View File

@ -0,0 +1,33 @@
//
// ManeshTraderMacUITestsLaunchTests.swift
// ManeshTraderMacUITests
//
// Created by Matt Bruce on 2/13/26.
//
import XCTest
final class ManeshTraderMacUITestsLaunchTests: 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)
}
}

30
ManeshTraderMac/README.md Normal file
View File

@ -0,0 +1,30 @@
# ManeshTraderMac (Xcode Shell App)
This Xcode app is a native macOS shell around the existing Streamlit trading app.
## What It Does
- Auto-starts backend when the app launches
- Starts/stops a bundled backend executable from app resources
- Embeds the local UI using `WKWebView` at `http://127.0.0.1:8501`
- Keeps users inside the app window (no external browser required)
## Build Self-Contained App
1. From project root, build the embedded backend + macOS app:
- `./scripts/build_selfcontained_mac_app.sh`
2. Output app bundle:
- `dist-mac/<timestamp>/ManeshTraderMac.app`
3. Optional DMG packaging:
- `APP_BUNDLE_PATH="dist-mac/<timestamp>/ManeshTraderMac.app" ./scripts/create_installer_dmg.sh`
## Run in Xcode
1. Generate embedded backend binary:
- `./scripts/build_embedded_backend.sh`
2. Open `ManeshTraderMac/ManeshTraderMac.xcodeproj`
3. Build/Run the `ManeshTraderMac` scheme
4. The app auto-starts backend on launch
## Notes
- Backend source of truth is the root web app (`app.py`, `manesh_trader/`).
- `scripts/build_embedded_backend.sh` compiles those files into:
- `ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/ManeshTraderBackend`
- The Swift host launches that embedded binary directly from the installed app bundle.