Unify ManeshTraderMac into main repository
This commit is contained in:
parent
cfe417a9e6
commit
895eac9cbf
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ dist-mac/
|
|||||||
dist-standalone/
|
dist-standalone/
|
||||||
|
|
||||||
ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/ManeshTraderBackend
|
ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/ManeshTraderBackend
|
||||||
|
**/xcuserdata/
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
Subproject commit 82f326ea935cde47e1bff98face941499d8d0a98
|
|
||||||
582
ManeshTraderMac/ManeshTraderMac.xcodeproj/project.pbxproj
Normal file
582
ManeshTraderMac/ManeshTraderMac.xcodeproj/project.pbxproj
Normal 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 */;
|
||||||
|
}
|
||||||
7
ManeshTraderMac/ManeshTraderMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
ManeshTraderMac/ManeshTraderMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
799
ManeshTraderMac/ManeshTraderMac/ContentView.swift
Normal file
799
ManeshTraderMac/ManeshTraderMac/ContentView.swift
Normal 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" : "Couldn’t 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.
|
||||||
18
ManeshTraderMac/ManeshTraderMac/ManeshTraderMacApp.swift
Normal file
18
ManeshTraderMac/ManeshTraderMac/ManeshTraderMacApp.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
30
ManeshTraderMac/README.md
Normal 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.
|
||||||
Loading…
Reference in New Issue
Block a user