From 895eac9cbf783eca419d997b3673bb7c19e09f4d Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 14 Feb 2026 10:43:21 -0600 Subject: [PATCH] Unify ManeshTraderMac into main repository --- .gitignore | 1 + ManeshTraderMac | 1 - .../ManeshTraderMac.xcodeproj/project.pbxproj | 582 +++++++++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++ .../Assets.xcassets/Contents.json | 6 + .../ManeshTraderMac/ContentView.swift | 799 ++++++++++++++++++ .../ManeshTraderMac/EmbeddedBackend/README.md | 6 + .../ManeshTraderMac/ManeshTraderMacApp.swift | 18 + .../ManeshTraderMacTests.swift | 17 + .../ManeshTraderMacUITests.swift | 41 + .../ManeshTraderMacUITestsLaunchTests.swift | 33 + ManeshTraderMac/README.md | 30 + 14 files changed, 1609 insertions(+), 1 deletion(-) delete mode 160000 ManeshTraderMac create mode 100644 ManeshTraderMac/ManeshTraderMac.xcodeproj/project.pbxproj create mode 100644 ManeshTraderMac/ManeshTraderMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ManeshTraderMac/ManeshTraderMac/Assets.xcassets/Contents.json create mode 100644 ManeshTraderMac/ManeshTraderMac/ContentView.swift create mode 100644 ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/README.md create mode 100644 ManeshTraderMac/ManeshTraderMac/ManeshTraderMacApp.swift create mode 100644 ManeshTraderMac/ManeshTraderMacTests/ManeshTraderMacTests.swift create mode 100644 ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITests.swift create mode 100644 ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITestsLaunchTests.swift create mode 100644 ManeshTraderMac/README.md diff --git a/.gitignore b/.gitignore index e6bc6b0..e91c2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist-mac/ dist-standalone/ ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/ManeshTraderBackend +**/xcuserdata/ diff --git a/ManeshTraderMac b/ManeshTraderMac deleted file mode 160000 index 82f326e..0000000 --- a/ManeshTraderMac +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 82f326ea935cde47e1bff98face941499d8d0a98 diff --git a/ManeshTraderMac/ManeshTraderMac.xcodeproj/project.pbxproj b/ManeshTraderMac/ManeshTraderMac.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dcf6860 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac.xcodeproj/project.pbxproj @@ -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 = ""; + }; + EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ManeshTraderMacTests; + sourceTree = ""; + }; + EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ManeshTraderMacUITests; + sourceTree = ""; + }; +/* 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 = ""; + }; + EAB6606D2F3FD5C000ED41BA /* Products */ = { + isa = PBXGroup; + children = ( + EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */, + EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */, + EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/ManeshTraderMac/ManeshTraderMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ManeshTraderMac/ManeshTraderMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AccentColor.colorset/Contents.json b/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AppIcon.appiconset/Contents.json b/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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 + } +} diff --git a/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/Contents.json b/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ManeshTraderMac/ManeshTraderMac/ContentView.swift b/ManeshTraderMac/ManeshTraderMac/ContentView.swift new file mode 100644 index 0000000..4ee002f --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac/ContentView.swift @@ -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.size)) + + var addr = sockaddr_in() + addr.sin_len = UInt8(MemoryLayout.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.stride)) == 0 + } + } + } +} diff --git a/ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/README.md b/ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/README.md new file mode 100644 index 0000000..60582da --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/README.md @@ -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. diff --git a/ManeshTraderMac/ManeshTraderMac/ManeshTraderMacApp.swift b/ManeshTraderMac/ManeshTraderMac/ManeshTraderMacApp.swift new file mode 100644 index 0000000..935bc22 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMac/ManeshTraderMacApp.swift @@ -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") + } + } +} diff --git a/ManeshTraderMac/ManeshTraderMacTests/ManeshTraderMacTests.swift b/ManeshTraderMac/ManeshTraderMacTests/ManeshTraderMacTests.swift new file mode 100644 index 0000000..81871cf --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMacTests/ManeshTraderMacTests.swift @@ -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. + } + +} diff --git a/ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITests.swift b/ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITests.swift new file mode 100644 index 0000000..5b204b0 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITests.swift @@ -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() + } + } +} diff --git a/ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITestsLaunchTests.swift b/ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITestsLaunchTests.swift new file mode 100644 index 0000000..849ace0 --- /dev/null +++ b/ManeshTraderMac/ManeshTraderMacUITests/ManeshTraderMacUITestsLaunchTests.swift @@ -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) + } +} diff --git a/ManeshTraderMac/README.md b/ManeshTraderMac/README.md new file mode 100644 index 0000000..22f2ded --- /dev/null +++ b/ManeshTraderMac/README.md @@ -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//ManeshTraderMac.app` +3. Optional DMG packaging: + - `APP_BUNDLE_PATH="dist-mac//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.