├── .github └── workflows │ └── linting.yml ├── .gitignore ├── .swiftlint.yml ├── Harbor.entitlements ├── Harbor.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Harbor.xcscheme ├── Harbor ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon-1024.png │ │ ├── AppIcon-128.png │ │ ├── AppIcon-16.png │ │ ├── AppIcon-256.png │ │ ├── AppIcon-32.png │ │ ├── AppIcon-512.png │ │ ├── AppIcon-64.png │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Dialogs │ └── BottleManagement │ │ └── NewBottleDropdown.swift ├── Extensions │ └── URLExtensions.swift ├── Harbor.entitlements ├── HarborApp.swift ├── Info.plist ├── Localizable.xcstrings ├── Models │ └── HarborBottle.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ └── BottleIcon.icns ├── Systems │ ├── BottleDX.swift │ ├── BrewUtils.swift │ ├── DXUtils.swift │ ├── GPKUtils.swift │ ├── Winetricks │ │ └── WinetricksUtils.swift │ └── XCLIUtils.swift ├── UIElements │ ├── BottleManagement │ │ ├── BottleCardListView.swift │ │ ├── BottleConfigDropdown.swift │ │ ├── BottleDetailsCommonView.swift │ │ ├── BottleOpsDropdown.swift │ │ ├── BottleTableView.swift │ │ ├── EnvironmentVarsEditor.swift │ │ ├── LaunchExtDropdown.swift │ │ └── TaskControllerView.swift │ ├── Commands │ │ ├── BottleMenu.swift │ │ ├── HarborMenu.swift │ │ └── ViewMenu.swift │ ├── GPKInstall │ │ ├── BrewInstallView.swift │ │ ├── GPKFastInstallView.swift │ │ ├── GPKSafeInstallView.swift │ │ └── XCLIInstallView.swift │ ├── MenuCommands │ │ ├── DXVKInstallView.swift │ │ └── GPTKConfigView.swift │ └── Winetricks │ │ └── WinetricksUI.swift ├── Utils │ ├── Environment+BrewUtils.swift │ ├── Environment+GPKUtils.swift │ ├── Environment+XCLIUtils.swift │ ├── HarborShortcuts.swift │ ├── HarborUtils.swift │ └── MenuUIStates.swift └── Views │ ├── BottleManagementCardView.swift │ ├── BottleManagementTableView.swift │ ├── BottleManagementView.swift │ └── SetupView.swift ├── LICENSE └── README.md /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Lint with SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/linting.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | push: 10 | branches: 11 | - senpai 12 | paths: 13 | - '.github/workflows/linting.yml' 14 | - '.swiftlint.yml' 15 | - '**/*.swift' 16 | workflow_dispatch: 17 | 18 | jobs: 19 | SwiftLint: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Run SwiftLint with --strict 24 | uses: norio-nomura/action-swiftlint@3.2.1 25 | with: 26 | args: --strict 27 | env: 28 | WORKING_DIRECTORY: Harbor 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - force_unwrapping 3 | -------------------------------------------------------------------------------- /Harbor.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Harbor.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AB0D00442A40A3630019D62F /* BottleDX.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0D00432A40A3630019D62F /* BottleDX.swift */; }; 11 | AB0D00462A40B27F0019D62F /* DXUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0D00452A40B27F0019D62F /* DXUtils.swift */; }; 12 | AB0D004A2A40B7670019D62F /* MenuUIStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0D00492A40B7670019D62F /* MenuUIStates.swift */; }; 13 | AB0D004C2A40B8900019D62F /* DXVKInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0D004B2A40B8900019D62F /* DXVKInstallView.swift */; }; 14 | AB0D004E2A40E3D30019D62F /* EnvironmentVarsEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0D004D2A40E3D30019D62F /* EnvironmentVarsEditor.swift */; }; 15 | AB0F6E242A4AEDDA00F642C7 /* BottleIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = AB0F6E232A4AEDDA00F642C7 /* BottleIcon.icns */; }; 16 | AB157BBE2B41289D00757AE0 /* TaskControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB157BBD2B41289C00757AE0 /* TaskControllerView.swift */; }; 17 | AB19DA972A358FE400967784 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = AB19DA962A358FE400967784 /* Localizable.xcstrings */; }; 18 | AB3BE5EE2A32248D00358BBC /* LaunchExtDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3BE5ED2A32248D00358BBC /* LaunchExtDropdown.swift */; }; 19 | AB5CC6C02A308BBB00AEBB2B /* HarborApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5CC6BF2A308BBB00AEBB2B /* HarborApp.swift */; }; 20 | AB5CC6C22A308BBB00AEBB2B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5CC6C12A308BBB00AEBB2B /* ContentView.swift */; }; 21 | AB5CC6C42A308BBC00AEBB2B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB5CC6C32A308BBC00AEBB2B /* Assets.xcassets */; }; 22 | AB5CC6C72A308BBC00AEBB2B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB5CC6C62A308BBC00AEBB2B /* Preview Assets.xcassets */; }; 23 | AB5CC6D62A30910300AEBB2B /* GPKUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5CC6D52A30910300AEBB2B /* GPKUtils.swift */; }; 24 | AB5CC6D82A30938A00AEBB2B /* BrewInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5CC6D72A30938A00AEBB2B /* BrewInstallView.swift */; }; 25 | AB5D49C62A3B5B0B008245A6 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB5D49C52A3B5B0B008245A6 /* URLExtensions.swift */; }; 26 | AB6254342A3CE30D002A6206 /* BottleCardListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6254332A3CE30D002A6206 /* BottleCardListView.swift */; }; 27 | AB6652C22A33342B00F3FC5D /* XCLIInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6652C12A33342B00F3FC5D /* XCLIInstallView.swift */; }; 28 | AB6652C42A3334E600F3FC5D /* XCLIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6652C32A3334E600F3FC5D /* XCLIUtils.swift */; }; 29 | AB6652C92A3349A400F3FC5D /* HarborMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6652C82A3349A400F3FC5D /* HarborMenu.swift */; }; 30 | AB6652CD2A3350EC00F3FC5D /* BottleConfigDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6652CC2A3350EC00F3FC5D /* BottleConfigDropdown.swift */; }; 31 | AB6A967F2A31045A003A019E /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6A967E2A31045A003A019E /* SetupView.swift */; }; 32 | AB7A81002A30CC7100AA71A6 /* BrewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7A80FF2A30CC7100AA71A6 /* BrewUtils.swift */; }; 33 | AB7A81022A30D2FE00AA71A6 /* HarborUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7A81012A30D2FE00AA71A6 /* HarborUtils.swift */; }; 34 | AB7D8E332A4DDE3400B55527 /* WinetricksUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7D8E322A4DDE3400B55527 /* WinetricksUtils.swift */; }; 35 | AB87CAF42A4AC67C00C32025 /* HarborShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB87CAF32A4AC67C00C32025 /* HarborShortcuts.swift */; }; 36 | AB95D9FD2A5011C5003402D2 /* GPTKConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB95D9FC2A5011C5003402D2 /* GPTKConfigView.swift */; }; 37 | ABB06E122AF559DB0078DE28 /* WinetricksUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB06E112AF559DB0078DE28 /* WinetricksUI.swift */; }; 38 | ABC0BFCE2A31627400382A42 /* BottleManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC0BFCD2A31627400382A42 /* BottleManagementView.swift */; }; 39 | ABC0BFD12A31629300382A42 /* HarborBottle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC0BFD02A31629300382A42 /* HarborBottle.swift */; }; 40 | ABC0BFD42A31691500382A42 /* BottleOpsDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC0BFD32A31691500382A42 /* BottleOpsDropdown.swift */; }; 41 | ABD56AB32A36E419002A439C /* GPKSafeInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD56AB12A36E419002A439C /* GPKSafeInstallView.swift */; }; 42 | ABD56AB42A36E419002A439C /* GPKFastInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD56AB22A36E419002A439C /* GPKFastInstallView.swift */; }; 43 | ABDA74592AD904E700802792 /* BottleDetailsCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABDA74582AD904E700802792 /* BottleDetailsCommonView.swift */; }; 44 | ABF5340A2A3F606A0030677A /* ViewMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF534092A3F606A0030677A /* ViewMenu.swift */; }; 45 | ABF5340C2A3F63BE0030677A /* BottleManagementCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF5340B2A3F63BE0030677A /* BottleManagementCardView.swift */; }; 46 | ABF5340E2A3F63FE0030677A /* BottleManagementTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF5340D2A3F63FE0030677A /* BottleManagementTableView.swift */; }; 47 | FC682EDA2A35FD57000C53D6 /* Environment+GPKUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC682ED92A35FD57000C53D6 /* Environment+GPKUtils.swift */; }; 48 | FC682EDC2A3601E0000C53D6 /* Environment+BrewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC682EDB2A3601E0000C53D6 /* Environment+BrewUtils.swift */; }; 49 | FC682EDF2A3604B4000C53D6 /* Environment+XCLIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC682EDE2A3604B4000C53D6 /* Environment+XCLIUtils.swift */; }; 50 | /* End PBXBuildFile section */ 51 | 52 | /* Begin PBXFileReference section */ 53 | AB0D00432A40A3630019D62F /* BottleDX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleDX.swift; sourceTree = ""; }; 54 | AB0D00452A40B27F0019D62F /* DXUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXUtils.swift; sourceTree = ""; }; 55 | AB0D00492A40B7670019D62F /* MenuUIStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuUIStates.swift; sourceTree = ""; }; 56 | AB0D004B2A40B8900019D62F /* DXVKInstallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXVKInstallView.swift; sourceTree = ""; }; 57 | AB0D004D2A40E3D30019D62F /* EnvironmentVarsEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentVarsEditor.swift; sourceTree = ""; }; 58 | AB0F6E232A4AEDDA00F642C7 /* BottleIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = BottleIcon.icns; sourceTree = ""; }; 59 | AB157BBD2B41289C00757AE0 /* TaskControllerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskControllerView.swift; sourceTree = ""; }; 60 | AB19DA962A358FE400967784 /* Localizable.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 61 | AB3BE5ED2A32248D00358BBC /* LaunchExtDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchExtDropdown.swift; sourceTree = ""; }; 62 | AB4707BF2A30C2C60019DC5F /* Harbor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Harbor.entitlements; sourceTree = ""; }; 63 | AB5CC6BC2A308BBB00AEBB2B /* Harbor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Harbor.app; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | AB5CC6BF2A308BBB00AEBB2B /* HarborApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HarborApp.swift; sourceTree = ""; }; 65 | AB5CC6C12A308BBB00AEBB2B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 66 | AB5CC6C32A308BBC00AEBB2B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 67 | AB5CC6C62A308BBC00AEBB2B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 68 | AB5CC6D52A30910300AEBB2B /* GPKUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPKUtils.swift; sourceTree = ""; }; 69 | AB5CC6D72A30938A00AEBB2B /* BrewInstallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewInstallView.swift; sourceTree = ""; }; 70 | AB5D49C52A3B5B0B008245A6 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; 71 | AB6254332A3CE30D002A6206 /* BottleCardListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleCardListView.swift; sourceTree = ""; }; 72 | AB6652C12A33342B00F3FC5D /* XCLIInstallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCLIInstallView.swift; sourceTree = ""; }; 73 | AB6652C32A3334E600F3FC5D /* XCLIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCLIUtils.swift; sourceTree = ""; }; 74 | AB6652C82A3349A400F3FC5D /* HarborMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HarborMenu.swift; sourceTree = ""; }; 75 | AB6652CC2A3350EC00F3FC5D /* BottleConfigDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleConfigDropdown.swift; sourceTree = ""; }; 76 | AB6A967E2A31045A003A019E /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 77 | AB7A80FF2A30CC7100AA71A6 /* BrewUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewUtils.swift; sourceTree = ""; }; 78 | AB7A81012A30D2FE00AA71A6 /* HarborUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HarborUtils.swift; sourceTree = ""; }; 79 | AB7D8E322A4DDE3400B55527 /* WinetricksUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinetricksUtils.swift; sourceTree = ""; }; 80 | AB87CAF32A4AC67C00C32025 /* HarborShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HarborShortcuts.swift; sourceTree = ""; }; 81 | AB95D9FC2A5011C5003402D2 /* GPTKConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPTKConfigView.swift; sourceTree = ""; }; 82 | ABB06E112AF559DB0078DE28 /* WinetricksUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinetricksUI.swift; sourceTree = ""; }; 83 | ABC0BFCD2A31627400382A42 /* BottleManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleManagementView.swift; sourceTree = ""; }; 84 | ABC0BFD02A31629300382A42 /* HarborBottle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HarborBottle.swift; sourceTree = ""; }; 85 | ABC0BFD32A31691500382A42 /* BottleOpsDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleOpsDropdown.swift; sourceTree = ""; }; 86 | ABD56AB12A36E419002A439C /* GPKSafeInstallView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPKSafeInstallView.swift; sourceTree = ""; }; 87 | ABD56AB22A36E419002A439C /* GPKFastInstallView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPKFastInstallView.swift; sourceTree = ""; }; 88 | ABDA74582AD904E700802792 /* BottleDetailsCommonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleDetailsCommonView.swift; sourceTree = ""; }; 89 | ABF534092A3F606A0030677A /* ViewMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewMenu.swift; sourceTree = ""; }; 90 | ABF5340B2A3F63BE0030677A /* BottleManagementCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleManagementCardView.swift; sourceTree = ""; }; 91 | ABF5340D2A3F63FE0030677A /* BottleManagementTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleManagementTableView.swift; sourceTree = ""; }; 92 | FC682ED92A35FD57000C53D6 /* Environment+GPKUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+GPKUtils.swift"; sourceTree = ""; }; 93 | FC682EDB2A3601E0000C53D6 /* Environment+BrewUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+BrewUtils.swift"; sourceTree = ""; }; 94 | FC682EDE2A3604B4000C53D6 /* Environment+XCLIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+XCLIUtils.swift"; sourceTree = ""; }; 95 | /* End PBXFileReference section */ 96 | 97 | /* Begin PBXFrameworksBuildPhase section */ 98 | AB5CC6B92A308BBB00AEBB2B /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | ); 103 | runOnlyForDeploymentPostprocessing = 0; 104 | }; 105 | /* End PBXFrameworksBuildPhase section */ 106 | 107 | /* Begin PBXGroup section */ 108 | AB0F6E222A4AEDB900F642C7 /* Resources */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | AB0F6E232A4AEDDA00F642C7 /* BottleIcon.icns */, 112 | ); 113 | path = Resources; 114 | sourceTree = ""; 115 | }; 116 | AB5CC6B32A308BBB00AEBB2B = { 117 | isa = PBXGroup; 118 | children = ( 119 | AB4707BF2A30C2C60019DC5F /* Harbor.entitlements */, 120 | AB5CC6BE2A308BBB00AEBB2B /* Harbor */, 121 | AB5CC6BD2A308BBB00AEBB2B /* Products */, 122 | ); 123 | sourceTree = ""; 124 | }; 125 | AB5CC6BD2A308BBB00AEBB2B /* Products */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | AB5CC6BC2A308BBB00AEBB2B /* Harbor.app */, 129 | ); 130 | name = Products; 131 | sourceTree = ""; 132 | }; 133 | AB5CC6BE2A308BBB00AEBB2B /* Harbor */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | AB5D49C42A3B4F93008245A6 /* Extensions */, 137 | ABC0BFCF2A31628600382A42 /* Models */, 138 | AB5CC6C52A308BBC00AEBB2B /* Preview Content */, 139 | AB0F6E222A4AEDB900F642C7 /* Resources */, 140 | FC682EDD2A360480000C53D6 /* Systems */, 141 | AB5CC6CE2A308CE200AEBB2B /* UIElements */, 142 | AB5CC6D42A3090E800AEBB2B /* Utils */, 143 | AB6A967D2A310443003A019E /* Views */, 144 | AB5CC6C12A308BBB00AEBB2B /* ContentView.swift */, 145 | AB5CC6BF2A308BBB00AEBB2B /* HarborApp.swift */, 146 | AB5CC6C32A308BBC00AEBB2B /* Assets.xcassets */, 147 | AB19DA962A358FE400967784 /* Localizable.xcstrings */, 148 | ); 149 | path = Harbor; 150 | sourceTree = ""; 151 | }; 152 | AB5CC6C52A308BBC00AEBB2B /* Preview Content */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | AB5CC6C62A308BBC00AEBB2B /* Preview Assets.xcassets */, 156 | ); 157 | path = "Preview Content"; 158 | sourceTree = ""; 159 | }; 160 | AB5CC6CE2A308CE200AEBB2B /* UIElements */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | ABB06E132AF576120078DE28 /* Winetricks */, 164 | ABD06FDD2A74D07E00605AC9 /* MenuCommands */, 165 | AB6652C72A33499700F3FC5D /* Commands */, 166 | ABC0BFD22A31690500382A42 /* BottleManagement */, 167 | AB5CC6CF2A308CFF00AEBB2B /* GPKInstall */, 168 | ); 169 | path = UIElements; 170 | sourceTree = ""; 171 | }; 172 | AB5CC6CF2A308CFF00AEBB2B /* GPKInstall */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | ABD56AB22A36E419002A439C /* GPKFastInstallView.swift */, 176 | ABD56AB12A36E419002A439C /* GPKSafeInstallView.swift */, 177 | AB5CC6D72A30938A00AEBB2B /* BrewInstallView.swift */, 178 | AB6652C12A33342B00F3FC5D /* XCLIInstallView.swift */, 179 | ); 180 | path = GPKInstall; 181 | sourceTree = ""; 182 | }; 183 | AB5CC6D42A3090E800AEBB2B /* Utils */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | AB7A81012A30D2FE00AA71A6 /* HarborUtils.swift */, 187 | FC682ED92A35FD57000C53D6 /* Environment+GPKUtils.swift */, 188 | FC682EDB2A3601E0000C53D6 /* Environment+BrewUtils.swift */, 189 | FC682EDE2A3604B4000C53D6 /* Environment+XCLIUtils.swift */, 190 | AB0D00492A40B7670019D62F /* MenuUIStates.swift */, 191 | AB87CAF32A4AC67C00C32025 /* HarborShortcuts.swift */, 192 | ); 193 | path = Utils; 194 | sourceTree = ""; 195 | }; 196 | AB5D49C42A3B4F93008245A6 /* Extensions */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | AB5D49C52A3B5B0B008245A6 /* URLExtensions.swift */, 200 | ); 201 | path = Extensions; 202 | sourceTree = ""; 203 | }; 204 | AB6652C72A33499700F3FC5D /* Commands */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | AB6652C82A3349A400F3FC5D /* HarborMenu.swift */, 208 | ABF534092A3F606A0030677A /* ViewMenu.swift */, 209 | ); 210 | path = Commands; 211 | sourceTree = ""; 212 | }; 213 | AB6A967D2A310443003A019E /* Views */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | AB6A967E2A31045A003A019E /* SetupView.swift */, 217 | ABC0BFCD2A31627400382A42 /* BottleManagementView.swift */, 218 | ABF5340D2A3F63FE0030677A /* BottleManagementTableView.swift */, 219 | ABF5340B2A3F63BE0030677A /* BottleManagementCardView.swift */, 220 | ); 221 | path = Views; 222 | sourceTree = ""; 223 | }; 224 | ABB06E102AF559C90078DE28 /* Winetricks */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | AB7D8E322A4DDE3400B55527 /* WinetricksUtils.swift */, 228 | ); 229 | path = Winetricks; 230 | sourceTree = ""; 231 | }; 232 | ABB06E132AF576120078DE28 /* Winetricks */ = { 233 | isa = PBXGroup; 234 | children = ( 235 | ABB06E112AF559DB0078DE28 /* WinetricksUI.swift */, 236 | ); 237 | path = Winetricks; 238 | sourceTree = ""; 239 | }; 240 | ABC0BFCF2A31628600382A42 /* Models */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | ABC0BFD02A31629300382A42 /* HarborBottle.swift */, 244 | ); 245 | path = Models; 246 | sourceTree = ""; 247 | }; 248 | ABC0BFD22A31690500382A42 /* BottleManagement */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | ABC0BFD32A31691500382A42 /* BottleOpsDropdown.swift */, 252 | AB3BE5ED2A32248D00358BBC /* LaunchExtDropdown.swift */, 253 | AB6652CC2A3350EC00F3FC5D /* BottleConfigDropdown.swift */, 254 | AB6254332A3CE30D002A6206 /* BottleCardListView.swift */, 255 | AB0D004D2A40E3D30019D62F /* EnvironmentVarsEditor.swift */, 256 | ABDA74582AD904E700802792 /* BottleDetailsCommonView.swift */, 257 | AB157BBD2B41289C00757AE0 /* TaskControllerView.swift */, 258 | ); 259 | path = BottleManagement; 260 | sourceTree = ""; 261 | }; 262 | ABD06FDD2A74D07E00605AC9 /* MenuCommands */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | AB0D004B2A40B8900019D62F /* DXVKInstallView.swift */, 266 | AB95D9FC2A5011C5003402D2 /* GPTKConfigView.swift */, 267 | ); 268 | path = MenuCommands; 269 | sourceTree = ""; 270 | }; 271 | FC682EDD2A360480000C53D6 /* Systems */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | AB7A80FF2A30CC7100AA71A6 /* BrewUtils.swift */, 275 | AB5CC6D52A30910300AEBB2B /* GPKUtils.swift */, 276 | AB6652C32A3334E600F3FC5D /* XCLIUtils.swift */, 277 | AB0D00452A40B27F0019D62F /* DXUtils.swift */, 278 | AB0D00432A40A3630019D62F /* BottleDX.swift */, 279 | ABB06E102AF559C90078DE28 /* Winetricks */, 280 | ); 281 | path = Systems; 282 | sourceTree = ""; 283 | }; 284 | /* End PBXGroup section */ 285 | 286 | /* Begin PBXNativeTarget section */ 287 | AB5CC6BB2A308BBB00AEBB2B /* Harbor */ = { 288 | isa = PBXNativeTarget; 289 | buildConfigurationList = AB5CC6CB2A308BBC00AEBB2B /* Build configuration list for PBXNativeTarget "Harbor" */; 290 | buildPhases = ( 291 | AB8747542A3630620099522A /* ShellScript */, 292 | AB5CC6B82A308BBB00AEBB2B /* Sources */, 293 | AB5CC6B92A308BBB00AEBB2B /* Frameworks */, 294 | AB5CC6BA2A308BBB00AEBB2B /* Resources */, 295 | ); 296 | buildRules = ( 297 | ); 298 | dependencies = ( 299 | ); 300 | name = Harbor; 301 | productName = Harbor; 302 | productReference = AB5CC6BC2A308BBB00AEBB2B /* Harbor.app */; 303 | productType = "com.apple.product-type.application"; 304 | }; 305 | /* End PBXNativeTarget section */ 306 | 307 | /* Begin PBXProject section */ 308 | AB5CC6B42A308BBB00AEBB2B /* Project object */ = { 309 | isa = PBXProject; 310 | attributes = { 311 | BuildIndependentTargetsInParallel = 1; 312 | LastSwiftUpdateCheck = 1430; 313 | LastUpgradeCheck = 1500; 314 | TargetAttributes = { 315 | AB5CC6BB2A308BBB00AEBB2B = { 316 | CreatedOnToolsVersion = 14.3.1; 317 | }; 318 | }; 319 | }; 320 | buildConfigurationList = AB5CC6B72A308BBB00AEBB2B /* Build configuration list for PBXProject "Harbor" */; 321 | compatibilityVersion = "Xcode 14.0"; 322 | developmentRegion = en; 323 | hasScannedForEncodings = 0; 324 | knownRegions = ( 325 | en, 326 | Base, 327 | vi, 328 | pl, 329 | ); 330 | mainGroup = AB5CC6B32A308BBB00AEBB2B; 331 | productRefGroup = AB5CC6BD2A308BBB00AEBB2B /* Products */; 332 | projectDirPath = ""; 333 | projectRoot = ""; 334 | targets = ( 335 | AB5CC6BB2A308BBB00AEBB2B /* Harbor */, 336 | ); 337 | }; 338 | /* End PBXProject section */ 339 | 340 | /* Begin PBXResourcesBuildPhase section */ 341 | AB5CC6BA2A308BBB00AEBB2B /* Resources */ = { 342 | isa = PBXResourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | AB5CC6C72A308BBC00AEBB2B /* Preview Assets.xcassets in Resources */, 346 | AB0F6E242A4AEDDA00F642C7 /* BottleIcon.icns in Resources */, 347 | AB5CC6C42A308BBC00AEBB2B /* Assets.xcassets in Resources */, 348 | AB19DA972A358FE400967784 /* Localizable.xcstrings in Resources */, 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | }; 352 | /* End PBXResourcesBuildPhase section */ 353 | 354 | /* Begin PBXShellScriptBuildPhase section */ 355 | AB8747542A3630620099522A /* ShellScript */ = { 356 | isa = PBXShellScriptBuildPhase; 357 | alwaysOutOfDate = 1; 358 | buildActionMask = 2147483647; 359 | files = ( 360 | ); 361 | inputFileListPaths = ( 362 | ); 363 | inputPaths = ( 364 | ); 365 | outputFileListPaths = ( 366 | ); 367 | outputPaths = ( 368 | ); 369 | runOnlyForDeploymentPostprocessing = 0; 370 | shellPath = /bin/sh; 371 | shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 372 | }; 373 | /* End PBXShellScriptBuildPhase section */ 374 | 375 | /* Begin PBXSourcesBuildPhase section */ 376 | AB5CC6B82A308BBB00AEBB2B /* Sources */ = { 377 | isa = PBXSourcesBuildPhase; 378 | buildActionMask = 2147483647; 379 | files = ( 380 | ABF5340E2A3F63FE0030677A /* BottleManagementTableView.swift in Sources */, 381 | AB5CC6D82A30938A00AEBB2B /* BrewInstallView.swift in Sources */, 382 | ABD56AB42A36E419002A439C /* GPKFastInstallView.swift in Sources */, 383 | FC682EDA2A35FD57000C53D6 /* Environment+GPKUtils.swift in Sources */, 384 | AB0D00442A40A3630019D62F /* BottleDX.swift in Sources */, 385 | AB0D004C2A40B8900019D62F /* DXVKInstallView.swift in Sources */, 386 | AB0D004E2A40E3D30019D62F /* EnvironmentVarsEditor.swift in Sources */, 387 | AB7A81002A30CC7100AA71A6 /* BrewUtils.swift in Sources */, 388 | AB6652CD2A3350EC00F3FC5D /* BottleConfigDropdown.swift in Sources */, 389 | AB5CC6D62A30910300AEBB2B /* GPKUtils.swift in Sources */, 390 | ABDA74592AD904E700802792 /* BottleDetailsCommonView.swift in Sources */, 391 | AB3BE5EE2A32248D00358BBC /* LaunchExtDropdown.swift in Sources */, 392 | AB5D49C62A3B5B0B008245A6 /* URLExtensions.swift in Sources */, 393 | AB5CC6C22A308BBB00AEBB2B /* ContentView.swift in Sources */, 394 | FC682EDC2A3601E0000C53D6 /* Environment+BrewUtils.swift in Sources */, 395 | ABD56AB32A36E419002A439C /* GPKSafeInstallView.swift in Sources */, 396 | AB5CC6C02A308BBB00AEBB2B /* HarborApp.swift in Sources */, 397 | AB7D8E332A4DDE3400B55527 /* WinetricksUtils.swift in Sources */, 398 | AB7A81022A30D2FE00AA71A6 /* HarborUtils.swift in Sources */, 399 | AB95D9FD2A5011C5003402D2 /* GPTKConfigView.swift in Sources */, 400 | ABF5340C2A3F63BE0030677A /* BottleManagementCardView.swift in Sources */, 401 | AB157BBE2B41289D00757AE0 /* TaskControllerView.swift in Sources */, 402 | AB6652C42A3334E600F3FC5D /* XCLIUtils.swift in Sources */, 403 | AB6254342A3CE30D002A6206 /* BottleCardListView.swift in Sources */, 404 | AB6A967F2A31045A003A019E /* SetupView.swift in Sources */, 405 | ABC0BFD12A31629300382A42 /* HarborBottle.swift in Sources */, 406 | AB0D004A2A40B7670019D62F /* MenuUIStates.swift in Sources */, 407 | ABB06E122AF559DB0078DE28 /* WinetricksUI.swift in Sources */, 408 | ABC0BFCE2A31627400382A42 /* BottleManagementView.swift in Sources */, 409 | AB87CAF42A4AC67C00C32025 /* HarborShortcuts.swift in Sources */, 410 | AB6652C22A33342B00F3FC5D /* XCLIInstallView.swift in Sources */, 411 | ABF5340A2A3F606A0030677A /* ViewMenu.swift in Sources */, 412 | AB6652C92A3349A400F3FC5D /* HarborMenu.swift in Sources */, 413 | AB0D00462A40B27F0019D62F /* DXUtils.swift in Sources */, 414 | FC682EDF2A3604B4000C53D6 /* Environment+XCLIUtils.swift in Sources */, 415 | ABC0BFD42A31691500382A42 /* BottleOpsDropdown.swift in Sources */, 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | }; 419 | /* End PBXSourcesBuildPhase section */ 420 | 421 | /* Begin XCBuildConfiguration section */ 422 | AB5CC6C92A308BBC00AEBB2B /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ALWAYS_SEARCH_USER_PATHS = NO; 426 | CLANG_ANALYZER_NONNULL = YES; 427 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 428 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 429 | CLANG_ENABLE_MODULES = YES; 430 | CLANG_ENABLE_OBJC_ARC = YES; 431 | CLANG_ENABLE_OBJC_WEAK = YES; 432 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 433 | CLANG_WARN_BOOL_CONVERSION = YES; 434 | CLANG_WARN_COMMA = YES; 435 | CLANG_WARN_CONSTANT_CONVERSION = YES; 436 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 437 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 438 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 439 | CLANG_WARN_EMPTY_BODY = YES; 440 | CLANG_WARN_ENUM_CONVERSION = YES; 441 | CLANG_WARN_INFINITE_RECURSION = YES; 442 | CLANG_WARN_INT_CONVERSION = YES; 443 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 444 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 445 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 446 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 447 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 448 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 449 | CLANG_WARN_STRICT_PROTOTYPES = YES; 450 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 451 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 452 | CLANG_WARN_UNREACHABLE_CODE = YES; 453 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 454 | COPY_PHASE_STRIP = NO; 455 | DEAD_CODE_STRIPPING = YES; 456 | DEBUG_INFORMATION_FORMAT = dwarf; 457 | ENABLE_STRICT_OBJC_MSGSEND = YES; 458 | ENABLE_TESTABILITY = YES; 459 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 460 | GCC_C_LANGUAGE_STANDARD = gnu11; 461 | GCC_DYNAMIC_NO_PIC = NO; 462 | GCC_NO_COMMON_BLOCKS = YES; 463 | GCC_OPTIMIZATION_LEVEL = 0; 464 | GCC_PREPROCESSOR_DEFINITIONS = ( 465 | "DEBUG=1", 466 | "$(inherited)", 467 | ); 468 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 469 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 470 | GCC_WARN_UNDECLARED_SELECTOR = YES; 471 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 472 | GCC_WARN_UNUSED_FUNCTION = YES; 473 | GCC_WARN_UNUSED_VARIABLE = YES; 474 | MACOSX_DEPLOYMENT_TARGET = 14.0; 475 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 476 | MTL_FAST_MATH = YES; 477 | ONLY_ACTIVE_ARCH = YES; 478 | SDKROOT = macosx; 479 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 480 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 481 | }; 482 | name = Debug; 483 | }; 484 | AB5CC6CA2A308BBC00AEBB2B /* Release */ = { 485 | isa = XCBuildConfiguration; 486 | buildSettings = { 487 | ALWAYS_SEARCH_USER_PATHS = NO; 488 | CLANG_ANALYZER_NONNULL = YES; 489 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 490 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 491 | CLANG_ENABLE_MODULES = YES; 492 | CLANG_ENABLE_OBJC_ARC = YES; 493 | CLANG_ENABLE_OBJC_WEAK = YES; 494 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 495 | CLANG_WARN_BOOL_CONVERSION = YES; 496 | CLANG_WARN_COMMA = YES; 497 | CLANG_WARN_CONSTANT_CONVERSION = YES; 498 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 499 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 500 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 501 | CLANG_WARN_EMPTY_BODY = YES; 502 | CLANG_WARN_ENUM_CONVERSION = YES; 503 | CLANG_WARN_INFINITE_RECURSION = YES; 504 | CLANG_WARN_INT_CONVERSION = YES; 505 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 506 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 507 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 508 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 509 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 510 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 511 | CLANG_WARN_STRICT_PROTOTYPES = YES; 512 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 513 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 514 | CLANG_WARN_UNREACHABLE_CODE = YES; 515 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 516 | COPY_PHASE_STRIP = NO; 517 | DEAD_CODE_STRIPPING = YES; 518 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 519 | ENABLE_NS_ASSERTIONS = NO; 520 | ENABLE_STRICT_OBJC_MSGSEND = YES; 521 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 522 | GCC_C_LANGUAGE_STANDARD = gnu11; 523 | GCC_NO_COMMON_BLOCKS = YES; 524 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 525 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 526 | GCC_WARN_UNDECLARED_SELECTOR = YES; 527 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 528 | GCC_WARN_UNUSED_FUNCTION = YES; 529 | GCC_WARN_UNUSED_VARIABLE = YES; 530 | MACOSX_DEPLOYMENT_TARGET = 14.0; 531 | MTL_ENABLE_DEBUG_INFO = NO; 532 | MTL_FAST_MATH = YES; 533 | SDKROOT = macosx; 534 | SWIFT_COMPILATION_MODE = wholemodule; 535 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 536 | }; 537 | name = Release; 538 | }; 539 | AB5CC6CC2A308BBC00AEBB2B /* Debug */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 543 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 544 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; 545 | CODE_SIGN_ENTITLEMENTS = Harbor/Harbor.entitlements; 546 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 547 | CODE_SIGN_STYLE = Automatic; 548 | COMBINE_HIDPI_IMAGES = YES; 549 | CURRENT_PROJECT_VERSION = 8; 550 | DEAD_CODE_STRIPPING = YES; 551 | DEVELOPMENT_ASSET_PATHS = "\"Harbor/Preview Content\""; 552 | DEVELOPMENT_TEAM = PSAZVTU7XU; 553 | ENABLE_HARDENED_RUNTIME = YES; 554 | ENABLE_PREVIEWS = YES; 555 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 556 | GENERATE_INFOPLIST_FILE = YES; 557 | INFOPLIST_FILE = Harbor/Info.plist; 558 | INFOPLIST_KEY_CFBundleDisplayName = Harbor; 559 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 560 | INFOPLIST_KEY_NSAppleEventsUsageDescription = "Harbor needs your permission to run terminal commands"; 561 | INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Harbor needs to access your Documents folder to look for the GPK"; 562 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 563 | LD_RUNPATH_SEARCH_PATHS = ( 564 | "$(inherited)", 565 | "@executable_path/../Frameworks", 566 | ); 567 | MACOSX_DEPLOYMENT_TARGET = 14.0; 568 | MARKETING_VERSION = 0.3.2; 569 | PRODUCT_BUNDLE_IDENTIFIER = dev.ohaiibuzzle.Harbor; 570 | PRODUCT_NAME = "$(TARGET_NAME)"; 571 | SWIFT_EMIT_LOC_STRINGS = YES; 572 | SWIFT_VERSION = 5.0; 573 | }; 574 | name = Debug; 575 | }; 576 | AB5CC6CD2A308BBC00AEBB2B /* Release */ = { 577 | isa = XCBuildConfiguration; 578 | buildSettings = { 579 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 580 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 581 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; 582 | CODE_SIGN_ENTITLEMENTS = Harbor/Harbor.entitlements; 583 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 584 | CODE_SIGN_STYLE = Automatic; 585 | COMBINE_HIDPI_IMAGES = YES; 586 | CURRENT_PROJECT_VERSION = 8; 587 | DEAD_CODE_STRIPPING = YES; 588 | DEVELOPMENT_ASSET_PATHS = "\"Harbor/Preview Content\""; 589 | DEVELOPMENT_TEAM = PSAZVTU7XU; 590 | ENABLE_HARDENED_RUNTIME = YES; 591 | ENABLE_PREVIEWS = YES; 592 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 593 | GENERATE_INFOPLIST_FILE = YES; 594 | INFOPLIST_FILE = Harbor/Info.plist; 595 | INFOPLIST_KEY_CFBundleDisplayName = Harbor; 596 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 597 | INFOPLIST_KEY_NSAppleEventsUsageDescription = "Harbor needs your permission to run terminal commands"; 598 | INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "Harbor needs to access your Documents folder to look for the GPK"; 599 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 600 | LD_RUNPATH_SEARCH_PATHS = ( 601 | "$(inherited)", 602 | "@executable_path/../Frameworks", 603 | ); 604 | MACOSX_DEPLOYMENT_TARGET = 14.0; 605 | MARKETING_VERSION = 0.3.2; 606 | PRODUCT_BUNDLE_IDENTIFIER = dev.ohaiibuzzle.Harbor; 607 | PRODUCT_NAME = "$(TARGET_NAME)"; 608 | SWIFT_EMIT_LOC_STRINGS = YES; 609 | SWIFT_VERSION = 5.0; 610 | }; 611 | name = Release; 612 | }; 613 | /* End XCBuildConfiguration section */ 614 | 615 | /* Begin XCConfigurationList section */ 616 | AB5CC6B72A308BBB00AEBB2B /* Build configuration list for PBXProject "Harbor" */ = { 617 | isa = XCConfigurationList; 618 | buildConfigurations = ( 619 | AB5CC6C92A308BBC00AEBB2B /* Debug */, 620 | AB5CC6CA2A308BBC00AEBB2B /* Release */, 621 | ); 622 | defaultConfigurationIsVisible = 0; 623 | defaultConfigurationName = Release; 624 | }; 625 | AB5CC6CB2A308BBC00AEBB2B /* Build configuration list for PBXNativeTarget "Harbor" */ = { 626 | isa = XCConfigurationList; 627 | buildConfigurations = ( 628 | AB5CC6CC2A308BBC00AEBB2B /* Debug */, 629 | AB5CC6CD2A308BBC00AEBB2B /* Release */, 630 | ); 631 | defaultConfigurationIsVisible = 0; 632 | defaultConfigurationName = Release; 633 | }; 634 | /* End XCConfigurationList section */ 635 | }; 636 | rootObject = AB5CC6B42A308BBB00AEBB2B /* Project object */; 637 | } 638 | -------------------------------------------------------------------------------- /Harbor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Harbor.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Harbor.xcodeproj/xcshareddata/xcschemes/Harbor.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Assets.xcassets/AppIcon.appiconset/AppIcon-64.png -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "AppIcon-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "AppIcon-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "AppIcon-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "AppIcon-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "AppIcon-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "AppIcon-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "AppIcon-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "AppIcon-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Harbor/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Harbor/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @Environment(\.gpkUtils) 12 | private var gpkUtils 13 | 14 | @Bindable var menuUIStates: MenuUIStates 15 | 16 | var body: some View { 17 | if gpkUtils.status != .installed { 18 | // GPK is not installed 19 | SetupView() 20 | } else { 21 | BottleManagementView(menuUIStates: menuUIStates) 22 | } 23 | } 24 | } 25 | 26 | struct ContentView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | ContentView(menuUIStates: MenuUIStates()) 29 | .environment(\.gpkUtils, .init()) 30 | .environment(\.brewUtils, .init()) 31 | .environment(\.xcliUtils, .init()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Harbor/Dialogs/BottleManagement/NewBottleDropdown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewBottleDropdown.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 08/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewBottleDropdown: View { 11 | @Binding var isPresented: Bool 12 | @Binding var bottle: BottleModel 13 | var editingMode: Bool = false 14 | 15 | @State var bottleName = "" 16 | 17 | var body: some View { 18 | VStack { 19 | Text("New Bottle") 20 | .font(.title) 21 | .padding() 22 | 23 | HStack { 24 | Text("Name") 25 | TextField("My Bottle", text: $bottle.name) 26 | } 27 | 28 | // Browsable file picker for new bottle folder 29 | HStack { 30 | Group { 31 | Text("Path") 32 | TextField("", text: $bottleName) 33 | Button("Browse") { 34 | let dialog = NSOpenPanel() 35 | dialog.title = "Choose a folder for your new bottle" 36 | dialog.showsResizeIndicator = true 37 | dialog.showsHiddenFiles = false 38 | dialog.canChooseDirectories = true 39 | dialog.canChooseFiles = false 40 | dialog.canCreateDirectories = true 41 | dialog.allowsMultipleSelection = false 42 | dialog.directoryURL = FileManager.default 43 | .urls(for: .documentDirectory, in: .userDomainMask).first 44 | if dialog.runModal() == NSApplication.ModalResponse.OK { 45 | if let result = dialog.url { 46 | bottleName = result.path 47 | } 48 | } else { 49 | // User clicked on "Cancel" 50 | return 51 | } 52 | } 53 | } 54 | .onChange(of: bottleName, perform: { value in 55 | bottle.path = URL(fileURLWithPath: value) 56 | }) 57 | } 58 | 59 | if editingMode { 60 | // Primary application 61 | HStack { 62 | Text("Primary Application") 63 | TextField("MyApp.exe", text: $bottle.primaryApplicationPath) 64 | } 65 | HStack { 66 | Text("Primary Application Argument") 67 | TextField("", text: $bottle.primaryApplicationArgument) 68 | } 69 | } 70 | 71 | // Cancel and Create buttons 72 | HStack { 73 | Button("Cancel") { 74 | isPresented = false 75 | } 76 | Button(editingMode ? "Save" : "Create") { 77 | if editingMode { 78 | // Save the bottle 79 | BottleLoader.shared.bottles = BottleLoader.shared.bottles.map { (bottle) -> BottleModel in 80 | if bottle.id == self.bottle.id { 81 | return self.bottle 82 | } else { 83 | return bottle 84 | } 85 | } 86 | } else { 87 | // Create the bottle 88 | let newBottle = BottleModel(id: UUID(), name: bottle.name, path: bottle.path) 89 | BottleLoader.shared.bottles.append(newBottle) 90 | } 91 | } 92 | } 93 | } 94 | .padding() 95 | .frame(width: 400, height: 200) 96 | } 97 | } 98 | 99 | struct EditBottleView: View { 100 | @Binding var isPresented: Bool 101 | @Binding var bottle: BottleModel 102 | 103 | @State var bottleName = "" 104 | 105 | var body: some View { 106 | // Basically reuse New in editing mode 107 | NewBottleDropdown(isPresented: $isPresented, bottle: $bottle, editingMode: true) 108 | } 109 | 110 | } 111 | 112 | #Preview { 113 | NewBottleDropdown(isPresented: Binding.constant(true), 114 | bottle: Binding.constant(BottleModel( 115 | id: UUID(), name: "My Bottle", path: URL(fileURLWithPath: "/Users/venti/Documents/My Bottle")))) 116 | } 117 | -------------------------------------------------------------------------------- /Harbor/Extensions/URLExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLExtensions.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 15/06/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | var prettyFileUrl: String { 12 | if !self.isFileURL { 13 | return self.absoluteString 14 | } 15 | guard var prettyPath = self.path.removingPercentEncoding else { 16 | return self.path 17 | } 18 | 19 | if path.hasPrefix("/Users") { 20 | // Remove /Users/ and replace with ~ 21 | prettyPath = prettyPath.replacingOccurrences(of: #"/Users/[^/]+/"#, 22 | with: "~/", options: .regularExpression) 23 | } 24 | 25 | return prettyPath 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Harbor/Harbor.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Harbor/HarborApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HarborApp.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct HarborApp: App { 12 | @State var menuUIStates = MenuUIStates() 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView(menuUIStates: menuUIStates) 17 | .environment(\.gpkUtils, .init()) 18 | .environment(\.brewUtils, .init()) 19 | .environment(\.xcliUtils, .init()) 20 | } 21 | .commands { 22 | HarborMenu(menuUIStates: menuUIStates) 23 | ViewMenu() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Harbor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeName 9 | MSI Package 10 | CFBundleTypeRole 11 | Editor 12 | LSHandlerRank 13 | Default 14 | LSItemContentTypes 15 | 16 | harbor-msi 17 | 18 | NSDocumentClass 19 | NSDocument 20 | 21 | 22 | UTImportedTypeDeclarations 23 | 24 | 25 | UTTypeConformsTo 26 | 27 | public.data 28 | 29 | UTTypeDescription 30 | Windows Installer Package 31 | UTTypeIcons 32 | 33 | UTTypeIdentifier 34 | harbor.msi-package 35 | UTTypeTagSpecification 36 | 37 | public.filename-extension 38 | 39 | msi 40 | 41 | public.mime-type 42 | 43 | application/x-ms-installer 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Harbor/Models/HarborBottle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleModel.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 08/06/2023. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import Observation 11 | 12 | enum WineSyncronizationPrimatives: String, Codable { 13 | case none = "None" 14 | case eSync = "ESync" 15 | case mSync = "MSync" 16 | } 17 | 18 | struct HarborBottle: Identifiable, Equatable, Codable { 19 | var id: UUID 20 | var name: String = "New Bottle" 21 | var path: URL 22 | var primaryApplicationPath: String = "" 23 | var primaryApplicationArgument: String = "" 24 | var primaryApplicationWorkDir: String = "" 25 | var enableHUD: Bool = false 26 | var syncPrimitives: WineSyncronizationPrimatives = .none 27 | var pleaseShutUp: Bool = true 28 | var envVars: [String: String] = .init() 29 | 30 | // swiftlint:disable cyclomatic_complexity 31 | // This function is a bit too complex right now 32 | func launchApplication(_ application: String, arguments: [String] = [], environmentVars: [String: String] = [:], 33 | workDir: String = "", isUnixPath: Bool = false) { 34 | let task = Process() 35 | task.launchPath = "/usr/local/opt/game-porting-toolkit/bin/wine64" 36 | task.arguments = ["start"] 37 | 38 | if !workDir.isEmpty { 39 | task.arguments?.append("/d") 40 | task.arguments?.append(workDir) 41 | } 42 | 43 | if isUnixPath { 44 | task.arguments?.append("/unix") 45 | } 46 | task.arguments?.append(application) 47 | 48 | if !arguments.isEmpty { 49 | task.arguments?.append(contentsOf: arguments) 50 | } 51 | 52 | // task.environment = ["MTL_HUD_ENABLED": "1", "WINEESYNC": "1", "WINEPREFIX": path.path] 53 | task.environment = ["WINEPREFIX": path.path] 54 | 55 | if enableHUD { 56 | task.environment?["MTL_HUD_ENABLED"] = "1" 57 | } 58 | 59 | switch syncPrimitives { 60 | case .none: 61 | break 62 | case .eSync: 63 | task.environment?["WINEESYNC"] = "1" 64 | case .mSync: 65 | task.environment?["WINEMSYNC"] = "1" 66 | } 67 | 68 | if !envVars.isEmpty { 69 | for (key, value) in envVars { 70 | task.environment?[key] = value 71 | } 72 | } 73 | 74 | if pleaseShutUp { 75 | task.standardOutput = nil 76 | task.standardError = nil 77 | } 78 | 79 | do { 80 | try task.run() 81 | } catch { 82 | HarborUtils.shared.quickError(error.localizedDescription) 83 | } 84 | } 85 | // swiftlint:enable cyclomatic_complexity 86 | 87 | func launchExtApplication(_ application: String, arguments: [String] = [], environmentVars: [String: String] = [:], 88 | workDir: String = "") { 89 | launchApplication(application, arguments: arguments, environmentVars: environmentVars, 90 | workDir: workDir, isUnixPath: true) 91 | } 92 | 93 | func launchPrimaryApplication() { 94 | launchApplication(primaryApplicationPath, 95 | arguments: primaryApplicationArgument.split(separator: " ").map(String.init), 96 | environmentVars: envVars, 97 | workDir: primaryApplicationWorkDir) 98 | } 99 | 100 | @discardableResult 101 | func directLaunchApplication(_ application: String, arguments: [String] = [], shouldWait: Bool = false) -> String { 102 | let task = Process() 103 | let pipe = Pipe() 104 | 105 | task.launchPath = "/usr/local/opt/game-porting-toolkit/bin/wine64" 106 | task.arguments = [application] 107 | task.standardOutput = pipe 108 | 109 | if !arguments.isEmpty { 110 | task.arguments?.append(contentsOf: arguments) 111 | } 112 | 113 | task.environment = ["WINEPREFIX": path.path] 114 | 115 | switch syncPrimitives { 116 | case .none: 117 | break 118 | case .eSync: 119 | task.environment?["WINEESYNC"] = "1" 120 | case .mSync: 121 | task.environment?["WINEMSYNC"] = "1" 122 | } 123 | 124 | do { 125 | try task.run() 126 | } catch { 127 | HarborUtils.shared.quickError(error.localizedDescription) 128 | } 129 | 130 | if shouldWait { 131 | task.waitUntilExit() 132 | } 133 | 134 | return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" 135 | } 136 | 137 | func pathFromUnixPath(_ unixPath: URL) -> String { 138 | let fullUnixPath = unixPath.path 139 | // trim everything up to and including the bottle name 140 | let bottlePath = fullUnixPath.replacingOccurrences(of: path.path, with: "") 141 | // trim the drive_c 142 | let driveCPath = bottlePath.replacingOccurrences(of: "/drive_c", with: "C:") 143 | // replace all slashes with backslashes 144 | let windowsPath = driveCPath.replacingOccurrences(of: "/", with: "\\") 145 | return windowsPath 146 | } 147 | 148 | func isAppOutsideBottle(_ unixPath: String) -> Bool { 149 | return !unixPath.contains(path.path) 150 | } 151 | 152 | func initializeBottle() { 153 | let task = Process() 154 | task.launchPath = "/usr/local/opt/game-porting-toolkit/bin/wine64" 155 | // Launch with WINE_PREFIX set to the bottle path 156 | task.environment = ["WINEPREFIX": path.path] 157 | 158 | // Run winecfg to bootstrap the bottle with a Windows 10 environment 159 | task.arguments = ["winecfg", "-v", "win10"] 160 | do { 161 | try task.run() 162 | } catch { 163 | HarborUtils.shared.quickError(error.localizedDescription) 164 | } 165 | task.waitUntilExit() 166 | } 167 | 168 | func killBottle() { 169 | // Run wineserver -k to kill the bottle 170 | let task = Process() 171 | task.launchPath = "/usr/local/opt/game-porting-toolkit/bin/wineserver" 172 | // Launch with WINE_PREFIX set to the bottle path 173 | task.environment = ["WINEPREFIX": path.path] 174 | task.arguments = ["-k"] 175 | 176 | do { 177 | try task.run() 178 | } catch { 179 | HarborUtils.shared.quickError(error.localizedDescription) 180 | } 181 | } 182 | 183 | init(from decoder: Decoder) throws { 184 | let container = try decoder.container(keyedBy: CodingKeys.self) 185 | id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() 186 | name = try container.decodeIfPresent(String.self, forKey: .name) ?? "New Bottle" 187 | path = try container.decodeIfPresent(URL.self, forKey: .path) ?? URL(fileURLWithPath: "") 188 | primaryApplicationPath = try container.decodeIfPresent(String.self, forKey: .primaryApplicationPath) ?? "" 189 | primaryApplicationArgument = try container 190 | .decodeIfPresent(String.self, forKey: .primaryApplicationArgument) ?? "" 191 | primaryApplicationWorkDir = try container.decodeIfPresent(String.self, forKey: .primaryApplicationWorkDir) ?? "" 192 | enableHUD = try container.decodeIfPresent(Bool.self, forKey: .enableHUD) ?? false 193 | syncPrimitives = try container 194 | .decodeIfPresent(WineSyncronizationPrimatives.self, forKey: .syncPrimitives) ?? .none 195 | pleaseShutUp = try container.decodeIfPresent(Bool.self, forKey: .pleaseShutUp) ?? true 196 | envVars = try container.decodeIfPresent([String: String].self, forKey: .envVars) ?? [:] 197 | } 198 | 199 | init (id: UUID, name: String, path: URL) { 200 | self.id = id 201 | self.name = name 202 | self.path = path 203 | } 204 | } 205 | 206 | @Observable 207 | class BottleList { 208 | var bottles: [HarborBottle] = BottleLoader.shared.load() 209 | 210 | func reload() { 211 | bottles = BottleLoader.shared.load() 212 | } 213 | 214 | func flush() { 215 | BottleLoader.shared.save(bottles) 216 | } 217 | } 218 | 219 | struct BottleLoader { 220 | static var shared = BottleLoader() 221 | 222 | var bottles: [HarborBottle] { 223 | get { 224 | return load() 225 | } 226 | set { 227 | save(newValue) 228 | } 229 | } 230 | 231 | func save(_ bottles: [HarborBottle]) { 232 | let containerHome = HarborUtils.shared.getContainerHome() 233 | let bottleListPath = containerHome.appendingPathComponent("bottles.json") 234 | let encoder = JSONEncoder() 235 | encoder.outputFormatting = .prettyPrinted 236 | do { 237 | let data = try encoder.encode(bottles) 238 | try data.write(to: bottleListPath) 239 | } catch { 240 | NSLog("Failed to save bottles.json") 241 | } 242 | } 243 | 244 | func load() -> [HarborBottle] { 245 | var bottles = [HarborBottle]() 246 | let containerHome = HarborUtils.shared.getContainerHome() 247 | // Load bottles.plist 248 | let bottleListPath = containerHome.appendingPathComponent("bottles.json") 249 | if FileManager.default.fileExists(atPath: bottleListPath.path) { 250 | let decoder = JSONDecoder() 251 | do { 252 | let data = try Data(contentsOf: bottleListPath) 253 | bottles = try decoder.decode([HarborBottle].self, from: data) 254 | } catch { 255 | NSLog("Failed to load bottles.json") 256 | } 257 | } 258 | return bottles 259 | } 260 | 261 | func delete(_ bottle: HarborBottle, _ checkbox: NSControl.StateValue) { 262 | var bottles = load() 263 | bottles.removeAll(where: { $0.id == bottle.id }) 264 | // Remove the bottle directory 265 | if checkbox == .on { 266 | try? FileManager.default.removeItem(at: bottle.path) 267 | } 268 | save(bottles) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /Harbor/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Harbor/Resources/BottleIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohaiibuzzle/Harbor/1633d5f0e50335675fd3a197dc4719671c370d3d/Harbor/Resources/BottleIcon.icns -------------------------------------------------------------------------------- /Harbor/Systems/BottleDX.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVKUtils.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 19/06/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DXBackend: String { 11 | case how = "did we get here?" 12 | case gptk = "GPTK" 13 | case dxvk = "DXVK" 14 | case wined3d = "WineD3D" 15 | } 16 | 17 | class BottleDX { 18 | static let shared = BottleDX() 19 | 20 | let dxvkOverrides = ["dxgi", "d3d9", "d3d10core", "d3d11"] 21 | let wined3dOverrides = ["dxgi", "d3d9", "d3d10", "d3d11", "d3d12"] 22 | 23 | func checkBottleBackend(for bottle: HarborBottle) -> DXBackend { 24 | let bottlePath = bottle.path 25 | // Check the backend.hrb file in the bottle's system32 26 | // If it doesn't exist -> GPTK 27 | // If it does exist -> check the contents (DXVK, WineD3D) 28 | let backendPath = bottlePath.appendingPathComponent("drive_c/windows/system32/backend.hrb") 29 | if FileManager.default.fileExists(atPath: backendPath.path) { 30 | do { 31 | let backendContents = try String(contentsOf: backendPath) 32 | if backendContents.contains("DXVK") { 33 | return .dxvk 34 | } else if backendContents.contains("WineD3D") { 35 | return .wined3d 36 | } 37 | } catch { 38 | HarborUtils.shared.quickError(error.localizedDescription) 39 | } 40 | } 41 | return .gptk 42 | } 43 | 44 | func updateDXBackend(for bottle: HarborBottle, using backend: DXBackend) { 45 | let bottlePath = bottle.path 46 | let backendKind = checkBottleBackend(for: bottle) 47 | if backendKind != .gptk { 48 | revertToGPTK(for: bottle) 49 | } 50 | switch backend { 51 | case .dxvk: 52 | installDXVK(for: bottle) 53 | case .wined3d: 54 | installWineD3D(for: bottle) 55 | default: 56 | return 57 | } 58 | // Create the backend.hrb file 59 | let backendPath = bottlePath.appendingPathComponent("drive_c/windows/system32/backend.hrb") 60 | do { 61 | try backend.rawValue.write(to: backendPath, atomically: true, encoding: .utf8) 62 | } catch { 63 | HarborUtils.shared.quickError(error.localizedDescription) 64 | } 65 | } 66 | 67 | func revertToGPTK(for bottle: HarborBottle) { 68 | let bottlePath = bottle.path 69 | let backendKind = checkBottleBackend(for: bottle) 70 | if backendKind == .gptk { 71 | return 72 | } 73 | switch backendKind { 74 | case .dxvk: 75 | removeDXVKFromBottle(bottle: bottle) 76 | case .wined3d: 77 | removeWineD3D(for: bottle) 78 | default: 79 | return 80 | } 81 | // Remove the backend.hrb file 82 | let backendPath = bottlePath.appendingPathComponent("drive_c/windows/system32/backend.hrb") 83 | if FileManager.default.fileExists(atPath: backendPath.path) { 84 | do { 85 | try FileManager.default.removeItem(at: backendPath) 86 | } catch { 87 | HarborUtils.shared.quickError(error.localizedDescription) 88 | } 89 | } 90 | } 91 | 92 | func installDXVK(for bottle: HarborBottle) { 93 | if !DXUtils.shared.isDXVKAvailable() { 94 | return 95 | } 96 | 97 | let bottlePath = bottle.path 98 | let dxvkDirPath = HarborUtils.shared.getContainerHome().appendingPathComponent("dxvk") 99 | 100 | for override in dxvkOverrides { 101 | let overridePath = bottlePath.appendingPathComponent("drive_c/windows/system32/\(override).dll") 102 | // rename the original dll to .orig 103 | if FileManager.default.fileExists(atPath: overridePath.path) { 104 | do { 105 | try FileManager.default.moveItem(at: overridePath, to: overridePath.appendingPathExtension("orig")) 106 | } catch { 107 | HarborUtils.shared.quickError(error.localizedDescription) 108 | } 109 | } 110 | // symlink the dxvk dll 111 | let dxvkOverridePath = dxvkDirPath.appendingPathComponent("\(override).dll") 112 | do { 113 | try FileManager.default.createSymbolicLink(at: overridePath, withDestinationURL: dxvkOverridePath) 114 | } catch { 115 | HarborUtils.shared.quickError(error.localizedDescription) 116 | } 117 | } 118 | 119 | // Add override to wine registry 120 | for override in dxvkOverrides { 121 | applyRegistryOverrides(in: bottle, for: override) 122 | } 123 | } 124 | 125 | func removeDXVKFromBottle(bottle: HarborBottle) { 126 | let bottlePath = bottle.path 127 | 128 | for override in dxvkOverrides { 129 | let overridePath = bottlePath.appendingPathComponent("drive_c/windows/system32/\(override).dll") 130 | // remove the symlink 131 | if FileManager.default.fileExists(atPath: overridePath.path) { 132 | do { 133 | try FileManager.default.removeItem(at: overridePath) 134 | } catch { 135 | HarborUtils.shared.quickError(error.localizedDescription) 136 | } 137 | } 138 | // rename the original dll back 139 | let overrideOrigPath = overridePath.appendingPathExtension("orig") 140 | if FileManager.default.fileExists(atPath: overrideOrigPath.path) { 141 | do { 142 | try FileManager.default.moveItem(at: overrideOrigPath, to: overridePath) 143 | } catch { 144 | HarborUtils.shared.quickError(error.localizedDescription) 145 | } 146 | } 147 | } 148 | 149 | // Remove override from wine registry 150 | for override in dxvkOverrides { 151 | removeRegistryOverrides(in: bottle, for: override) 152 | } 153 | } 154 | 155 | func installWineD3D(for bottle: HarborBottle) { 156 | let bottlePath = bottle.path 157 | let wined3dDirPath = HarborUtils.shared.getContainerHome().appendingPathComponent("wined3d") 158 | 159 | for override in wined3dOverrides { 160 | let overridePath = bottlePath.appendingPathComponent("drive_c/windows/system32/\(override).dll") 161 | // rename the original dll to .orig 162 | if FileManager.default.fileExists(atPath: overridePath.path) { 163 | do { 164 | try FileManager.default.moveItem(at: overridePath, to: overridePath.appendingPathExtension("orig")) 165 | } catch { 166 | HarborUtils.shared.quickError(error.localizedDescription) 167 | } 168 | } 169 | // symlink the wined3d dll 170 | let wined3dOverridePath = wined3dDirPath.appendingPathComponent("\(override).dll") 171 | do { 172 | try FileManager.default.createSymbolicLink(at: overridePath, withDestinationURL: wined3dOverridePath) 173 | } catch { 174 | HarborUtils.shared.quickError(error.localizedDescription) 175 | } 176 | } 177 | 178 | // Add override to wine registry 179 | for override in wined3dOverrides { 180 | applyRegistryOverrides(in: bottle, for: override) 181 | } 182 | } 183 | 184 | func removeWineD3D(for bottle: HarborBottle) { 185 | let bottlePath = bottle.path 186 | 187 | for override in wined3dOverrides { 188 | let overridePath = bottlePath.appendingPathComponent("drive_c/windows/system32/\(override).dll") 189 | // remove the symlink 190 | if FileManager.default.fileExists(atPath: overridePath.path) { 191 | do { 192 | try FileManager.default.removeItem(at: overridePath) 193 | } catch { 194 | HarborUtils.shared.quickError(error.localizedDescription) 195 | } 196 | } 197 | // rename the original dll back 198 | let overrideOrigPath = overridePath.appendingPathExtension("orig") 199 | if FileManager.default.fileExists(atPath: overrideOrigPath.path) { 200 | do { 201 | try FileManager.default.moveItem(at: overrideOrigPath, to: overridePath) 202 | } catch { 203 | HarborUtils.shared.quickError(error.localizedDescription) 204 | } 205 | } 206 | } 207 | 208 | // Remove override from wine registry 209 | for override in wined3dOverrides { 210 | removeRegistryOverrides(in: bottle, for: override) 211 | } 212 | } 213 | 214 | func applyRegistryOverrides(in bottle: HarborBottle, for dll: String) { 215 | bottle.directLaunchApplication("reg.exe", arguments: ["add", 216 | #"HKEY_CURRENT_USER\Software\Wine\DllOverrides"#, "/v", 217 | dll, "/d", "native", "/f"], shouldWait: true) 218 | } 219 | 220 | func removeRegistryOverrides(in bottle: HarborBottle, for dll: String) { 221 | bottle.directLaunchApplication("reg.exe", arguments: ["delete", 222 | #"HKEY_CURRENT_USER\Software\Wine\DllOverrides"#, "/v", 223 | dll, "/f"], shouldWait: true) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Harbor/Systems/BrewUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrewUtils.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @Observable 12 | final class BrewUtils { 13 | var installed = false 14 | let x64BrewPrefix = "/usr/local/Homebrew" 15 | 16 | init() { 17 | testX64Brew() 18 | } 19 | 20 | func testX64Brew() { 21 | guard FileManager.default.fileExists(atPath: x64BrewPrefix) else { 22 | installed = false 23 | return 24 | } 25 | 26 | // Launch homebrew from within to check if it's correctly installed 27 | let task = Process() 28 | task.executableURL = URL(fileURLWithPath: "/bin/bash") 29 | task.arguments = ["--", "\(x64BrewPrefix)/bin/brew", "--version"] 30 | task.standardOutput = nil 31 | task.standardError = nil 32 | do { 33 | try task.run() 34 | } catch { 35 | return 36 | } 37 | task.waitUntilExit() 38 | 39 | installed = task.terminationStatus == 0 40 | } 41 | 42 | func installX64Brew() { 43 | // Use AppleScript to control Terminal and install Homebrew 44 | let aaplScript = """ 45 | property shellScript : "/bin/bash -c \ 46 | \\"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\\" && \ 47 | clear && echo 'Harbor: Installation complete. You can now close this window.' && exit" 48 | 49 | tell application "Terminal" 50 | activate 51 | -- Enter x86_64 shell 52 | do script "arch -x86_64 /bin/sh" 53 | delay 1 54 | -- Install Homebrew 55 | do script shellScript in front window 56 | end tell 57 | """ 58 | // Launch the AppleScript and wait for it to finish 59 | Task(priority: .userInitiated) { 60 | NSLog("Harbor: Launching Homebrew installation") 61 | let script = NSAppleScript(source: aaplScript) 62 | var errors: NSDictionary? 63 | script?.executeAndReturnError(&errors) 64 | if let errors = errors { 65 | NSLog("Harbor: Homebrew installation failed") 66 | NSLog("\(errors)") 67 | } 68 | } 69 | 70 | repeat { 71 | self.testX64Brew() 72 | 73 | // NSLog("Harbor: Waiting for Homebrew installation to complete") 74 | sleep(1) 75 | } while installed == false 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Harbor/Systems/DXUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DXVKUtils.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 19/06/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | struct DXUtils { 12 | static let shared = DXUtils() 13 | 14 | var DXVKLibsAvailable: Bool { 15 | let harborContainer = HarborUtils.shared.getContainerHome() 16 | let dxvkDirPath = harborContainer.appendingPathComponent("dxvk").appendingPathComponent("dxgi.dll") 17 | return FileManager.default.fileExists(atPath: dxvkDirPath.path) 18 | } 19 | 20 | var vulkanAvailable: Bool { 21 | // Check for the existance of /usr/local/opt/game-porting-toolkit/lib/wine/winevulkan.dll 22 | return FileManager.default 23 | .fileExists(atPath: "/usr/local/opt/game-porting-toolkit/lib/wine/x86_64-windows/winevulkan.dll") 24 | } 25 | 26 | func isDXVKAvailable() -> Bool { 27 | return DXVKLibsAvailable && vulkanAvailable 28 | } 29 | 30 | func untarDXVKLibs(dxvkZip: URL) { 31 | let dxvkPath = HarborUtils.shared.getContainerHome().appendingPathComponent("dxvk") 32 | if !FileManager.default.fileExists(atPath: dxvkPath.path) { 33 | do { 34 | try FileManager.default.createDirectory(at: dxvkPath, 35 | withIntermediateDirectories: true, attributes: nil) 36 | } catch { 37 | NSLog("Harbor: Failed to create Harbor home directory") 38 | } 39 | } 40 | 41 | // Untar the content of the x64 subdirectory under the tarball into the dxvk directory 42 | let task = Process() 43 | task.launchPath = "/usr/bin/tar" 44 | task.arguments = ["-xf", dxvkZip.path, "-C", dxvkPath.path, "--strip-components=2", "*/x64/*"] 45 | do { 46 | try task.run() 47 | } catch { 48 | HarborUtils.shared.quickError(error.localizedDescription) 49 | } 50 | task.waitUntilExit() 51 | } 52 | 53 | func isWineD3DAvailable() -> Bool { // Unfortunately have to exist because previously we didn't save it 54 | let harborContainer = HarborUtils.shared.getContainerHome() 55 | let wined3dDirPath = harborContainer.appendingPathComponent("wined3d").appendingPathComponent("d3d11.dll") 56 | return FileManager.default.fileExists(atPath: wined3dDirPath.path) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Harbor/Systems/GPKUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPKStatus.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import Observation 11 | 12 | enum GPKStatus { 13 | case notInstalled 14 | case partiallyInstalled 15 | case installed 16 | } 17 | 18 | @Observable 19 | final class GPKUtils { 20 | var status: GPKStatus = .notInstalled 21 | 22 | init() { 23 | checkGPKInstallStatus() 24 | } 25 | 26 | func checkGPKInstallStatus() { 27 | let isDir = UnsafeMutablePointer.allocate(capacity: 1) 28 | isDir.initialize(to: false) 29 | defer { isDir.deallocate() } 30 | 31 | // Check if /usr/local/opt/game-porting-toolkit/bin/wine64 has existed 32 | let gpkD3DLib = URL(fileURLWithPath: "/usr/local/opt/game-porting-toolkit/lib/external/libd3dshared.dylib") 33 | let gpkWine64 = URL(fileURLWithPath: "/usr/local/opt/game-porting-toolkit/bin/wine64") 34 | 35 | let gpkD3DLibInstalled = FileManager.default.fileExists(atPath: gpkD3DLib.path, isDirectory: isDir) 36 | let gpkWine64Installed = FileManager.default.fileExists(atPath: gpkWine64.path, isDirectory: isDir) 37 | 38 | self.status = 39 | switch (gpkD3DLibInstalled, gpkWine64Installed) { 40 | case (true, true): 41 | .installed 42 | case (false, false): 43 | .notInstalled 44 | default: 45 | .partiallyInstalled 46 | } 47 | } 48 | 49 | func installGPK(using brewUtils: BrewUtils) { 50 | // Abort if Brew is not installed 51 | brewUtils.testX64Brew() 52 | if brewUtils.installed == false { 53 | NSLog("Harbor: Brew not installed. Aborting") 54 | return 55 | } 56 | 57 | let aaplScript = """ 58 | property shellScript : "clear && \(brewUtils.x64BrewPrefix)/bin/brew install \ 59 | apple/apple/game-porting-toolkit && \ 60 | clear && echo '\(String(localized: "setup.message.complete"))' && exit" 61 | 62 | tell application "Terminal" 63 | activate 64 | -- Enter x86_64 shell 65 | do script "arch -x86_64 /bin/sh" 66 | delay 2 67 | -- Install Homebrew 68 | do script shellScript in front window 69 | end tell 70 | """ 71 | 72 | Task { 73 | if let script = NSAppleScript(source: aaplScript) { 74 | var error: NSDictionary? 75 | script.executeAndReturnError(&error) 76 | if let error = error { 77 | NSLog("Harbor: Failed to execute AppleScript: \(error)") 78 | } 79 | } 80 | } 81 | 82 | repeat { 83 | // Wait for GPK to be installed 84 | sleep(5) 85 | checkGPKInstallStatus() 86 | } while self.status == .notInstalled 87 | 88 | // Backup the original WineD3D libraries 89 | GPKInstallationInternal.shared.saveWineD3Dlibs() 90 | 91 | // Copy the GPK libraries 92 | GPKInstallationInternal.shared.mountAndCopyGPKLibs() 93 | } 94 | 95 | func fastInstallGPK(using brewUtils: BrewUtils, gpkBottle: URL, bundledGPK: Bool = false) { 96 | // Abort if Brew is not installed 97 | brewUtils.testX64Brew() 98 | if brewUtils.installed == false { 99 | NSLog("Harbor: Brew not installed. Aborting") 100 | return 101 | } 102 | 103 | // Check if GPK bottle exists 104 | if !FileManager.default.fileExists(atPath: gpkBottle.path) { 105 | NSLog("Harbor: GPK bottle not found. Aborting") 106 | return 107 | } 108 | 109 | let aaplScript = """ 110 | property shellScript : "clear && \(brewUtils.x64BrewPrefix)/bin/brew install gstreamer pkg-config zlib \ 111 | freetype sdl2 libgphoto2 faudio jpeg libpng mpg123 libtiff libgsm glib gnutls libusb gettext molten-vk && \ 112 | /usr/bin/xattr -r -d com.apple.quarantine \(gpkBottle.path) && \ 113 | \(brewUtils.x64BrewPrefix)/bin/brew install --ignore-dependencies -- \(gpkBottle.path) && \ 114 | clear && echo '\(String(localized: "setup.message.complete"))' && exit" 115 | 116 | tell application "Terminal" 117 | activate 118 | -- Enter x86_64 shell 119 | do script "arch -x86_64 /bin/sh" 120 | delay 2 121 | -- Install Homebrew 122 | do script shellScript in front window 123 | end tell 124 | """ 125 | 126 | Task { 127 | if let script = NSAppleScript(source: aaplScript) { 128 | var error: NSDictionary? 129 | script.executeAndReturnError(&error) 130 | if let error = error { 131 | NSLog("Harbor: Failed to execute AppleScript: \(error)") 132 | } 133 | } else { 134 | return 135 | } 136 | } 137 | 138 | repeat { 139 | // Wait for GPK to be installed 140 | sleep(5) 141 | checkGPKInstallStatus() 142 | } while self.status == .notInstalled 143 | 144 | // Backup the original WineD3D libraries 145 | GPKInstallationInternal.shared.saveWineD3Dlibs() 146 | 147 | // Copy the GPK libraries 148 | if bundledGPK { 149 | GPKInstallationInternal.shared.copyGPKFromArchive(from: gpkBottle) 150 | } else { 151 | GPKInstallationInternal.shared.mountAndCopyGPKLibs() 152 | } 153 | } 154 | 155 | func showGPKInstallAlert() -> Bool { 156 | // Popup an alert warning the user about the GPK installation process 157 | let alert = NSAlert() 158 | alert.messageText = String(localized: "alert.GPKInstall.title") 159 | alert.informativeText = String(localized: "alert.GPKInstall.informativeText") 160 | alert.alertStyle = .warning 161 | alert.addButton(withTitle: String(localized: "btn.OK")) 162 | alert.addButton(withTitle: String(localized: "btn.cancel")) 163 | 164 | if alert.runModal() == .alertFirstButtonReturn { 165 | // User clicked OK. Go time. 166 | return true 167 | } else { 168 | // User clicked Cancel 169 | return false 170 | } 171 | } 172 | 173 | func reinstallGPKLibraries() { 174 | // Remove the GPK libraries 175 | let gpkLib = URL(fileURLWithPath: "/usr/local/opt/game-porting-toolkit/lib/external") 176 | if FileManager.default.fileExists(atPath: gpkLib.path) { 177 | do { 178 | try FileManager.default.removeItem(at: gpkLib) 179 | } catch { 180 | HarborUtils.shared.quickError(error.localizedDescription) 181 | return 182 | } 183 | } 184 | // Copy the GPK libraries 185 | GPKInstallationInternal.shared.mountAndCopyGPKLibs() 186 | } 187 | 188 | func completelyRemoveGPK() { 189 | // Remove the GPK bottle from Brew 190 | let aaplScript = """ 191 | property shellScript : "clear && /usr/local/Homebrew/bin/brew uninstall game-porting-toolkit && \ 192 | echo '\(String(localized: "setup.message.removalComplete"))' && exit" 193 | 194 | tell application "Terminal" 195 | activate 196 | -- Enter x86_64 shell 197 | do script "arch -x86_64 /bin/sh" 198 | delay 2 199 | -- Run removal 200 | do script shellScript in front window 201 | end tell 202 | """ 203 | 204 | if let script = NSAppleScript(source: aaplScript) { 205 | var error: NSDictionary? 206 | script.executeAndReturnError(&error) 207 | if let error = error { 208 | NSLog("Harbor: Failed to execute AppleScript: \(error)") 209 | } else { 210 | status = .notInstalled 211 | } 212 | } else { 213 | return 214 | } 215 | } 216 | } 217 | 218 | class GPKInstallationInternal { 219 | static let shared = GPKInstallationInternal() 220 | func saveWineD3Dlibs() { 221 | // Save the original WineD3D libraries (d3d9.dll, d3d10.dll, d3d11.dll, d3d12.dll, dxgi.dll) 222 | let harborContainer = HarborUtils.shared.getContainerHome().appendingPathComponent("wined3d") 223 | // Clean the folder if needed 224 | if FileManager.default.fileExists(atPath: harborContainer.path) { 225 | do { 226 | try FileManager.default.removeItem(at: harborContainer) 227 | } catch { 228 | HarborUtils.shared.quickError(error.localizedDescription) 229 | return 230 | } 231 | } 232 | do { 233 | try FileManager.default.createDirectory(at: harborContainer, 234 | withIntermediateDirectories: true, attributes: nil) 235 | } catch { 236 | HarborUtils.shared.quickError(error.localizedDescription) 237 | return 238 | } 239 | 240 | let gpkLib = URL(fileURLWithPath: "/usr/local/opt/game-porting-toolkit/lib/wine/x86_64-windows") 241 | let wineD3Dlibs = ["d3d9.dll", "d3d10.dll", "d3d11.dll", "d3d12.dll", "dxgi.dll"] 242 | for lib in wineD3Dlibs { 243 | let libPath = gpkLib.appendingPathComponent(lib) 244 | let libDest = harborContainer.appendingPathComponent(lib) 245 | if FileManager.default.fileExists(atPath: libPath.path) { 246 | do { 247 | try FileManager.default.copyItem(at: libPath, to: libDest) 248 | } catch { 249 | HarborUtils.shared.quickError(error.localizedDescription) 250 | return 251 | } 252 | } 253 | } 254 | } 255 | 256 | func mountAndCopyGPKLibs() { 257 | // Mounts the GPK disk image 258 | let harborContainer = HarborUtils.shared.getContainerHome() 259 | let gpkDMG = harborContainer.appendingPathComponent("GPK.dmg") 260 | // final sanity check, and then copy everything from /lib inside the image 261 | // to /usr/local/opt/game-porting-toolkit/lib 262 | if FileManager.default.fileExists(atPath: gpkDMG.path) { 263 | // launch hdiutil to mount the image 264 | let hdiutil = Process() 265 | hdiutil.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") 266 | hdiutil.arguments = ["attach", gpkDMG.path] 267 | hdiutil.launch() 268 | hdiutil.waitUntilExit() 269 | 270 | // Get the mounted volume name (starts with Game Porting Toolkit) 271 | guard let mountedVolume = try? FileManager.default.contentsOfDirectory(atPath: "/Volumes") 272 | .first(where: { $0.starts(with: "Game Porting Toolkit") }) 273 | else { 274 | NSLog("Harbor: Failed to find mounted GPK disk image") 275 | return 276 | } 277 | let gpkVolume = URL(fileURLWithPath: "/Volumes/\(mountedVolume)") 278 | 279 | let gpkLib: URL 280 | // Check if the directory `redist/` exist 281 | if FileManager.default.fileExists(atPath: gpkVolume.appendingPathComponent("redist").path) { 282 | gpkLib = gpkVolume.appendingPathComponent("redist").appendingPathComponent("lib") 283 | } else { 284 | gpkLib = gpkVolume.appendingPathComponent("lib") 285 | } 286 | 287 | let gpkLibDest = URL(fileURLWithPath: "/usr/local/opt/game-porting-toolkit/lib") 288 | 289 | // Merge the content from /Volumes/Game Porting Toolkit*/lib to /usr/local/opt/game-porting-toolkit/lib 290 | let dittoProcess = Process() 291 | dittoProcess.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") 292 | dittoProcess.arguments = ["-V", gpkLib.path, gpkLibDest.path] 293 | do { 294 | try dittoProcess.run() 295 | } catch { 296 | HarborUtils.shared.quickError(error.localizedDescription) 297 | } 298 | dittoProcess.waitUntilExit() 299 | 300 | // Copy all the gameportingtoolkit* binaries to Harbor's container (for later use) 301 | let harborContainer = HarborUtils.shared.getContainerHome() 302 | let gpkBinDest = harborContainer.appendingPathComponent("bin") 303 | if FileManager.default.fileExists(atPath: gpkBinDest.path) == false { 304 | try? FileManager.default.createDirectory(at: gpkBinDest, 305 | withIntermediateDirectories: true, attributes: nil) 306 | } 307 | 308 | // Unmount the image 309 | let hdiutil2 = Process() 310 | hdiutil2.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") 311 | hdiutil2.arguments = ["detach", gpkVolume.path] 312 | do { 313 | try hdiutil2.run() 314 | } catch { 315 | HarborUtils.shared.quickError(error.localizedDescription) 316 | } 317 | hdiutil2.waitUntilExit() 318 | } else { 319 | NSLog("Harbor: GPK disk image not found. Aborting") 320 | } 321 | } 322 | 323 | func copyGPKFromArchive(from gpkBottle: URL) { 324 | let gpkLibs = gpkBottle.deletingLastPathComponent().appending(path: "gptk_libs") 325 | // Unquaratine this folder 326 | let xattr = Process() 327 | xattr.executableURL = URL(fileURLWithPath: "/usr/bin/xattr") 328 | xattr.arguments = ["-r", "-d", "com.apple.quarantine", gpkLibs.path] 329 | do { 330 | try xattr.run() 331 | } catch { 332 | HarborUtils.shared.quickError(error.localizedDescription) 333 | } 334 | xattr.waitUntilExit() 335 | 336 | // Copy the GPK libraries 337 | let gpkLibDest = URL(fileURLWithPath: "/usr/local/opt/game-porting-toolkit/lib") 338 | let dittoProcess = Process() 339 | dittoProcess.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") 340 | dittoProcess.arguments = ["-V", gpkLibs.path, gpkLibDest.path] 341 | do { 342 | try dittoProcess.run() 343 | } catch { 344 | HarborUtils.shared.quickError(error.localizedDescription) 345 | } 346 | dittoProcess.waitUntilExit() 347 | } 348 | 349 | } 350 | -------------------------------------------------------------------------------- /Harbor/Systems/Winetricks/WinetricksUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WinetricksUtil.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 29/06/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WinetricksVerb: Identifiable { 11 | var id = UUID() 12 | 13 | var name: String 14 | var description: String 15 | } 16 | 17 | struct WinetricksCategory { 18 | var name: String 19 | var verbs: [WinetricksVerb] 20 | } 21 | 22 | struct WinetricksUtils { 23 | static let shared = WinetricksUtils() 24 | 25 | private let winetricksUpstreamURL = "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks" 26 | private let winetricksVerbs = "https://raw.githubusercontent.com/Winetricks/winetricks/master/files/verbs/all.txt" 27 | 28 | func launchWinetricksShell(for bottle: HarborBottle, with verb: String? = nil) { 29 | var shellScript = """ 30 | clear && \ 31 | cd '\(bottle.path.path(percentEncoded: false))' && \ 32 | export WINEPREFIX='\(bottle.path.path(percentEncoded: false))' && \ 33 | export WINE='/usr/local/opt/game-porting-toolkit/bin/wine64' && \ 34 | which cabextract || brew install cabextract && \ 35 | echo 'Updating Winetricks' && curl -L '\(winetricksUpstreamURL)' -o winetricks && chmod +x winetricks && \ 36 | echo 'Winetricks updated. Use ./winetricks to run it.' 37 | """ 38 | 39 | if let verb = verb { 40 | shellScript.append(" && ./winetricks \(verb)") 41 | } 42 | 43 | let aaplScript = """ 44 | tell application "Terminal" 45 | activate 46 | delay 1 47 | do script "\(shellScript)" 48 | end tell 49 | """ 50 | 51 | Task { 52 | var error: NSDictionary? 53 | if let scriptObject = NSAppleScript(source: aaplScript) { 54 | scriptObject.executeAndReturnError(&error) 55 | if let error = error { 56 | NSLog("Harbor: Error while launching Winetricks: \(error)") 57 | } 58 | } 59 | } 60 | } 61 | 62 | func parseVerbs() async -> [WinetricksCategory] { 63 | var verbs: String? 64 | // grab the verbs file 65 | guard let verbsURL = URL(string: winetricksVerbs) else { 66 | return [] 67 | } 68 | 69 | do { 70 | let (data, _) = try await URLSession.shared.data(from: verbsURL) 71 | verbs = String(data: data, encoding: .utf8) 72 | } catch { 73 | return [] 74 | } 75 | 76 | // Read the file line by line 77 | let lines = verbs?.components(separatedBy: "\n") ?? [""] 78 | var categories: [WinetricksCategory] = [] 79 | var currentCategory: WinetricksCategory? 80 | 81 | for line in lines { 82 | // categories are label as "===== =====" 83 | if line.starts(with: "=====") { 84 | // if we have a current category, add it to the list 85 | if let currentCategory = currentCategory { 86 | categories.append(currentCategory) 87 | } 88 | 89 | // create a new category 90 | // capitalize the first letter of the category name 91 | let categoryName = line.replacingOccurrences(of: "=====", with: "").trimmingCharacters(in: .whitespaces) 92 | currentCategory = WinetricksCategory(name: categoryName, verbs: []) 93 | } else { 94 | guard currentCategory != nil else { 95 | continue 96 | } 97 | 98 | // if we have a current category, add the verb to it 99 | // verbs eg. "3m_library 3M Cloud Library (3M Company, 2015) [downloadable]" 100 | let verbName = line.components(separatedBy: " ")[0] 101 | let verbDescription = line.replacingOccurrences(of: "\(verbName) ", with: "") 102 | .trimmingCharacters(in: .whitespaces) 103 | currentCategory?.verbs.append(WinetricksVerb(name: verbName, description: verbDescription)) 104 | } 105 | } 106 | 107 | // add the last category 108 | if let currentCategory = currentCategory { 109 | categories.append(currentCategory) 110 | } 111 | 112 | // remove the "prefix" category 113 | categories.removeAll(where: { $0.name == "prefix" }) 114 | 115 | return categories 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Harbor/Systems/XCLIUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCLIUtils.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 09/06/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @Observable 12 | final class XCLIUtils { 13 | var installed = false 14 | 15 | init() { 16 | checkXcliInstalled() 17 | } 18 | 19 | func checkXcliInstalled() { 20 | // SANITY: Check if /Library/Developer/CommandLineTools exists 21 | guard FileManager.default.fileExists(atPath: "/Library/Developer/CommandLineTools") else { 22 | installed = false 23 | return 24 | } 25 | 26 | // pkgutil --pkg-info=com.apple.pkg.CLTools_Executables | grep version 27 | // Looking for 'version: 15.0.0.0.1.1685693485' 28 | let task = Process() 29 | task.launchPath = "/usr/sbin/pkgutil" 30 | task.arguments = ["--pkg-info=com.apple.pkg.CLTools_Executables"] 31 | let pipe = Pipe() 32 | task.standardOutput = pipe 33 | do { 34 | try task.run() 35 | } catch { 36 | return 37 | } 38 | task.waitUntilExit() 39 | installed = task.terminationStatus == 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/BottleCardListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VeryFancyBottleView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 17/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleCardListView: View { 11 | @Bindable var bottles: BottleList 12 | @Binding var isShowingDetails: Bool 13 | 14 | var body: some View { 15 | ScrollView { 16 | LazyVStack { 17 | ForEach($bottles.bottles) { bottle in 18 | BottleCardView(bottle: bottle, isShowingDetails: $isShowingDetails) 19 | } 20 | } 21 | .padding() 22 | } 23 | } 24 | } 25 | 26 | struct BottleCardView: View { 27 | @Binding var bottle: HarborBottle 28 | @Binding var isShowingDetails: Bool 29 | 30 | @State var isBeingHoveredUpon = false 31 | @State var showLaunchExtDropdown = false 32 | @State var isShowingStatusOverlay = false 33 | 34 | @State var overlayText = "Nothing" 35 | @State var overlayColor = Color.accentColor 36 | var body: some View { 37 | ZStack { 38 | HStack { 39 | VStack(alignment: .leading) { 40 | Text(bottle.name) 41 | .font(.headline) 42 | Text(bottle.path.prettyFileUrl) 43 | .font(.subheadline) 44 | if let primaryApp = bottle.primaryApplicationPath.components(separatedBy: "\\").last { 45 | Text(primaryApp) 46 | .font(.subheadline) 47 | } 48 | } 49 | Spacer() 50 | Group { 51 | Group { 52 | Button { 53 | bottle.launchPrimaryApplication() 54 | displayOverlay(with: String(localized: "strings.bottleStarted"), for: 3) 55 | } label: { 56 | Image(systemName: "play") 57 | } 58 | .buttonStyle(.borderless) 59 | Button { 60 | showLaunchExtDropdown.toggle() 61 | } label: { 62 | Image(systemName: "tray.and.arrow.down") 63 | } 64 | .buttonStyle(.borderless) 65 | Button { 66 | bottle.killBottle() 67 | } label: { 68 | Image(systemName: "stop") 69 | } 70 | .buttonStyle(.borderless) 71 | } 72 | .opacity(isBeingHoveredUpon ? 100 : 0) 73 | NavigationLink { 74 | BottleCardDetailedView(bottle: $bottle, isShowingDetail: $isShowingDetails) 75 | } label: { 76 | Image(systemName: "chevron.forward.circle.fill") 77 | } 78 | .buttonStyle(.borderless) 79 | } 80 | } 81 | .padding() 82 | // An overlay covering the entire card 83 | if isShowingStatusOverlay { 84 | VStack { 85 | Text(overlayText) 86 | } 87 | .frame(maxWidth: .infinity, maxHeight: .infinity) 88 | .background(overlayColor) 89 | .font(.title) 90 | .clipShape(RoundedRectangle(cornerSize: CGSize(width: 20, height: 10))) 91 | .transition(.push(from: .leading)) 92 | } 93 | } 94 | .clipShape(RoundedRectangle(cornerSize: CGSize(width: 20, height: 10))) 95 | .background(in: RoundedRectangle(cornerSize: 96 | CGSize(width: 20, height: 10)), 97 | fillStyle: .init()) 98 | .sheet(isPresented: $showLaunchExtDropdown) { 99 | LaunchExtDropdown(isPresented: $showLaunchExtDropdown, bottle: bottle) 100 | } 101 | .onTapGesture(count: 2, perform: { 102 | bottle.launchPrimaryApplication() 103 | displayOverlay(with: String(localized: "strings.bottleStarted"), for: 3) 104 | }) 105 | .onHover(perform: { hovering in 106 | isBeingHoveredUpon = hovering 107 | }) 108 | } 109 | 110 | func displayOverlay(with text: String, for time: UInt32) { 111 | overlayText = text 112 | withAnimation { 113 | isShowingStatusOverlay = true 114 | } 115 | Task.detached { 116 | sleep(time) 117 | Task { @MainActor in 118 | withAnimation { 119 | isShowingStatusOverlay = false 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | struct BottleCardDetailedView: View { 127 | @Binding var bottle: HarborBottle 128 | @Binding var isShowingDetail: Bool 129 | @Environment(\.presentationMode) var presentationMode: Binding 130 | 131 | @State var bottlePath = "" 132 | 133 | let monospaceFont = Font.body.monospaced() 134 | 135 | var body: some View { 136 | ScrollView { 137 | VStack { 138 | Form { 139 | Section { 140 | TextField("sheet.new.bottleNameLabel", text: $bottle.name) 141 | } 142 | Section { 143 | HStack { 144 | Text("sheet.new.bottlePathLabel") 145 | TextField("", text: $bottlePath) 146 | .textFieldStyle(.plain) 147 | .font(monospaceFont) 148 | .disabled(true) 149 | .onAppear { 150 | bottlePath = bottle.path.prettyFileUrl 151 | } 152 | } 153 | } 154 | Section { 155 | Text("sheet.edit.primaryAppLabel") 156 | HStack { 157 | TextField("", text: $bottle.primaryApplicationPath) 158 | .font(monospaceFont) 159 | Button("btn.browse") { 160 | let dialog = NSOpenPanel() 161 | dialog.title = "sheet.edit.primaryApp.popup" 162 | dialog.showsResizeIndicator = true 163 | dialog.showsHiddenFiles = false 164 | dialog.canChooseDirectories = false 165 | dialog.canChooseFiles = true 166 | dialog.canCreateDirectories = false 167 | dialog.allowsMultipleSelection = false 168 | dialog.directoryURL = bottle.path 169 | if dialog.runModal() == NSApplication.ModalResponse.OK { 170 | if let result = dialog.url { 171 | bottle.primaryApplicationPath = bottle.pathFromUnixPath(result) 172 | } 173 | } else { 174 | // User clicked on "Cancel" 175 | return 176 | } 177 | } 178 | } 179 | } 180 | Section { 181 | Text("sheet.edit.primaryAppArgsLabel") 182 | TextField("", text: $bottle.primaryApplicationArgument) 183 | .font(monospaceFont) 184 | } 185 | Section { 186 | Text("sheet.edit.primaryAppWorkDirLabel") 187 | HStack { 188 | TextField("", text: $bottle.primaryApplicationWorkDir) 189 | .font(monospaceFont) 190 | Button("btn.Auto") { 191 | // Path up to the executable 192 | // Remove the executable name from the Windows path 193 | let path = bottle.primaryApplicationPath 194 | .replacingOccurrences(of: "\\", with: "/") 195 | .components(separatedBy: "/") 196 | .dropLast() 197 | .joined(separator: "\\") 198 | bottle.primaryApplicationWorkDir = path 199 | } 200 | Button("btn.browse") { 201 | let dialog = NSOpenPanel() 202 | dialog.title = "sheet.edit.primaryApp.popup" 203 | dialog.showsResizeIndicator = true 204 | dialog.showsHiddenFiles = false 205 | dialog.canChooseDirectories = true 206 | dialog.canChooseFiles = false 207 | dialog.canCreateDirectories = true 208 | dialog.allowsMultipleSelection = false 209 | dialog.directoryURL = bottle.path 210 | if dialog.runModal() == NSApplication.ModalResponse.OK { 211 | if let result = dialog.url { 212 | bottle.primaryApplicationWorkDir = bottle.pathFromUnixPath(result) 213 | } 214 | } else { 215 | // User clicked on "Cancel" 216 | return 217 | } 218 | } 219 | } 220 | } 221 | Section { 222 | Text("sheet.edit.envVars") 223 | EnvironmentVarsEditor(environmentVars: $bottle.envVars) 224 | } 225 | } 226 | .formStyle(.grouped) 227 | Form { 228 | Section { 229 | TaskControllerView(bottle: $bottle) 230 | } 231 | } 232 | .formStyle(.grouped) 233 | Form { 234 | Section { 235 | Toggle("sheet.advConf.hudToggle", isOn: $bottle.enableHUD) 236 | SyncPrimitivesSelector(bottle: $bottle) 237 | Toggle("sheet.advConf.stdOutToggle", isOn: $bottle.pleaseShutUp) 238 | DXVKToggle(bottle: $bottle) 239 | RetinaModeToggle(bottle: $bottle) 240 | } 241 | Section { 242 | HStack { 243 | Button("sheet.advConf.winecfgBtn") { 244 | bottle.launchApplication("winecfg") 245 | } 246 | Button("sheet.advConf.explorerBtn") { 247 | bottle.launchApplication("explorer") 248 | } 249 | Button("sheet.advConf.regeditBtn") { 250 | bottle.launchApplication("regedit") 251 | } 252 | Spacer() 253 | Button("sheet.advConf.Winetricks") { 254 | WinetricksUI.openWindow(for: bottle) 255 | } 256 | } 257 | } 258 | } 259 | .formStyle(.grouped) 260 | HStack { 261 | Spacer() 262 | Button { 263 | HarborShortcuts.shared.createDesktopShortcut(for: bottle) 264 | } label: { 265 | Label("sheet.advConf.desktopShortcut", systemImage: "desktopcomputer") 266 | } 267 | Button { 268 | bottle.directLaunchApplication("wineboot", arguments: ["-u"]) 269 | } label: { 270 | Label("sheet.advConf.update", systemImage: "arrow.clockwise") 271 | } 272 | Button { 273 | // ALARM 274 | let alert = NSAlert() 275 | alert.messageText = String(localized: "home.alert.deleteTitle") 276 | alert.alertStyle = .critical 277 | let checkbox = NSButton(checkboxWithTitle: 278 | String(format: String(localized: "home.alert.deletePath %@"), 279 | bottle.path.prettyFileUrl), target: nil, action: nil) 280 | checkbox.state = .on 281 | alert.accessoryView = checkbox 282 | alert.addButton(withTitle: String(localized: "btn.delete")) 283 | alert.addButton(withTitle: String(localized: "btn.cancel")) 284 | if alert.runModal() == .alertFirstButtonReturn { 285 | // User clicked on "Delete" 286 | BottleLoader.shared.delete(bottle, checkbox.state) 287 | self.presentationMode.wrappedValue.dismiss() 288 | } else { 289 | // User clicked on "Cancel" 290 | return 291 | } 292 | } label: { 293 | Label("home.btn.nuke", systemImage: "trash") 294 | } 295 | .buttonStyle(.borderedProminent) 296 | .tint(.red) 297 | } 298 | .padding(.horizontal) 299 | } 300 | } 301 | .padding() 302 | .navigationTitle(bottle.name) 303 | .onAppear { 304 | isShowingDetail = true 305 | } 306 | .onDisappear { 307 | if let bottleIndex = BottleLoader.shared.bottles.firstIndex(where: { $0.id == bottle.id }) { 308 | BottleLoader.shared.bottles[bottleIndex] = bottle 309 | } 310 | isShowingDetail = false 311 | } 312 | } 313 | } 314 | 315 | struct BottleListCardView_Previews: PreviewProvider { 316 | static var previews: some View { 317 | BottleCardListView(bottles: BottleList(), isShowingDetails: Binding.constant(false)) 318 | .environment(\.brewUtils, .init()) 319 | } 320 | } 321 | 322 | struct BottleCardDetailedView_Previews: PreviewProvider { 323 | static var sampleBottle = HarborBottle(id: UUID(), name: "Demo", 324 | path: FileManager.default.urls(for: .documentDirectory, 325 | in: .userDomainMask).first ?? 326 | URL(filePath: "")) 327 | static var previews: some View { 328 | BottleCardDetailedView(bottle: Binding.constant(sampleBottle), isShowingDetail: Binding.constant(true)) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/BottleConfigDropdown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleConfigDropdown.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 09/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleConfigDropdown: View { 11 | @Binding var isPresented: Bool 12 | @Binding var bottle: HarborBottle 13 | 14 | var body: some View { 15 | VStack { 16 | Text("sheet.advConf.title \(bottle.name)") 17 | .font(.title) 18 | .padding() 19 | Spacer() 20 | Form { 21 | Section { 22 | Toggle("sheet.advConf.hudToggle", isOn: $bottle.enableHUD) 23 | SyncPrimitivesSelector(bottle: $bottle) 24 | Toggle("sheet.advConf.stdOutToggle", isOn: $bottle.pleaseShutUp) 25 | DXVKToggle(bottle: $bottle) 26 | RetinaModeToggle(bottle: $bottle) 27 | } 28 | Section { 29 | HStack { 30 | Button("sheet.advConf.winecfgBtn") { 31 | bottle.launchApplication("winecfg") 32 | } 33 | Button("sheet.advConf.explorerBtn") { 34 | bottle.launchApplication("explorer") 35 | } 36 | Button("sheet.advConf.regeditBtn") { 37 | bottle.launchApplication("regedit") 38 | } 39 | Spacer() 40 | Button("sheet.advConf.Winetricks") { 41 | WinetricksUI.openWindow(for: bottle) 42 | } 43 | } 44 | } 45 | Section { 46 | HStack { 47 | Spacer() 48 | Button("sheet.advConf.desktopShortcut") { 49 | HarborShortcuts.shared.createDesktopShortcut(for: bottle) 50 | } 51 | Button("sheet.advConf.update") { 52 | bottle.directLaunchApplication("wineboot", arguments: ["-b"]) 53 | } 54 | Spacer() 55 | } 56 | } 57 | } 58 | .formStyle(.grouped) 59 | Spacer() 60 | Button("btn.OK") { 61 | if let bottleIndex = BottleLoader.shared.bottles.firstIndex(where: { $0.id == bottle.id }) { 62 | BottleLoader.shared.bottles[bottleIndex] = bottle 63 | } 64 | isPresented = false 65 | } 66 | .buttonStyle(.borderedProminent) 67 | .tint(.accentColor) 68 | } 69 | .frame(minWidth: 300, minHeight: 300) 70 | .padding() 71 | } 72 | } 73 | 74 | struct BottleConfigDropdown_Previews: PreviewProvider { 75 | static var previews: some View { 76 | BottleConfigDropdown(isPresented: Binding.constant(true), 77 | bottle: Binding.constant(HarborBottle( 78 | id: UUID(), name: "Bottle", path: URL(fileURLWithPath: "")))) 79 | .environment(\.brewUtils, .init()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/BottleDetailsCommonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleDetailsCommonView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 13/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SyncPrimitivesSelector: View { 11 | @Binding var bottle: HarborBottle 12 | 13 | var body: some View { 14 | Group { 15 | HStack { 16 | Picker("sheet.advConf.SyncPrimitives", selection: $bottle.syncPrimitives) { 17 | Text("sheet.advConf.SyncPrimitives.none").tag(WineSyncronizationPrimatives.none) 18 | Text(WineSyncronizationPrimatives.eSync.rawValue).tag(WineSyncronizationPrimatives.eSync) 19 | Text(WineSyncronizationPrimatives.mSync.rawValue).tag(WineSyncronizationPrimatives.mSync) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | struct DXVKToggle: View { 27 | @Binding var bottle: HarborBottle 28 | @State var canSetDX = false 29 | @State var bottleDXBackend: DXBackend = .how 30 | var body: some View { 31 | Group { 32 | if canSetDX { 33 | HStack { 34 | Picker("sheet.advConf.DXBackend", selection: $bottleDXBackend) { 35 | Text(DXBackend.gptk.rawValue).tag(DXBackend.gptk) 36 | Text(DXBackend.dxvk.rawValue).tag(DXBackend.dxvk) 37 | .disabled(!DXUtils.shared.isDXVKAvailable()) 38 | Text(DXBackend.wined3d.rawValue).tag(DXBackend.wined3d) 39 | .disabled(!DXUtils.shared.isWineD3DAvailable()) 40 | } 41 | .disabled(!canSetDX) 42 | } 43 | .onChange(of: bottleDXBackend) { _, newValue in 44 | canSetDX = false 45 | Task.detached { 46 | BottleDX.shared.updateDXBackend(for: bottle, using: newValue) 47 | Task { @MainActor in 48 | canSetDX = true 49 | } 50 | } 51 | } 52 | } else { 53 | HStack { 54 | Text("sheet.advConf.dxvkToggle") 55 | Spacer() 56 | ProgressView() 57 | .progressViewStyle(.circular) 58 | .controlSize(.small) 59 | } 60 | } 61 | } 62 | .onAppear { 63 | Task.detached { 64 | bottleDXBackend = BottleDX.shared.checkBottleBackend(for: bottle) 65 | Task { @MainActor in 66 | canSetDX = true 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | struct RetinaModeToggle: View { 74 | @Binding var bottle: HarborBottle 75 | @State var canSetRetinaMode = false 76 | @State var bottleRetinaMode = false 77 | 78 | var body: some View { 79 | Group { 80 | if canSetRetinaMode { 81 | Toggle("sheet.advConf.retinaToggle", isOn: $bottleRetinaMode) 82 | .disabled(!canSetRetinaMode) 83 | .onChange(of: bottleRetinaMode) { _, newValue in 84 | canSetRetinaMode = false 85 | Task.detached { 86 | setRetinaMode(newValue) 87 | Task { @MainActor in 88 | canSetRetinaMode = true 89 | } 90 | } 91 | } 92 | } else { 93 | HStack { 94 | Text("sheet.advConf.retinaToggle") 95 | Spacer() 96 | ProgressView() 97 | .progressViewStyle(.circular) 98 | .controlSize(.small) 99 | } 100 | } 101 | } 102 | .onAppear { 103 | Task.detached(priority: .background) { 104 | bottleRetinaMode = queryRetinaMode() 105 | Task { @MainActor in 106 | canSetRetinaMode = true 107 | } 108 | } 109 | } 110 | } 111 | func queryRetinaMode() -> Bool { 112 | let result = bottle.directLaunchApplication("reg.exe", arguments: ["query", #"HKCU\Software\Wine\Mac Driver"#, 113 | "-v", "RetinaMode"]) 114 | if let result = result.trimmingCharacters(in: .whitespacesAndNewlines).split(separator: " ").last { 115 | return result == "y" 116 | } else { 117 | return false 118 | } 119 | } 120 | func setRetinaMode(_ value: Bool) { 121 | if value { 122 | bottle.directLaunchApplication("reg.exe", arguments: 123 | ["add", #"HKCU\Software\Wine\Mac Driver"#, 124 | "/v", "RetinaMode", 125 | "/d", "y", "/f"]) 126 | } else { 127 | bottle.directLaunchApplication("reg.exe", arguments: 128 | ["delete", 129 | #"HKCU\Software\Wine\Mac Driver"#, 130 | "/v", "RetinaMode", "/f"]) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/BottleOpsDropdown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewBottleDropdown.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 08/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewBottleDropdown: View { 11 | @Binding var isPresented: Bool 12 | var editingMode: Bool = false 13 | 14 | @State var bottle: HarborBottle 15 | @State var bottlePath = "" 16 | @State var isWorking = false 17 | 18 | let monospaceFont = Font.body.monospaced() 19 | 20 | var body: some View { 21 | ZStack { 22 | VStack { 23 | Text(editingMode ? "sheet.edit.title" : "sheet.new.title") 24 | .font(.title) 25 | .padding() 26 | 27 | Spacer() 28 | 29 | Form { 30 | Section { 31 | HStack { 32 | Text("sheet.new.bottleNameLabel") 33 | TextField("", text: $bottle.name) 34 | .onChange(of: bottle.name) { oldValue, newValue in 35 | // Prevent it from being empty 36 | if newValue == "" { 37 | bottle.name = oldValue 38 | } else { 39 | bottle.name = newValue 40 | } 41 | } 42 | } 43 | } 44 | 45 | // Browsable file picker for new bottle folder 46 | Section { 47 | HStack { 48 | Text("sheet.new.bottlePathLabel") 49 | TextField("", text: $bottlePath) 50 | .font(monospaceFont) 51 | Button("btn.browse") { 52 | let dialog = NSOpenPanel() 53 | dialog.title = "sheet.new.title" 54 | dialog.showsResizeIndicator = true 55 | dialog.showsHiddenFiles = false 56 | dialog.canChooseDirectories = true 57 | dialog.canChooseFiles = false 58 | dialog.canCreateDirectories = true 59 | dialog.allowsMultipleSelection = false 60 | dialog.directoryURL = FileManager.default 61 | .urls(for: .documentDirectory, in: .userDomainMask).first 62 | if dialog.runModal() == NSApplication.ModalResponse.OK { 63 | if let result = dialog.url { 64 | bottlePath = result.path 65 | } 66 | } else { 67 | // User clicked on "Cancel" 68 | return 69 | } 70 | } 71 | } 72 | .onChange(of: bottlePath) { _, value in 73 | bottle.path = URL(fileURLWithPath: value) 74 | } 75 | } 76 | .disabled(editingMode) 77 | 78 | if editingMode { 79 | Section { 80 | HStack { 81 | Text("sheet.edit.primaryAppLabel") 82 | TextField("", text: $bottle.primaryApplicationPath) 83 | .font(monospaceFont) 84 | Button("btn.browse") { 85 | let dialog = NSOpenPanel() 86 | dialog.title = "sheet.edit.primaryApp.popup" 87 | dialog.showsResizeIndicator = true 88 | dialog.showsHiddenFiles = false 89 | dialog.canChooseDirectories = false 90 | dialog.canChooseFiles = true 91 | dialog.canCreateDirectories = false 92 | dialog.allowsMultipleSelection = false 93 | dialog.directoryURL = bottle.path 94 | if dialog.runModal() == NSApplication.ModalResponse.OK { 95 | if let result = dialog.url { 96 | bottle.primaryApplicationPath = bottle.pathFromUnixPath(result) 97 | } 98 | } else { 99 | // User clicked on "Cancel" 100 | return 101 | } 102 | } 103 | } 104 | } 105 | Section { 106 | HStack { 107 | Text("sheet.edit.primaryAppArgsLabel") 108 | TextField("", text: $bottle.primaryApplicationArgument) 109 | } 110 | HStack { 111 | Text("sheet.edit.primaryAppWorkDirLabel") 112 | TextField("", text: $bottle.primaryApplicationWorkDir) 113 | Button("btn.Auto") { 114 | // Path up to the executable 115 | // Remove the executable name from the Windows path 116 | let path = bottle.primaryApplicationPath 117 | .replacingOccurrences(of: "\\", with: "/") 118 | .components(separatedBy: "/") 119 | .dropLast() 120 | .joined(separator: "\\") 121 | bottle.primaryApplicationWorkDir = path 122 | } 123 | Button("btn.browse") { 124 | let dialog = NSOpenPanel() 125 | dialog.title = "sheet.edit.primaryApp.popup" 126 | dialog.showsResizeIndicator = true 127 | dialog.showsHiddenFiles = false 128 | dialog.canChooseDirectories = true 129 | dialog.canChooseFiles = false 130 | dialog.canCreateDirectories = true 131 | dialog.allowsMultipleSelection = false 132 | dialog.directoryURL = bottle.path 133 | if dialog.runModal() == NSApplication.ModalResponse.OK { 134 | if let result = dialog.url { 135 | bottle.primaryApplicationWorkDir = bottle.pathFromUnixPath(result) 136 | } 137 | } else { 138 | // User clicked on "Cancel" 139 | return 140 | } 141 | } 142 | } 143 | } 144 | Section { 145 | Text("sheet.edit.envVars") 146 | EnvironmentVarsEditor(environmentVars: $bottle.envVars) 147 | } 148 | } 149 | } 150 | .formStyle(.grouped) 151 | 152 | Spacer() 153 | 154 | // Cancel and Create buttons 155 | HStack { 156 | Button("btn.cancel") { 157 | isPresented = false 158 | } 159 | Button(editingMode ? "btn.done" : "btn.create") { 160 | if editingMode { 161 | // Save the bottle 162 | if let bottleIndex = BottleLoader.shared.bottles.firstIndex(where: { $0.id == bottle.id }) { 163 | BottleLoader.shared.bottles[bottleIndex] = bottle 164 | } 165 | isPresented = false 166 | } else { 167 | // Create the bottle 168 | isWorking = true 169 | Task.detached { 170 | // We quickly check the dir. If it contains Wine file structure (eg. drive_c) 171 | // create the bottle WITH it. 172 | // Otherwise, create the bottle with the name as the new directory. 173 | let isWineDir = FileManager.default 174 | .fileExists(atPath: bottle.path.appendingPathComponent("drive_c").path) 175 | if !isWineDir { 176 | let newDir = bottle.path.appendingPathComponent(bottle.name) 177 | try? FileManager.default 178 | .createDirectory(at: newDir, withIntermediateDirectories: true, attributes: nil) 179 | bottle.path = newDir 180 | } 181 | bottle.initializeBottle() 182 | Task { @MainActor in 183 | BottleLoader.shared.bottles.append(bottle) 184 | isWorking = false 185 | isPresented = false 186 | } 187 | } 188 | } 189 | } 190 | .disabled(bottle.name == "" || bottle.path.absoluteString == "file:///" || isWorking) 191 | .buttonStyle(.borderedProminent) 192 | .tint(.accentColor) 193 | } 194 | .padding() 195 | } 196 | .disabled(isWorking) 197 | if isWorking { 198 | // Bar indicating progress 199 | ProgressView() 200 | .progressViewStyle(.circular) 201 | .controlSize(.regular) 202 | } 203 | } 204 | .padding() 205 | .frame(minWidth: 500) 206 | } 207 | } 208 | 209 | struct EditBottleView: View { 210 | @Binding var isPresented: Bool 211 | var bottle: HarborBottle 212 | var body: some View { 213 | // Basically reuse New in editing mode 214 | NewBottleDropdown(isPresented: $isPresented, editingMode: true, 215 | bottle: bottle, bottlePath: bottle.path.absoluteString) 216 | } 217 | 218 | } 219 | 220 | struct NewBottleDropdown_Previews: PreviewProvider { 221 | static var previews: some View { 222 | EditBottleView(isPresented: Binding.constant(true), 223 | bottle: HarborBottle(id: UUID(), name: "My Bottle", 224 | path: URL(fileURLWithPath: "/Users/venti/Documents/My Bottle"))) 225 | .environment(\.xcliUtils, .init()) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/BottleTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Toolbar.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 09/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleTableView: View { 11 | @Binding var bottles: [BottleModel] 12 | @Binding var selectedBottle: BottleModel.ID? 13 | 14 | @Binding var showNewBottleSheet: Bool 15 | @Binding var showEditBottleSheet: Bool 16 | @Binding var showLaunchExtSheet: Bool 17 | 18 | @State private var sortOrder = [KeyPathComparator(\BottleModel.name)] 19 | 20 | var body: some View { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/EnvironmentVarsEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentVarsEditor.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 20/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EnvironmentVarsEditor: View { 11 | @Binding var environmentVars: [String: String] 12 | var body: some View { 13 | VStack { 14 | ScrollView { 15 | HStack(alignment: .bottom) { 16 | LazyVStack { 17 | ForEach(Array(environmentVars.keys), id: \.self) { key in 18 | KVLPairEditor(environmentVars: $environmentVars, 19 | keyValuePair: Binding( 20 | get: { (key, environmentVars[key] ?? "") }, 21 | set: { _, _ in })) 22 | } 23 | } 24 | Button { 25 | environmentVars["Key_\(environmentVars.count + 1)"] = "Value \(environmentVars.count + 1)" 26 | } label: { 27 | Image(systemName: "plus") 28 | } 29 | } 30 | } 31 | } 32 | .frame(maxHeight: 200) 33 | } 34 | } 35 | 36 | struct EnvironmentVarsEditor_Previews: PreviewProvider { 37 | @State static var previewVars = ["Key 1": "Value 1", 38 | "Key 2": "Value 2", 39 | "Key 3": "Value 3"] 40 | static var previews: some View { 41 | EnvironmentVarsEditor(environmentVars: $previewVars) 42 | .environment(\.brewUtils, .init()) 43 | } 44 | } 45 | 46 | struct KVLPairEditor: View { 47 | @Binding var environmentVars: [String: String] 48 | @Binding var keyValuePair: (key: String, value: String) 49 | 50 | @State var tempKey: String = "" 51 | 52 | var body: some View { 53 | HStack { 54 | TextField("", text: $tempKey) 55 | .onSubmit { 56 | environmentVars.removeValue(forKey: keyValuePair.key) 57 | environmentVars[tempKey] = keyValuePair.value 58 | } 59 | TextField("", text: Binding( 60 | get: { keyValuePair.value }, 61 | set: { newValue in 62 | environmentVars[keyValuePair.key] = newValue 63 | })) 64 | Button { 65 | environmentVars.removeValue(forKey: keyValuePair.key) 66 | } label: { 67 | Image(systemName: "minus") 68 | } 69 | } 70 | .onAppear { 71 | tempKey = keyValuePair.key 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/LaunchExtDropdown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchExtDropdown.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 08/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | import UniformTypeIdentifiers 10 | 11 | struct LaunchExtDropdown: View { 12 | @Binding var isPresented: Bool 13 | var bottle: HarborBottle 14 | 15 | @State var applicationPath = "" 16 | @State var applicationArgument = "" 17 | @State var applicationWorkDir = "" 18 | @State var applicationEnvVars = [String: String]() 19 | 20 | let monospaceFont = Font.body.monospaced() 21 | 22 | var body: some View { 23 | VStack { 24 | Text("sheet.launchExt.title") 25 | .font(.title) 26 | .padding() 27 | 28 | Text("sheet.launchExt.subtitle \(bottle.name)") 29 | .padding() 30 | Form { 31 | Section { 32 | HStack { 33 | Text("sheet.launchExt.applicationLabel") 34 | TextField("", text: $applicationPath) 35 | .font(monospaceFont) 36 | Button("btn.browse") { 37 | let dialog = NSOpenPanel() 38 | dialog.title = "sheet.launchExt.title" 39 | dialog.showsResizeIndicator = true 40 | dialog.showsHiddenFiles = false 41 | dialog.canChooseDirectories = false 42 | dialog.canChooseFiles = true 43 | dialog.canCreateDirectories = false 44 | dialog.allowsMultipleSelection = false 45 | dialog.allowedContentTypes = [.exe, UTType(importedAs: "harbor.msi-package")] 46 | dialog.directoryURL = bottle.path 47 | if dialog.runModal() == NSApplication.ModalResponse.OK { 48 | if let result = dialog.url { 49 | applicationPath = result.path 50 | } 51 | } else { 52 | // User clicked on "Cancel" 53 | return 54 | } 55 | } 56 | } 57 | } 58 | 59 | Section { 60 | HStack { 61 | Text("sheet.launchExt.argsLabel") 62 | TextField("", text: $applicationArgument) 63 | .font(monospaceFont) 64 | } 65 | } 66 | 67 | Section { 68 | HStack { 69 | Text("sheet.launchExt.appWorkDirLabel") 70 | TextField("", text: $applicationWorkDir) 71 | Button("btn.browse") { 72 | let dialog = NSOpenPanel() 73 | dialog.title = "sheet.launchExt.workDir.popup" 74 | dialog.showsResizeIndicator = true 75 | dialog.showsHiddenFiles = false 76 | dialog.canChooseDirectories = true 77 | dialog.canChooseFiles = false 78 | dialog.canCreateDirectories = true 79 | dialog.allowsMultipleSelection = false 80 | dialog.directoryURL = bottle.path 81 | if dialog.runModal() == NSApplication.ModalResponse.OK { 82 | if let result = dialog.url { 83 | applicationWorkDir = bottle.pathFromUnixPath(result) 84 | } 85 | } else { 86 | // User clicked on "Cancel" 87 | return 88 | } 89 | } 90 | } 91 | } 92 | Section { 93 | Text("sheet.edit.envVars") 94 | EnvironmentVarsEditor(environmentVars: $applicationEnvVars) 95 | } 96 | } 97 | .padding() 98 | .formStyle(.grouped) 99 | 100 | HStack { 101 | Spacer() 102 | Button("btn.cancel") { 103 | isPresented = false 104 | } 105 | Button("btn.launch") { 106 | bottle.launchExtApplication(applicationPath, 107 | arguments: applicationArgument.components(separatedBy: " "), 108 | workDir: applicationWorkDir) 109 | isPresented = false 110 | } 111 | .disabled(applicationPath.isEmpty) 112 | .buttonStyle(.borderedProminent) 113 | .tint(.accentColor) 114 | Spacer() 115 | } 116 | } 117 | .padding() 118 | } 119 | } 120 | 121 | struct LaunchExtDropdown_Previews: PreviewProvider { 122 | static var previews: some View { 123 | LaunchExtDropdown(isPresented: Binding.constant(true), 124 | bottle: HarborBottle(id: UUID(), name: "Demo", path: URL(fileURLWithPath: ""))) 125 | .environment(\.brewUtils, .init()) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Harbor/UIElements/BottleManagement/TaskControllerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskControllerView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 31/12/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleProcess: Identifiable { 11 | var id = UUID() 12 | var pid: String 13 | var procName: String 14 | } 15 | 16 | struct TaskControllerView: View { 17 | @Binding var bottle: HarborBottle 18 | 19 | @State private var processes = [BottleProcess]() 20 | @State private var processSortOrder = [KeyPathComparator(\BottleProcess.pid)] 21 | @State private var selectedProcess: BottleProcess.ID? 22 | 23 | var body: some View { 24 | ZStack { 25 | if !processes.isEmpty { 26 | VStack { 27 | ScrollView { 28 | Table(processes, selection: $selectedProcess, sortOrder: $processSortOrder) { 29 | TableColumn("process.table.pid", value: \.pid) 30 | TableColumn("process.table.executable", value: \.procName) 31 | } 32 | .frame(minHeight: 250) 33 | } 34 | 35 | HStack { 36 | Spacer() 37 | Button("process.table.refresh") { 38 | Task.detached(priority: .userInitiated) { 39 | processes.removeAll() 40 | fetchProcesses() 41 | } 42 | } 43 | Button("process.table.kill") { 44 | Task.detached(priority: .userInitiated) { 45 | killProcess() 46 | } 47 | } 48 | } 49 | .padding() 50 | } 51 | } else { 52 | HStack(alignment: .center) { 53 | Spacer() 54 | VStack(alignment: .center) { 55 | ProgressView() 56 | .padding() 57 | Text("process.table.loading") 58 | } 59 | Spacer() 60 | } 61 | } 62 | } 63 | .onAppear { 64 | Task.detached(priority: .userInitiated) { 65 | fetchProcesses() 66 | } 67 | } 68 | } 69 | 70 | func fetchProcesses() { 71 | let taskList = bottle.directLaunchApplication("tasklist.exe", shouldWait: true) 72 | var newProcessList = [BottleProcess]() 73 | 74 | for line in taskList.components(separatedBy: "\n") { 75 | let components = line.components(separatedBy: ",") 76 | if components.count > 1 { 77 | let pid = components[1] 78 | let procName = components[0] 79 | newProcessList.append(BottleProcess(pid: pid, procName: procName)) 80 | } 81 | } 82 | 83 | processes = newProcessList 84 | } 85 | 86 | func killProcess() { 87 | if let thisProcess = processes.first(where: { $0.id == selectedProcess }) { 88 | bottle.directLaunchApplication("taskkill.exe", 89 | arguments: ["/PID", thisProcess.pid, "/F"], 90 | shouldWait: true) 91 | // Sleep a bit before refreshing the list 92 | Thread.sleep(forTimeInterval: 2) 93 | fetchProcesses() 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Harbor/UIElements/Commands/BottleMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleMenu.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 09/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleMenu: Commands { 11 | @Binding var bottles: [BottleModel] 12 | @Binding var selectedBottle: BottleModel.ID? 13 | 14 | var body: some Commands { 15 | CommandMenu("Bottle") { 16 | Button("Launch winecfg") { 17 | if let bottle = bottles.first(where: { $0.id == selectedBottle }) { 18 | bottle.launchApplication("winecfg") 19 | } 20 | } 21 | Button("Launch explorer") { 22 | if let bottle = bottles.first(where: { $0.id == selectedBottle }) { 23 | bottle.launchApplication("explorer") 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Harbor/UIElements/Commands/HarborMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileMenu.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 09/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HarborMenu: Commands { 11 | @Bindable var menuUIStates: MenuUIStates 12 | var body: some Commands { 13 | CommandGroup(after: .appVisibility) { 14 | Divider() 15 | Button("menu.harbor.killAll") { 16 | HarborUtils.shared.dropNukeOnWine() 17 | } 18 | .keyboardShortcut("k", modifiers: [.command, .option, .shift]) 19 | Button("menu.harbor.nukeShaders") { 20 | HarborUtils.shared.dropNukeOnWine() // I'd rather prevent issues 21 | HarborUtils.shared.wipeShaderCache() 22 | } 23 | Divider() 24 | Button("menu.harbor.installDXVK") { 25 | menuUIStates.shouldShowDXVKSheet = true 26 | } 27 | Button("sheet.GPTKConfig.title") { 28 | menuUIStates.shouldShowGPTKReinstallSheet = true 29 | } 30 | // Divider() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Harbor/UIElements/Commands/ViewMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewMenu.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 18/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ViewMenu: Commands { 11 | @AppStorage("ViewMode") var viewMode: BottleManagementViewModes = .card 12 | 13 | var body: some Commands { 14 | CommandGroup(before: .toolbar) { 15 | Picker("view.mode", selection: $viewMode) { 16 | Text("view.mode.card").tag(BottleManagementViewModes.card) 17 | Text("view.mode.table").tag(BottleManagementViewModes.table) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Harbor/UIElements/GPKInstall/BrewInstallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrewInstallView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BrewInstallView: View { 11 | @Binding var isPresented: Bool 12 | 13 | @Environment(\.brewUtils) 14 | var brewUtils 15 | 16 | @State var isInstallingBrew = false 17 | // Timer to periodically check if Homebrew is installed 18 | let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() 19 | 20 | var body: some View { 21 | VStack { 22 | Group { 23 | Text("sheet.HBInstall.title") 24 | .padding() 25 | .bold() 26 | .font(.title) 27 | Text("sheet.HBInstall.subtitle") 28 | .multilineTextAlignment(.center) 29 | } 30 | Spacer() 31 | Group { 32 | if !isInstallingBrew { 33 | Group { 34 | if brewUtils.installed { 35 | Text("sheet.HBInstall.status.installed") 36 | .foregroundColor(.green) 37 | } else { 38 | Text("sheet.HBInstall.status.notInstalled") 39 | .foregroundColor(.red) 40 | } 41 | } 42 | } else { 43 | // Loading indicator 44 | ProgressView() 45 | .progressViewStyle(CircularProgressViewStyle()) 46 | .onReceive(timer) { _ in 47 | brewUtils.testX64Brew() 48 | 49 | if brewUtils.installed { 50 | timer.upstream.connect().cancel() 51 | } 52 | } 53 | Text("sheet.HBInstall.status.installing") 54 | } 55 | } 56 | .padding() 57 | Spacer() 58 | HStack { 59 | Button("btn.cancel") { 60 | isPresented = false 61 | } 62 | if brewUtils.installed { 63 | Button("btn.OK") { 64 | isPresented = false 65 | } 66 | } else { 67 | Button("btn.install") { 68 | Task.detached(priority: .userInitiated) { 69 | isInstallingBrew = true 70 | brewUtils.installX64Brew() 71 | isInstallingBrew = false 72 | } 73 | } 74 | } 75 | } 76 | } 77 | .padding() 78 | .frame(minHeight: 300) 79 | } 80 | } 81 | 82 | struct BrewInstallView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | BrewInstallView(isPresented: Binding.constant(true)) 85 | .environment(\.brewUtils, .init()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Harbor/UIElements/GPKInstall/GPKFastInstallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPKFastInstallView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 12/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GPKFastInstallView: View { 11 | @Binding var isPresented: Bool 12 | 13 | @State var gpkSelected = false 14 | @State var gpkInstalling = false 15 | @State var gpkPath: URL? 16 | 17 | @Environment(\.gpkUtils) 18 | var gpkUtils 19 | @Environment(\.brewUtils) 20 | var brewUtils 21 | 22 | @State var isUsingNewArchive = false 23 | 24 | var body: some View { 25 | VStack { 26 | Text("sheet.GPKInstall.title") 27 | .bold() 28 | .font(.title) 29 | .padding() 30 | Text("sheet.fastGPKInstall.subtitle") 31 | .multilineTextAlignment(.center) 32 | .padding() 33 | 34 | if gpkUtils.status == .installed { 35 | Text("sheet.GPKInstall.status.installed") 36 | .foregroundColor(.green) 37 | .padding() 38 | } else { 39 | VStack { 40 | HStack { 41 | Button { 42 | if let url = URL(string: 43 | "https://github.com/ohaiibuzzle/HarborBuilder/actions/workflows/1.build-gptk.yml") { 44 | NSWorkspace.shared.open(url) 45 | } 46 | } label: { 47 | Text("HarborBuilder") 48 | .frame(minWidth: 200) 49 | } 50 | Text("") 51 | } 52 | Grid { 53 | GridRow { 54 | Button { 55 | let panel = NSOpenPanel() 56 | panel.canChooseFiles = true 57 | panel.canChooseDirectories = false 58 | panel.allowsMultipleSelection = false 59 | panel.allowedContentTypes = [.gzip] 60 | panel.begin { response in 61 | if response == .OK { 62 | let result = panel.url 63 | if let result = result { 64 | gpkPath = result 65 | if checkForNewBuilderFormat(for: result) { 66 | isUsingNewArchive = true 67 | } 68 | } 69 | } 70 | } 71 | } label: { 72 | Text("sheet.fastGPKInstall.btn.selectBottle") 73 | .frame(minWidth: 200) 74 | } 75 | if let gpkPath = gpkPath { 76 | Text(gpkPath.lastPathComponent) 77 | } else { 78 | Text("") 79 | } 80 | } 81 | if !isUsingNewArchive { 82 | GridRow { 83 | Button { 84 | let panel = NSOpenPanel() 85 | panel.canChooseFiles = true 86 | panel.canChooseDirectories = false 87 | panel.allowsMultipleSelection = false 88 | panel.allowedContentTypes = [.diskImage] 89 | panel.begin { response in 90 | if response == .OK { 91 | let result = panel.url 92 | if let result = result { 93 | let destination = HarborUtils.shared.getContainerHome() 94 | .appendingPathComponent("GPK.dmg") 95 | do { 96 | // Remove any existing GPK.dmg 97 | if FileManager.default.fileExists(atPath: destination.path) { 98 | try FileManager.default.removeItem(at: destination) 99 | } 100 | try FileManager.default.copyItem(at: result, to: destination) 101 | gpkSelected = true 102 | } catch { 103 | NSLog("sheet.GPKInstall.status.failedCopy \(destination)") 104 | } 105 | } 106 | } 107 | } 108 | } label: { 109 | Text("sheet.GPKInstall.btn.selectGPK") 110 | .frame(minWidth: 200) 111 | } 112 | if gpkSelected { 113 | Text("sheet.GPKInstall.status.selected") 114 | .foregroundColor(.green) 115 | } else { 116 | Text("") 117 | } 118 | } 119 | } else { 120 | Text("sheet.fastGPKInstall.fastArchiveDetected") 121 | } 122 | } 123 | .padding() 124 | 125 | HStack { 126 | Button("btn.cancel") { 127 | isPresented = false 128 | } 129 | 130 | if gpkUtils.status != .installed { 131 | Button(action: { 132 | if let gpkConcretePath = gpkPath { 133 | gpkInstalling = true 134 | Task.detached(priority: .userInitiated) { 135 | gpkUtils.fastInstallGPK(using: brewUtils, 136 | gpkBottle: gpkConcretePath, 137 | bundledGPK: isUsingNewArchive) 138 | Task { @MainActor in 139 | gpkInstalling = false 140 | gpkUtils.checkGPKInstallStatus() 141 | } 142 | } 143 | } 144 | }, label: { 145 | Text("sheet.GPKInstall.btn.install") 146 | }) 147 | .disabled(!gpkSelected && !isUsingNewArchive) 148 | } else { 149 | Button(action: { 150 | isPresented = false 151 | }, label: { 152 | Text("btn.OK") 153 | }) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | .padding() 160 | .frame(minHeight: 300) 161 | } 162 | 163 | // Function to check if the new Builder format (incl. gptk) is being used 164 | // Input would be a file url 165 | func checkForNewBuilderFormat(for path: URL) -> Bool { 166 | let upperDir = path.deletingLastPathComponent() 167 | // Look for gptk_libs 168 | let gptkLibs = upperDir.appendingPathComponent("gptk_libs") 169 | if FileManager.default.fileExists(atPath: gptkLibs.path) { 170 | return true 171 | } 172 | return false 173 | } 174 | } 175 | 176 | struct GPKFastInstallView_Previews: PreviewProvider { 177 | static var previews: some View { 178 | GPKFastInstallView(isPresented: Binding.constant(true)) 179 | .environment(\.gpkUtils, .init()) 180 | .environment(\.brewUtils, .init()) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Harbor/UIElements/GPKInstall/GPKSafeInstallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPKDownloadView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GPKSafeInstallView: View { 11 | @Binding var isPresented: Bool 12 | @State var gpkSelected = false 13 | @State var gpkInstalling = false 14 | 15 | @Environment(\.gpkUtils) 16 | var gpkUtils 17 | @Environment(\.brewUtils) 18 | var brewUtils 19 | 20 | var body: some View { 21 | VStack { 22 | Text("sheet.GPKInstall.title") 23 | .bold() 24 | .font(.title) 25 | .padding() 26 | Text("sheet.GPKInstall.subTitle") 27 | .multilineTextAlignment(.center) 28 | .padding() 29 | if gpkUtils.status == .installed { 30 | Text("sheet.GPKInstall.status.installed") 31 | .foregroundColor(.green) 32 | .padding() 33 | } else { 34 | VStack { 35 | HStack { 36 | Button(action: { 37 | if let url = URL( 38 | string: "https://developer.apple.com/download/more/?=game%20porting%20toolkit") { 39 | NSWorkspace.shared.open(url) 40 | } 41 | }, label: { 42 | Text("sheet.GPKInstall.btn.download") 43 | }) 44 | // Browse button for GPK 45 | Button("btn.browse") { 46 | let panel = NSOpenPanel() 47 | panel.canChooseFiles = true 48 | panel.canChooseDirectories = false 49 | panel.allowsMultipleSelection = false 50 | panel.allowedContentTypes = [.diskImage] 51 | panel.begin { response in 52 | if response == .OK { 53 | let result = panel.url 54 | if let result = result { 55 | let destination = HarborUtils.shared.getContainerHome() 56 | .appendingPathComponent("GPK.dmg") 57 | do { 58 | // Remove any existing GPK.dmg 59 | if FileManager.default.fileExists(atPath: destination.path) { 60 | try FileManager.default.removeItem(at: destination) 61 | } 62 | try FileManager.default.copyItem(at: result, to: destination) 63 | gpkSelected = true 64 | } catch { 65 | NSLog("sheet.GPKInstall.status.failedCopy \(destination)") 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | if gpkSelected { 73 | Text("sheet.GPKInstall.status.selected") 74 | .foregroundColor(.green) 75 | } 76 | } 77 | .padding() 78 | } 79 | 80 | HStack { 81 | Button("btn.cancel") { 82 | isPresented = false 83 | } 84 | 85 | if gpkUtils.status != .installed { 86 | Button(action: { 87 | if gpkUtils.showGPKInstallAlert() { 88 | gpkInstalling = true 89 | Task.detached(priority: .userInitiated) { 90 | gpkUtils.installGPK(using: brewUtils) 91 | Task { @MainActor in 92 | gpkInstalling = false 93 | gpkUtils.checkGPKInstallStatus() 94 | } 95 | } 96 | } 97 | }, label: { 98 | Text("sheet.GPKInstall.btn.install") 99 | }) 100 | .disabled(gpkSelected == false) 101 | } else { 102 | Button(action: { 103 | isPresented = false 104 | }, label: { 105 | Text("btn.OK") 106 | }) 107 | } 108 | } 109 | } 110 | .padding() 111 | .frame(minHeight: 300) 112 | } 113 | } 114 | 115 | struct GPKSafeInstallView_Previews: PreviewProvider { 116 | static var previews: some View { 117 | GPKSafeInstallView(isPresented: Binding.constant(true)) 118 | .environment(\.gpkUtils, .init()) 119 | .environment(\.brewUtils, .init()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Harbor/UIElements/GPKInstall/XCLIInstallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCLIInstallView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 09/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct XCLIInstallView: View { 11 | @State var userHasAttemptedAutomaticInstall = false 12 | 13 | @Binding var isPresented: Bool 14 | 15 | @Environment(\.xcliUtils) 16 | var xcliUtils 17 | 18 | let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() 19 | 20 | var body: some View { 21 | VStack { 22 | Group { 23 | Text("sheet.XCLIInstall.title") 24 | .padding() 25 | .bold() 26 | .font(.title) 27 | Text("sheet.XCLIInstall.subtitle") 28 | .multilineTextAlignment(.center) 29 | } 30 | 31 | Group { 32 | if !xcliUtils.installed { 33 | ProgressView() 34 | .padding() 35 | .onReceive(timer) { _ in 36 | xcliUtils.checkXcliInstalled() 37 | 38 | if xcliUtils.installed { 39 | timer.upstream.connect().cancel() 40 | isPresented = false 41 | } 42 | } 43 | if userHasAttemptedAutomaticInstall { 44 | Text("sheet.XCLIInstall.status.waiting") 45 | .multilineTextAlignment(.center) 46 | 47 | Text("sheet.XCLIInstall.automaticFailed") 48 | Button("btn.download") { 49 | if let url = URL(string: 50 | "https://developer.apple.com/download/more/?=command%20line%20tools") { 51 | // Have to be done manually since Apple don't beta seed this 52 | NSWorkspace.shared.open(url) 53 | } 54 | } 55 | } else { 56 | Button("sheet.XCLIInstall.attemptAutomaticInstall") { 57 | let aaplScript = """ 58 | property shellScript : "clear && xcode-select --install || \ 59 | echo '\(String(localized: "sheet.XCLIInstall.shellFinished"))'" 60 | tell application "Terminal" 61 | activate 62 | delay 2 63 | -- Install Run shell script 64 | do script shellScript in front window 65 | end tell 66 | """ 67 | Task.detached { 68 | if let script = NSAppleScript(source: aaplScript) { 69 | var error: NSDictionary? 70 | script.executeAndReturnError(&error) 71 | if let error = error { 72 | NSLog("Harbor: Failed to execute AppleScript: \(error)") 73 | } 74 | } else { 75 | return 76 | } 77 | } 78 | } 79 | } 80 | } else { 81 | Text("sheet.XCLIInstall.status.installed") 82 | .padding() 83 | .multilineTextAlignment(.center) 84 | .foregroundColor(.green) 85 | } 86 | } 87 | .padding() 88 | 89 | HStack { 90 | Spacer() 91 | Button("btn.cancel") { 92 | isPresented = false 93 | } 94 | .padding() 95 | .keyboardShortcut(.cancelAction) 96 | 97 | Button("btn.OK") { 98 | xcliUtils.checkXcliInstalled() 99 | isPresented = false 100 | } 101 | .disabled(!xcliUtils.installed) 102 | .padding() 103 | .keyboardShortcut(.defaultAction) 104 | Spacer() 105 | } 106 | } 107 | .frame(minHeight: 300) 108 | } 109 | } 110 | 111 | struct XCLIInstallView_Previews: PreviewProvider { 112 | static var previews: some View { 113 | XCLIInstallView(isPresented: Binding.constant(true)) 114 | .environment(\.xcliUtils, .init()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Harbor/UIElements/MenuCommands/DXVKInstallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DXVKInstallView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 19/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DXVKInstallView: View { 11 | @Binding var isPresented: Bool 12 | @State var dxvkPath: URL? 13 | @State var isWorking = false 14 | var body: some View { 15 | VStack { 16 | Text("sheet.dxvk.title") 17 | .font(.title) 18 | .bold() 19 | .padding() 20 | Text("sheet.dxvk.descriptions") 21 | .multilineTextAlignment(.center) 22 | .padding() 23 | Spacer() 24 | 25 | if !isWorking { 26 | Button("btn.browse") { 27 | let panel = NSOpenPanel() 28 | panel.canChooseFiles = true 29 | panel.canChooseDirectories = false 30 | panel.allowsMultipleSelection = false 31 | panel.allowedContentTypes = [.gzip] 32 | panel.begin { response in 33 | if response == .OK { 34 | let result = panel.url 35 | if let result = result { 36 | dxvkPath = result 37 | } 38 | } 39 | } 40 | } 41 | .disabled(!DXUtils.shared.vulkanAvailable) 42 | if dxvkPath != nil { 43 | Text("sheet.dxvk.dxvkSelected") 44 | .foregroundColor(.green) 45 | } 46 | } else { 47 | ProgressView() 48 | .progressViewStyle(.circular) 49 | } 50 | Spacer() 51 | HStack { 52 | Button("btn.cancel") { 53 | isPresented = false 54 | } 55 | Button("btn.OK") { 56 | if let dxvkPath = dxvkPath { 57 | isWorking = true 58 | Task.detached { 59 | DXUtils.shared.untarDXVKLibs(dxvkZip: dxvkPath) 60 | Task { @MainActor in 61 | isWorking = false 62 | isPresented = false 63 | } 64 | } 65 | } 66 | } 67 | .disabled(dxvkPath == nil || !DXUtils.shared.vulkanAvailable) 68 | .buttonStyle(.borderedProminent) 69 | } 70 | .disabled(isWorking) 71 | } 72 | .padding() 73 | .frame(minHeight: 300) 74 | } 75 | } 76 | 77 | struct DXVKInstallView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | DXVKInstallView(isPresented: Binding.constant(true)) 80 | .environment(\.xcliUtils, .init()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Harbor/UIElements/MenuCommands/GPTKConfigView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReinstallGPTKView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 01/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GPTKConfigView: View { 11 | @Binding var isPresented: Bool 12 | @State var gpkSelected = false 13 | @Environment(\.gpkUtils) var gpkUtils 14 | 15 | var body: some View { 16 | VStack { 17 | Text("sheet.GPTKConfig.title") 18 | .font(.title) 19 | .padding() 20 | 21 | Text("sheet.GPTKConfig.subtitle") 22 | .multilineTextAlignment(.center) 23 | 24 | VStack(alignment: .center) { 25 | HStack { 26 | Button("btn.browse") { 27 | let panel = NSOpenPanel() 28 | panel.canChooseFiles = true 29 | panel.canChooseDirectories = false 30 | panel.allowsMultipleSelection = false 31 | panel.allowedContentTypes = [.diskImage] 32 | panel.begin { response in 33 | if response == .OK { 34 | let result = panel.url 35 | if let result = result { 36 | let destination = HarborUtils.shared.getContainerHome() 37 | .appendingPathComponent("GPK.dmg") 38 | do { 39 | // Remove any existing GPK.dmg 40 | if FileManager.default.fileExists(atPath: destination.path) { 41 | try FileManager.default.removeItem(at: destination) 42 | } 43 | try FileManager.default.copyItem(at: result, to: destination) 44 | gpkSelected = true 45 | } catch { 46 | NSLog("sheet.GPKInstall.status.failedCopy \(destination)") 47 | } 48 | } 49 | } 50 | } 51 | } 52 | if gpkSelected { 53 | Text("sheet.GPKInstall.status.selected") 54 | .foregroundColor(.green) 55 | } 56 | } 57 | .padding() 58 | 59 | Button("sheet.GPTKConfig.updateGPTKLibs") { 60 | gpkUtils.reinstallGPKLibraries() 61 | isPresented.toggle() 62 | } 63 | .disabled(!gpkSelected) 64 | 65 | Button("sheet.GPTKConfig.removeGPTK") { 66 | let alert = NSAlert() 67 | alert.messageText = String(localized: "sheet.GPTKConfig.removeGPTK.title") 68 | alert.informativeText = String(localized: "sheet.GPTKConfig.removeGPTK.subtitle") 69 | alert.addButton(withTitle: String(localized: "home.btn.nuke")) 70 | alert.addButton(withTitle: String(localized: "btn.cancel")) 71 | alert.alertStyle = .warning 72 | alert.runModal() == .alertFirstButtonReturn ? { 73 | Task.detached { 74 | gpkUtils.completelyRemoveGPK() 75 | Task { @MainActor in 76 | isPresented.toggle() 77 | } 78 | } 79 | }() : () 80 | } 81 | .buttonStyle(.borderedProminent) 82 | .tint(.red) 83 | } 84 | .padding() 85 | Button("btn.OK") { 86 | isPresented.toggle() 87 | } 88 | } 89 | .padding() 90 | } 91 | } 92 | 93 | struct GPTKConfigView_Previews: PreviewProvider { 94 | static var previews: some View { 95 | GPTKConfigView(isPresented: Binding.constant(true)) 96 | .environment(\.brewUtils, .init()) 97 | .environment(\.gpkUtils, .init()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Harbor/UIElements/Winetricks/WinetricksUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WinetricksUI.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 03/11/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WinetricksUI: View { 11 | let window: NSWindow 12 | 13 | @Binding var bottle: HarborBottle 14 | 15 | @State private var wineTricks: [WinetricksCategory]? 16 | @State private var selectedTrick: WinetricksVerb.ID? 17 | 18 | var body: some View { 19 | VStack { 20 | VStack { 21 | Text("sheet.winetricks.title") 22 | .font(.title) 23 | Text("sheet.winetricks.subtitle \(bottle.name)") 24 | .multilineTextAlignment(.center) 25 | } 26 | .padding(.bottom) 27 | 28 | // Tabbed view 29 | if let wineTricks = wineTricks { 30 | TabView { 31 | ForEach(wineTricks, id: \.name) { category in 32 | Table(category.verbs, selection: $selectedTrick) { 33 | TableColumn("sheet.winetricks.table.name", value: \.name) 34 | TableColumn("sheet.winetricks.table.description", value: \.description) 35 | } 36 | .tabItem { 37 | Text(category.name) 38 | } 39 | } 40 | } 41 | HStack { 42 | Spacer() 43 | Button("btn.close") { 44 | window.close() 45 | } 46 | Button("btn.install") { 47 | guard let selectedTrick = selectedTrick else { 48 | return 49 | } 50 | 51 | let trick = wineTricks.flatMap { $0.verbs }.first(where: { $0.id == selectedTrick }) 52 | WinetricksUtils.shared.launchWinetricksShell(for: bottle, with: trick?.name) 53 | } 54 | .buttonStyle(.borderedProminent) 55 | } 56 | } else { 57 | Spacer() 58 | ProgressView() 59 | .progressViewStyle(.circular) 60 | .controlSize(.large) 61 | Spacer() 62 | } 63 | } 64 | .padding() 65 | .onAppear { 66 | Task.detached { 67 | wineTricks = await WinetricksUtils.shared.parseVerbs() 68 | } 69 | } 70 | .frame(minWidth: 600, minHeight: 400) 71 | } 72 | 73 | static func openWindow(for bottle: HarborBottle) { 74 | let window = NSWindow( 75 | contentRect: NSRect(x: 0, y: 0, width: 600, height: 400), 76 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 77 | backing: .buffered, defer: false) 78 | 79 | window.title = "Winetricks" 80 | window.center() 81 | window.setFrameAutosaveName("Winetricks") 82 | window.contentView = NSHostingView(rootView: WinetricksUI(window: window, bottle: .constant(bottle))) 83 | window.makeKeyAndOrderFront(nil) 84 | } 85 | } 86 | 87 | #Preview { 88 | WinetricksUI(window: NSWindow(), bottle: .constant(HarborBottle(id: UUID(), 89 | name: "Test", path: URL(fileURLWithPath: "/Users/venti/.wine")))) 90 | } 91 | -------------------------------------------------------------------------------- /Harbor/Utils/Environment+BrewUtils.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | private struct BrewUtilsEnvironmentKey: EnvironmentKey { 4 | static var defaultValue: BrewUtils = .init() 5 | } 6 | 7 | extension EnvironmentValues { 8 | var brewUtils: BrewUtils { 9 | get { self[BrewUtilsEnvironmentKey.self] } 10 | set { self[BrewUtilsEnvironmentKey.self] = newValue } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Harbor/Utils/Environment+GPKUtils.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | private struct GPKUtilsEnvironmentKey: EnvironmentKey { 4 | static let defaultValue: GPKUtils = .init() 5 | } 6 | 7 | extension EnvironmentValues { 8 | var gpkUtils: GPKUtils { 9 | get { self[GPKUtilsEnvironmentKey.self] } 10 | set { self[GPKUtilsEnvironmentKey.self] = newValue } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Harbor/Utils/Environment+XCLIUtils.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | private struct XCLIUtilsEnvironmentKey: EnvironmentKey { 4 | static var defaultValue: XCLIUtils = .init() 5 | } 6 | 7 | extension EnvironmentValues { 8 | var xcliUtils: XCLIUtils { 9 | get { self[XCLIUtilsEnvironmentKey.self] } 10 | set { self[XCLIUtilsEnvironmentKey.self] = newValue } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Harbor/Utils/HarborShortcuts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URIHandler.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 27/06/2023. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | struct HarborShortcuts { 12 | static let shared = HarborShortcuts() 13 | 14 | func generateAppleScriptLauncher(bottle: HarborBottle) -> String { 15 | let wine64Path = "/usr/local/opt/game-porting-toolkit/bin/wine64" 16 | 17 | var shellScript = "" 18 | 19 | shellScript += "WINEPREFIX='\(bottle.path.path)'" 20 | if bottle.enableHUD { 21 | shellScript += " MTL_HUD_ENABLED=1" 22 | } 23 | switch bottle.syncPrimitives { 24 | case .none: 25 | break 26 | case .eSync: 27 | shellScript += " WINEESYNC=1" 28 | case .mSync: 29 | shellScript += " WINEMSYNC=1" 30 | } 31 | 32 | if !bottle.envVars.isEmpty { 33 | for (key, value) in bottle.envVars { 34 | shellScript += " \(key)='\(value)'" 35 | } 36 | } 37 | 38 | shellScript += " '\(wine64Path)' start" 39 | 40 | if !bottle.primaryApplicationWorkDir.isEmpty { 41 | shellScript += " /d '\(bottle.primaryApplicationWorkDir)'" 42 | } 43 | 44 | if !bottle.primaryApplicationPath.isEmpty { 45 | shellScript += " '\(bottle.primaryApplicationPath)'" 46 | } 47 | 48 | if !bottle.primaryApplicationArgument.isEmpty { 49 | shellScript += " \(bottle.primaryApplicationArgument)" 50 | } 51 | 52 | shellScript = shellScript.replacingOccurrences(of: "\\", with: "\\\\") 53 | shellScript = shellScript.replacingOccurrences(of: "\"", with: "\\\"") 54 | 55 | let aaplScript = """ 56 | do shell script "\(shellScript)" 57 | """ 58 | 59 | return aaplScript 60 | } 61 | 62 | func createDesktopShortcut(for bottle: HarborBottle) { 63 | let aaplScript = generateAppleScriptLauncher(bottle: bottle) 64 | 65 | let tempDir = FileManager.default.temporaryDirectory 66 | guard let desktopPath = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first else { 67 | return 68 | } 69 | let scriptFile = tempDir.appendingPathComponent("launch.scpt") 70 | let appFile = desktopPath.appendingPathComponent("\(bottle.name).app") 71 | 72 | do { 73 | try aaplScript.write(to: scriptFile, atomically: true, encoding: .utf8) 74 | let compileProcess = Process() 75 | compileProcess.launchPath = "/usr/bin/osacompile" 76 | compileProcess.arguments = ["-o", appFile.path, scriptFile.path] 77 | try compileProcess.run() 78 | compileProcess.waitUntilExit() 79 | 80 | if compileProcess.terminationStatus != 0 { 81 | NSLog("Failed to compile AppleScript") 82 | return 83 | } 84 | 85 | // Replace the applet.icns with BottleIcon.icns from the assets 86 | if let iconFile = Bundle.main.url(forResource: "BottleIcon", withExtension: "icns") { 87 | let iconDestination = appFile.appendingPathComponent("Contents/Resources/applet.icns") 88 | try FileManager.default.removeItem(at: iconDestination) 89 | try FileManager.default.copyItem(at: iconFile, to: iconDestination) 90 | } 91 | 92 | // Remove the temporary files 93 | try FileManager.default.removeItem(at: scriptFile) 94 | 95 | let alert = NSAlert() 96 | alert.messageText = String(localized: "desktopShortcutCreated.title \(bottle.name)") 97 | alert.informativeText = String(localized: "desktopShortcutCreated.message \(bottle.name)") 98 | alert.alertStyle = .informational 99 | alert.addButton(withTitle: "OK") 100 | alert.runModal() 101 | } catch { 102 | HarborUtils.shared.quickError(error.localizedDescription) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Harbor/Utils/HarborUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonUtils.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 07/06/2023. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | struct HarborUtils { 12 | static let shared = HarborUtils() 13 | 14 | func getContainerHome() -> URL { 15 | // Since we are running without App Sandbox, we start at Home... 16 | // So we create our own 17 | let home = FileManager.default.homeDirectoryForCurrentUser 18 | let harborHome = home.appendingPathComponent("Library/Containers/dev.ohaiibuzzle.Harbor/Data") 19 | // Create it if needed 20 | if !FileManager.default.fileExists(atPath: harborHome.path) { 21 | do { 22 | try FileManager.default.createDirectory(at: harborHome, 23 | withIntermediateDirectories: true, attributes: nil) 24 | } catch { 25 | NSLog("Harbor: Failed to create Harbor home directory") 26 | } 27 | } 28 | return harborHome 29 | } 30 | 31 | func quickError(_ errorMessage: String) { 32 | let alert = NSAlert() 33 | alert.alertStyle = .critical 34 | alert.messageText = String(localized: "harbor.errorAlert") 35 | alert.informativeText = errorMessage 36 | alert.runModal() 37 | } 38 | 39 | func dropNukeOnWine() { 40 | // SIGKILL any `wineserver` processes 41 | var task = Process() 42 | task.launchPath = "/usr/bin/killall" 43 | task.arguments = ["-9", "wineserver"] 44 | do { 45 | try task.run() 46 | } catch { 47 | HarborUtils.shared.quickError(error.localizedDescription) 48 | } 49 | task.waitUntilExit() 50 | 51 | task = Process() 52 | task.launchPath = "/usr/bin/killall" 53 | task.arguments = ["-9", "wine64-preloader"] 54 | do { 55 | try task.run() 56 | } catch { 57 | HarborUtils.shared.quickError(error.localizedDescription) 58 | } 59 | task.waitUntilExit() 60 | } 61 | 62 | func wipeShaderCache() { 63 | // cd $(getconf DARWIN_USER_CACHE_DIR)/d3dm 64 | let getconf = Process() 65 | getconf.executableURL = URL(fileURLWithPath: "/usr/bin/getconf") 66 | getconf.arguments = ["DARWIN_USER_CACHE_DIR"] 67 | let pipe = Pipe() 68 | getconf.standardOutput = pipe 69 | do { 70 | try getconf.run() 71 | } catch { 72 | HarborUtils.shared.quickError(error.localizedDescription) 73 | } 74 | getconf.waitUntilExit() 75 | 76 | let getconfOutput = pipe.fileHandleForReading.readDataToEndOfFile() 77 | guard let getconfOutputString = String(data: getconfOutput, encoding: .utf8) else { 78 | return 79 | } 80 | 81 | let d3dmPath = URL(fileURLWithPath: getconfOutputString.trimmingCharacters(in: .whitespacesAndNewlines)) 82 | .appendingPathComponent("d3dm").path 83 | do { 84 | try FileManager.default.removeItem(atPath: d3dmPath) 85 | } catch { 86 | HarborUtils.shared.quickError(error.localizedDescription) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Harbor/Utils/MenuUIStates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuUIState.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 19/06/2023. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @Observable 12 | final class MenuUIStates { 13 | // This class is for stuff that needs to be passed between 14 | // the menu items and the UI elements (eg. sheets within views) 15 | var shouldShowDXVKSheet = false 16 | var shouldShowGPTKReinstallSheet = false 17 | } 18 | -------------------------------------------------------------------------------- /Harbor/Views/BottleManagementCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleManagementCardView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 18/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleManagementCardView: View { 11 | @State private var bottleState = BottleList() 12 | @State private var selectedBottle: HarborBottle.ID? 13 | 14 | @State private var showNewBottleSheet = false 15 | @State private var showBottleDetail = false 16 | 17 | @State private var sortOrder = [KeyPathComparator(\HarborBottle.name)] 18 | 19 | var body: some View { 20 | NavigationStack { 21 | VStack { 22 | VStack { 23 | Text("home.bottles.title") 24 | .font(.title) 25 | .padding() 26 | Text("home.bottles.subtitle") 27 | .padding() 28 | .multilineTextAlignment(.center) 29 | } 30 | BottleCardListView(bottles: bottleState, isShowingDetails: $showBottleDetail) 31 | } 32 | .toolbar { 33 | ToolbarItem(placement: .automatic) { 34 | Button { 35 | showNewBottleSheet = true 36 | } label: { 37 | Label("home.btn.new", systemImage: "plus") 38 | } 39 | } 40 | } 41 | .sheet(isPresented: $showNewBottleSheet) { 42 | NewBottleDropdown(isPresented: $showNewBottleSheet, 43 | bottle: HarborBottle(id: UUID(), name: "", path: URL(fileURLWithPath: ""))) 44 | } 45 | .onChange(of: showNewBottleSheet) { 46 | bottleState.reload() 47 | } 48 | .onChange(of: showBottleDetail) { 49 | bottleState.reload() 50 | } 51 | } 52 | } 53 | } 54 | 55 | struct BottleManagementCardView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | BottleManagementCardView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Harbor/Views/BottleManagementTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleManagementTableView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 18/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BottleManagementTableView: View { 11 | @State private var bottleState = BottleList() 12 | @State private var selectedBottle: HarborBottle.ID? 13 | 14 | @State private var showNewBottleSheet = false 15 | @State private var showEditBottleSheet = false 16 | @State private var showLaunchExtSheet = false 17 | @State private var showAdvConfigSheet = false 18 | 19 | @State private var sortOrder = [KeyPathComparator(\HarborBottle.name)] 20 | var body: some View { 21 | VStack { 22 | VStack { 23 | Text("home.bottles.title") 24 | .font(.title) 25 | .padding() 26 | Text("home.bottles.subtitle") 27 | .padding() 28 | .multilineTextAlignment(.center) 29 | } 30 | 31 | Table(bottleState.bottles, selection: $selectedBottle, sortOrder: $sortOrder) { 32 | TableColumn("home.table.name", value: \.name) 33 | TableColumn("home.table.path", value: \.path.prettyFileUrl) 34 | TableColumn("home.table.primaryApp", value: \.primaryApplicationPath) 35 | } 36 | .padding() 37 | .frame(minWidth: 500, minHeight: 200) 38 | .onChange(of: sortOrder) { _, newOrder in 39 | bottleState.bottles.sort(using: newOrder) 40 | } 41 | } 42 | .toolbar { 43 | ToolbarItem(placement: .automatic) { 44 | Button { 45 | showNewBottleSheet = true 46 | } label: { 47 | Label("home.btn.new", systemImage: "plus") 48 | } 49 | } 50 | ToolbarItem(placement: .automatic) { 51 | Button { 52 | showEditBottleSheet = true 53 | } label: { 54 | Label("home.btn.edit", systemImage: "pencil") 55 | } 56 | .disabled(selectedBottle == nil) 57 | } 58 | ToolbarItem(placement: .automatic) { 59 | Button { 60 | if let thisBottle = bottleState.bottles.first(where: { $0.id == selectedBottle }) { 61 | thisBottle.launchPrimaryApplication() 62 | } 63 | } label: { 64 | Label("home.btn.run", systemImage: "play") 65 | } 66 | .disabled(selectedBottle == nil) 67 | } 68 | ToolbarItem(placement: .automatic) { 69 | Button { 70 | showLaunchExtSheet = true 71 | } label: { 72 | Label("home.btn.runExt", systemImage: "tray.and.arrow.down") 73 | } 74 | .disabled(selectedBottle == nil) 75 | } 76 | ToolbarItem(placement: .automatic) { 77 | Button { 78 | if let thisBottle = bottleState.bottles.first(where: { $0.id == selectedBottle }) { 79 | thisBottle.killBottle() 80 | } 81 | } label: { 82 | Label("home.btn.kill", systemImage: "stop") 83 | } 84 | .disabled(selectedBottle == nil) 85 | } 86 | ToolbarItem(placement: .automatic) { 87 | Button { 88 | showAdvConfigSheet = true 89 | } label: { 90 | Label("home.btn.advConf", systemImage: "gear") 91 | } 92 | .disabled(selectedBottle == nil) 93 | } 94 | ToolbarItem(placement: .automatic) { 95 | Button { 96 | if let thisBottle = bottleState.bottles.first(where: { $0.id == selectedBottle }) { 97 | // ALARM 98 | let alert = NSAlert() 99 | alert.messageText = String(localized: "home.alert.deleteTitle") 100 | alert.alertStyle = .critical 101 | let checkbox = NSButton(checkboxWithTitle: 102 | String(format: String(localized: "home.alert.deletePath %@"), 103 | thisBottle.path.prettyFileUrl), target: nil, action: nil) 104 | checkbox.state = .on 105 | alert.accessoryView = checkbox 106 | alert.addButton(withTitle: String(localized: "btn.delete")) 107 | alert.addButton(withTitle: String(localized: "btn.cancel")) 108 | if alert.runModal() == .alertFirstButtonReturn { 109 | // User clicked on "Delete" 110 | if let thisBottle = bottleState.bottles.first(where: { $0.id == selectedBottle }) { 111 | BottleLoader.shared.delete(thisBottle, checkbox.state) 112 | bottleState.bottles.removeAll(where: { $0.id == selectedBottle }) 113 | bottleState.flush() 114 | bottleState.reload() 115 | selectedBottle = nil 116 | } 117 | } else { 118 | // User clicked on "Cancel" 119 | return 120 | } 121 | } 122 | } label: { 123 | Label("home.btn.nuke", systemImage: "trash") 124 | } 125 | .disabled(selectedBottle == nil) 126 | } 127 | } 128 | .sheet(isPresented: $showNewBottleSheet) { 129 | NewBottleDropdown(isPresented: $showNewBottleSheet, 130 | bottle: HarborBottle(id: UUID(), name: "", path: URL(fileURLWithPath: ""))) 131 | } 132 | .sheet(isPresented: $showEditBottleSheet) { 133 | if let thisBottle = bottleState.bottles.first(where: { $0.id == selectedBottle }) { 134 | EditBottleView(isPresented: $showEditBottleSheet, 135 | bottle: thisBottle) 136 | } 137 | } 138 | .sheet(isPresented: $showLaunchExtSheet) { 139 | if let thisBottle = bottleState.bottles.first(where: { $0.id == selectedBottle }) { 140 | LaunchExtDropdown(isPresented: $showLaunchExtSheet, 141 | bottle: thisBottle) 142 | } 143 | } 144 | .sheet(isPresented: $showAdvConfigSheet) { 145 | if let thisBottle = $bottleState.bottles.first(where: { $0.id == selectedBottle }) { 146 | BottleConfigDropdown(isPresented: $showAdvConfigSheet, 147 | bottle: thisBottle) 148 | } 149 | } 150 | .onChange(of: showNewBottleSheet) { 151 | bottleState.reload() 152 | } 153 | .onChange(of: showEditBottleSheet) { 154 | bottleState.reload() 155 | } 156 | 157 | } 158 | } 159 | 160 | struct BottleManagementTableView_Previews: PreviewProvider { 161 | static var previews: some View { 162 | BottleManagementTableView() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Harbor/Views/BottleManagementView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleManagementView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 08/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | import Observation 10 | 11 | enum BottleManagementViewModes: String, CaseIterable, Identifiable { 12 | var id: Self { self } 13 | 14 | case table 15 | case card 16 | } 17 | 18 | struct BottleManagementView: View { 19 | @Bindable var menuUIStates: MenuUIStates 20 | @AppStorage("ViewMode") var viewMode: BottleManagementViewModes = .card 21 | var body: some View { 22 | Group { 23 | if viewMode == .card { 24 | BottleManagementCardView() 25 | } else { 26 | BottleManagementTableView() 27 | } 28 | } 29 | .sheet(isPresented: $menuUIStates.shouldShowDXVKSheet) { 30 | DXVKInstallView(isPresented: $menuUIStates.shouldShowDXVKSheet) 31 | } 32 | .sheet(isPresented: $menuUIStates.shouldShowGPTKReinstallSheet) { 33 | GPTKConfigView(isPresented: $menuUIStates.shouldShowGPTKReinstallSheet) 34 | } 35 | } 36 | } 37 | 38 | struct BottleManagementView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | BottleManagementView(menuUIStates: MenuUIStates()) 41 | .environment(\.brewUtils, .init()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Harbor/Views/SetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupView.swift 3 | // Harbor 4 | // 5 | // Created by Venti on 08/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SetupView: View { 11 | @State var isXcliInstallerDropdownShown = false 12 | @State var isBrewInstallerDropdownShown = false 13 | @State var isGPKSafeInstallerDropdownShown = false 14 | @State var isGPKFastInstallerDropdownShown = false 15 | 16 | @Environment(\.brewUtils) 17 | var brewUtils 18 | 19 | @Environment(\.xcliUtils) 20 | var xcliUtils 21 | 22 | var body: some View { 23 | VStack { 24 | Text("setup.title") 25 | .font(.largeTitle) 26 | .bold() 27 | .padding() 28 | Text("setup.subtitle") 29 | .multilineTextAlignment(.center) 30 | Spacer() 31 | HStack { 32 | Spacer() 33 | VStack { 34 | Text("setup.saferSetup") 35 | .font(.title) 36 | Text("setup.saferSetup.subtitle") 37 | .multilineTextAlignment(.center) 38 | Group { 39 | Button { 40 | isXcliInstallerDropdownShown.toggle() 41 | } label: { 42 | Text("setup.btn.installXCLI15") 43 | .frame(minWidth: 150) 44 | } 45 | Button { 46 | isBrewInstallerDropdownShown.toggle() 47 | } label: { 48 | Text("setup.btn.installHB") 49 | .frame(minWidth: 150) 50 | } 51 | .disabled(!xcliUtils.installed) 52 | Button { 53 | isGPKSafeInstallerDropdownShown.toggle() 54 | } label: { 55 | Text("setup.btn.installGPK") 56 | .frame(minWidth: 150) 57 | } 58 | .disabled(!brewUtils.installed) 59 | } 60 | } 61 | .padding() 62 | Spacer() 63 | VStack { 64 | Text("setup.soIWouldLikeToLiveDangerously") 65 | .font(.title) 66 | Text("setup.fasterSetup.subtitle") 67 | .multilineTextAlignment(.center) 68 | 69 | Group { 70 | Button { 71 | isXcliInstallerDropdownShown.toggle() 72 | } label: { 73 | Text("setup.btn.installXCLI15") 74 | .frame(minWidth: 150) 75 | } 76 | Button { 77 | isBrewInstallerDropdownShown.toggle() 78 | } label: { 79 | Text("setup.btn.installHB") 80 | .frame(minWidth: 150) 81 | } 82 | .disabled(!xcliUtils.installed) 83 | Button { 84 | isGPKFastInstallerDropdownShown.toggle() 85 | } label: { 86 | Text("setup.btn.installGPK") 87 | .frame(minWidth: 150) 88 | } 89 | .disabled(!brewUtils.installed) 90 | } 91 | } 92 | Spacer() 93 | } 94 | .sheet(isPresented: $isXcliInstallerDropdownShown) { 95 | XCLIInstallView(isPresented: $isXcliInstallerDropdownShown) 96 | } 97 | .sheet(isPresented: $isBrewInstallerDropdownShown) { 98 | BrewInstallView(isPresented: $isBrewInstallerDropdownShown) 99 | } 100 | .sheet(isPresented: $isGPKSafeInstallerDropdownShown) { 101 | GPKSafeInstallView(isPresented: $isGPKSafeInstallerDropdownShown) 102 | } 103 | .sheet(isPresented: $isGPKFastInstallerDropdownShown) { 104 | GPKFastInstallView(isPresented: $isGPKFastInstallerDropdownShown) 105 | } 106 | Spacer() 107 | } 108 | } 109 | } 110 | 111 | struct SetupView_Previews: PreviewProvider { 112 | static var previews: some View { 113 | SetupView() 114 | .environment(\.brewUtils, .init()) 115 | .environment(\.xcliUtils, .init()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harbor 2 |

3 | 4 |

A simple, stupid wrapper for Apple's Game Porting Toolkit

5 |

6 | 7 | # What's this? 8 | Basically, it's a Wine wrapper with QoL features that eases the installation process of Apple's GPTK (via their homebrew tap) 9 | 10 | # What's up with the icon? 11 | Cooked up in about 2 minutes with IconKitchen. I'm a software engineer, not a graphic designer. 12 | 13 | # Too simplistic? 14 | If you are in need of something *stronger*, get [Whisky](https://github.com/IsaacMarovitz/Whisky). This is just the bare minimum to get GPTK working 15 | 16 | # Downloads 17 | Harbor has not quite aged enough yet, but if sailing the high seas is your jam, [start your journey](https://github.com/ohaiibuzzle/Harbor/releases) 18 | 19 | Have fun and report bugs! 20 | --------------------------------------------------------------------------------