├── images ├── cw-dark.png └── cw-light.png ├── Whisky ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 128@2x.png │ │ ├── 16@2x.png │ │ ├── 256@2x.png │ │ ├── 32@2x.png │ │ ├── 512@2x.png │ │ ├── 16R64x1.png │ │ ├── 32R64x1.png │ │ ├── 128R128x1.png │ │ ├── 256R256x1.png │ │ ├── 512R512x1.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Whisky.entitlements ├── Utils │ ├── Rosetta2.swift │ ├── WhiskyCmd.swift │ ├── Winetricks.swift │ ├── ProgramShortcut.swift │ └── Logger.swift ├── Views │ ├── AboutView.swift │ ├── SparkleView.swift │ ├── SetupView.swift │ ├── Setup Views │ │ ├── GPTKInstallView.swift │ │ └── RosettaView.swift │ ├── FileOpenView.swift │ ├── BottleRenameView.swift │ ├── BottleCreationView.swift │ └── WhiskyApp.swift ├── View Models │ └── BottleVM.swift ├── Info.plist ├── AppDelegate.swift ├── Models │ └── Program.swift ├── zh-Hans.lproj │ └── Localizable.strings ├── ko.lproj │ └── Localizable.strings ├── zh-Hant.lproj │ └── Localizable.strings ├── ja.lproj │ └── Localizable.strings ├── vi.lproj │ └── Localizable.strings ├── en.lproj │ └── Localizable.strings ├── nl.lproj │ └── Localizable.strings ├── da.lproj │ └── Localizable.strings ├── pt-PT.lproj │ └── Localizable.strings ├── fi.lproj │ └── Localizable.strings ├── tr.lproj │ └── Localizable.strings ├── it.lproj │ └── Localizable.strings ├── pt-BR.lproj │ └── Localizable.strings ├── es.lproj │ └── Localizable.strings ├── ru.lproj │ └── Localizable.strings ├── uk.lproj │ └── Localizable.strings ├── pl.lproj │ └── Localizable.strings ├── fr.lproj │ └── Localizable.strings └── de.lproj │ └── Localizable.strings ├── WhiskyThumbnail ├── Icons.xcassets │ ├── Contents.json │ └── Icon.imageset │ │ ├── 512R512x1.png │ │ └── Contents.json ├── WhiskyThumbnail.entitlements ├── Info.plist └── ThumbnailProvider.swift ├── crowdin.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── bug.yml └── workflows │ ├── SwiftLint.yml │ └── Release.yml ├── Whisky.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ ├── WhiskyKit.xcscheme │ ├── Whisky.xcscheme │ ├── WhiskyCmd.xcscheme │ └── WhiskyThumbnail.xcscheme ├── .swiftlint.yml ├── CONTRIBUTING.md ├── WhiskyKit ├── Whisky │ ├── Bottle.swift │ ├── Program.swift │ ├── BottleData.swift │ └── ProgramSettings.swift ├── Extensions │ ├── DataExtensions.swift │ └── URLExtensions.swift ├── Tar.swift ├── GPTK │ └── GPTKDownloader.swift └── PE │ └── ShellLink.swift ├── README.md └── .gitignore /images/cw-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/images/cw-dark.png -------------------------------------------------------------------------------- /images/cw-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/images/cw-light.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WhiskyThumbnail/Icons.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /Whisky/en.lproj/Localizable.strings 3 | translation: /Whisky/%osx_code%/Localizable.strings 4 | -------------------------------------------------------------------------------- /Whisky/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: isaacmarovitz 4 | custom: "https://www.codeweavers.com/store?ad=1010" 5 | -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/128@2x.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/16@2x.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/256@2x.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/32@2x.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/512@2x.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/16R64x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/16R64x1.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/32R64x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/32R64x1.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/128R128x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/128R128x1.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/256R256x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/256R256x1.png -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/512R512x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/Whisky/Assets.xcassets/AppIcon.appiconset/512R512x1.png -------------------------------------------------------------------------------- /WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BomberFish/Whisky-Intel/HEAD/WhiskyThumbnail/Icons.xcassets/Icon.imageset/512R512x1.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | - name: Discord Support 3 | url: https://discord.gg/K4AQukwAke 4 | about: Please ask for support first to confirm the issue before submitting a report. -------------------------------------------------------------------------------- /Whisky.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Whisky.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemOrangeColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WhiskyThumbnail/WhiskyThumbnail.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /WhiskyThumbnail/Icons.xcassets/Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "512R512x1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/SwiftLint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | concurrency: 4 | group: pr-checks-${{ github.event.number }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | push: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | SwiftLint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: SwiftLint 18 | uses: norio-nomura/action-swiftlint@3.2.1 19 | with: 20 | args: --strict 21 | -------------------------------------------------------------------------------- /Whisky/Whisky.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.device.audio-input 12 | 13 | com.apple.security.device.camera 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Create Discord Embed 13 | uses: tsickert/discord-webhook@v5.3.0 14 | with: 15 | webhook-url: ${{ secrets.WEBHOOK }} 16 | embed-title: ${{ github.event.release.name }} 17 | embed-url: https://github.com/Whisky-App/Whisky/releases/download/${{ github.event.release.tag_name }}/Whisky.zip 18 | embed-description: ${{ github.event.release.body }} 19 | embed-color: 9442302 20 | -------------------------------------------------------------------------------- /WhiskyThumbnail/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | QLSupportedContentTypes 10 | 11 | com.microsoft.windows-executable 12 | 13 | QLThumbnailMinimumDimension 14 | 0 15 | 16 | NSExtensionPointIdentifier 17 | com.apple.quicklook.thumbnail 18 | NSExtensionPrincipalClass 19 | $(PRODUCT_MODULE_NAME).ThumbnailProvider 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Whisky.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // ___TARGETNAME___ 9 | // 10 | // This file is part of Whisky. 11 | // 12 | // Whisky is free software: you can redistribute it and/or modify it under the terms 13 | // of the GNU General Public License as published by the Free Software Foundation, 14 | // either version 3 of the License, or (at your option) any later version. 15 | // 16 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 17 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 18 | // See the GNU General Public License for more details. 19 | // 20 | // You should have received a copy of the GNU General Public License along with Whisky. 21 | // If not, see https://www.gnu.org/licenses/. 22 | // 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature. 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: related-to-problem 8 | attributes: 9 | label: Is your feature request related to a problem? 10 | description: A clear and concise description of what the problem is. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: solution 15 | attributes: 16 | label: Describe the solution you'd like 17 | description: A clear and concise description of what you're suggesting. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: extra-info 22 | attributes: 23 | label: Anything else? 24 | description: Upload any relevant images, links, screenshots that might help realise your suggestion. 25 | validations: 26 | required: true 27 | - type: checkboxes 28 | attributes: 29 | label: Issue Language 30 | description: All issues must be written in clear plain English so that all devs are able to read them. 31 | options: 32 | - label: Yes my issue is written in English 33 | required: true -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - force_unwrapping 3 | - file_header 4 | 5 | excluded: 6 | - .build 7 | 8 | file_header: 9 | severity: error 10 | required_pattern: | 11 | \/\/ .*?\.swift 12 | \/\/ .*? 13 | \/\/ 14 | \/\/ This file is part of Whisky\. 15 | \/\/ 16 | \/\/ Whisky is free software: you can redistribute it and\/or modify it under the terms 17 | \/\/ of the GNU General Public License as published by the Free Software Foundation, 18 | \/\/ either version 3 of the License, or \(at your option\) any later version\. 19 | \/\/ 20 | \/\/ Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 21 | \/\/ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE\. 22 | \/\/ See the GNU General Public License for more details\. 23 | \/\/ 24 | \/\/ You should have received a copy of the GNU General Public License along with Whisky\. 25 | \/\/ If not, see https:\/\/www\.gnu\.org\/licenses\/\. 26 | \/\/ 27 | -------------------------------------------------------------------------------- /Whisky/Utils/Rosetta2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rosetta2.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | class Rosetta2 { 22 | static let rosetta2RuntimeBin = "/Library/Apple/usr/libexec/oah/libRosettaRuntime" 23 | 24 | static let isRosettaInstalled: Bool = { 25 | return FileManager.default.fileExists(atPath: rosetta2RuntimeBin) 26 | }() 27 | 28 | static func launchRosettaInstaller() { 29 | let task = Process() 30 | task.launchPath = "/usr/sbin/softwareupdate" 31 | task.arguments = ["--install-rosetta"] 32 | do { 33 | try task.run() 34 | } catch { 35 | NSLog("Failed to install Rosetta 2: \(error)") 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Whisky/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16R64x1.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32R64x1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128R128x1.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256R256x1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512R512x1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Thanks for your interest! First, make a fork of Whisky, make a new branch for your changes, and get coding! 4 | 5 | # Build environment 6 | 7 | Whisky is built using Xcode 15 on macOS Sonoma. All external dependencies are handled through the Swift Package Manager. 8 | 9 | # Code style 10 | 11 | Every Whisky commit is automatically linted using SwiftLint. You can run these checks locally simply by building in Xcode, violations will appear as errors or warnings. For your pull request to be merged, you must meet all the requirements outlined by SwiftLint and have no violations. 12 | 13 | Generally, it is not advised to disable a SwiftLint rule, but there are certain situations where it is necessary. Please use your discretion when disabling rules temporarily. 14 | 15 | SwiftLint does not fully check indentation, but we ask that you indent with 4-width spaces. This can be automatically configured in Xcode's settings. 16 | 17 | All added strings must be properly localised and added to the EN strings file. Do not add keys for other languages or translate within your PR. All translations should be handled on [Crowdin](https://crowdin.com/project/whisky). 18 | 19 | # Making your PR 20 | 21 | Please provide a detailed description of your changes in your PR. If your commits contain UI changes, we ask that you provide screenshots. 22 | 23 | # Review 24 | 25 | Once your pull request passes CI SwiftLint checks and builds, it will be ready for review. You may receive feedback on code that should changed. Once you have received an approval, your code will be merged! 26 | -------------------------------------------------------------------------------- /Whisky/Views/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | 21 | struct AboutView: View { 22 | let version: String = "Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "")) " 23 | var body: some View { 24 | HStack(spacing: 20) { 25 | Image(nsImage: (NSImage(named: "AppIcon") ?? NSImage(data: .init(count: 0)))!) 26 | VStack(alignment: .leading) { 27 | Text("Whisky") 28 | .font(.system(size: 30, weight: .light)) 29 | Text("A modern Wine wrapper for macOS.") 30 | .fontWeight(.bold) 31 | Text(version) 32 | } 33 | } 34 | .padding() 35 | .frame(minWidth: 400, minHeight: 150) 36 | } 37 | } 38 | 39 | #Preview { 40 | AboutView() 41 | } 42 | -------------------------------------------------------------------------------- /Whisky/Views/SparkleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SparkleView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | import Sparkle 21 | 22 | struct SparkleView: View { 23 | @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel 24 | private let updater: SPUUpdater 25 | 26 | init(updater: SPUUpdater) { 27 | self.updater = updater 28 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) 29 | } 30 | 31 | var body: some View { 32 | Button("check.updates", action: updater.checkForUpdates) 33 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 34 | } 35 | } 36 | 37 | // This view model class publishes when new updates can be checked by the user 38 | final class CheckForUpdatesViewModel: ObservableObject { 39 | @Published var canCheckForUpdates = false 40 | 41 | init(updater: SPUUpdater) { 42 | updater.publisher(for: \.canCheckForUpdates) 43 | .assign(to: &$canCheckForUpdates) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /WhiskyKit/Whisky/Bottle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bottle.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | public class Bottle: Hashable, Identifiable { 22 | public static func == (lhs: Bottle, rhs: Bottle) -> Bool { 23 | return lhs.url == rhs.url 24 | } 25 | 26 | public func hash(into hasher: inout Hasher) { 27 | return hasher.combine(url) 28 | } 29 | public var id: URL { 30 | self.url 31 | } 32 | 33 | public var url: URL 34 | public var settings: BottleSettings 35 | public var programs: [Program] = [] 36 | public var inFlight: Bool = false 37 | 38 | public init(bottleUrl: URL, inFlight: Bool = false) { 39 | self.settings = BottleSettings(bottleURL: bottleUrl) 40 | self.url = bottleUrl 41 | self.inFlight = inFlight 42 | } 43 | } 44 | 45 | extension Array where Element == Bottle { 46 | public mutating func sortByName() { 47 | self.sort { $0.settings.name.lowercased() < $1.settings.name.lowercased() } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug. 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Please provide a description of the bug. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: reproduce 15 | attributes: 16 | label: Steps to reproduce 17 | description: Please provide detailed step-by-step instructions on how to reproduce this issue. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: expected-behaviour 22 | attributes: 23 | label: Expected behaviour 24 | description: Please provide a clear a detailed description of what you expected to happen. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: logs 29 | attributes: 30 | label: Logs 31 | description: Please provide the logs from the launched Wine process. 32 | render: bash 33 | validations: 34 | required: true 35 | - type: dropdown 36 | id: whisky-version 37 | attributes: 38 | label: What version of Whisky are you using? 39 | options: 40 | - "2.0.0" 41 | - "<2.0.0" 42 | validations: 43 | required: true 44 | - type: dropdown 45 | id: mac-version 46 | attributes: 47 | label: What version of macOS are you using? 48 | options: 49 | - "Sonoma (macOS 14)" 50 | validations: 51 | required: true 52 | - type: checkboxes 53 | attributes: 54 | label: Issue Language 55 | description: All issues must be written in clear plain English so that all devs are able to read them. 56 | options: 57 | - label: Yes my issue is written in English 58 | required: true 59 | -------------------------------------------------------------------------------- /WhiskyKit/Whisky/Program.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Program.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | public class Program: Hashable { 22 | public static func == (lhs: Program, rhs: Program) -> Bool { 23 | lhs.url == rhs.url 24 | } 25 | 26 | public func hash(into hasher: inout Hasher) { 27 | return hasher.combine(url) 28 | } 29 | 30 | public var name: String 31 | public var url: URL 32 | public var settings: ProgramSettings 33 | public var bottle: Bottle 34 | public var pinned: Bool 35 | public var peFile: PEFile? 36 | 37 | public init(name: String, url: URL, bottle: Bottle) { 38 | self.name = name 39 | self.url = url 40 | self.bottle = bottle 41 | self.settings = ProgramSettings(bottleUrl: bottle.url, name: name) 42 | self.pinned = bottle.settings.pins.contains(where: { $0.url == url }) 43 | do { 44 | self.peFile = try PEFile(data: Data(contentsOf: url)) 45 | } catch { 46 | self.peFile = nil 47 | } 48 | } 49 | 50 | public func generateEnvironment() -> [String: String] { 51 | var environment = settings.environment 52 | if settings.locale != .auto { 53 | environment.updateValue(settings.locale.rawValue, forKey: "LC_ALL") 54 | } 55 | return environment 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Whisky/Views/SetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | 21 | enum SetupStage { 22 | case rosetta 23 | case gptkDownload 24 | case gptkInstall 25 | } 26 | 27 | struct SetupView: View { 28 | @State private var path: [SetupStage] = [] 29 | @State var tarLocation: URL = URL(fileURLWithPath: "") 30 | @Binding var showSetup: Bool 31 | var firstTime: Bool = true 32 | 33 | var body: some View { 34 | VStack { 35 | NavigationStack(path: $path) { 36 | WelcomeView(path: $path, showSetup: $showSetup, firstTime: firstTime) 37 | .navigationBarBackButtonHidden(true) 38 | .navigationDestination(for: SetupStage.self) { stage in 39 | switch stage { 40 | case .rosetta: 41 | RosettaView(path: $path, showSetup: $showSetup) 42 | case .gptkDownload: 43 | GPTKDownloadView(tarLocation: $tarLocation, path: $path) 44 | case .gptkInstall: 45 | GPTKInstallView(tarLocation: $tarLocation, path: $path, showSetup: $showSetup) 46 | } 47 | } 48 | } 49 | } 50 | .padding() 51 | .interactiveDismissDisabled() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /WhiskyKit/Extensions/DataExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataExtensions.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | extension Data { 22 | public func extract(_ type: T.Type, offset: Int = 0) -> T? { 23 | if offset + MemoryLayout.size < self.count { 24 | let data = self[offset...size] 25 | return data.withUnsafeBytes { $0.loadUnaligned(as: T.self) } 26 | } else { 27 | return nil 28 | } 29 | } 30 | 31 | // Thanks ChatGPT 32 | public func nullTerminatedStrings(using encoding: String.Encoding = .utf8) -> [String] { 33 | var strings = [String]() 34 | self.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in 35 | if let baseAddress = ptr.baseAddress { 36 | var strStart = baseAddress 37 | let strEnd = baseAddress + self.count 38 | while strStart < strEnd { 39 | let strPtr = strStart.assumingMemoryBound(to: CChar.self) 40 | let strLen = strnlen(strPtr, self.count) 41 | let strData = Data(bytes: strPtr, count: strLen) 42 | if let str = String(data: strData, encoding: encoding) { 43 | strings.append(str) 44 | } 45 | strStart = strStart.advanced(by: strLen + 1) 46 | } 47 | } 48 | } 49 | return strings 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "alamofire", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Alamofire/Alamofire", 7 | "state" : { 8 | "revision" : "78424be314842833c04bc3bef5b72e85fff99204", 9 | "version" : "5.6.4" 10 | } 11 | }, 12 | { 13 | "identity" : "progress.swift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/jkandzi/Progress.swift", 16 | "state" : { 17 | "revision" : "fed6598735d7982058690acf8f52a0a5fdaeb3e0", 18 | "version" : "0.4.0" 19 | } 20 | }, 21 | { 22 | "identity" : "semanticversion", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/SwiftPackageIndex/SemanticVersion", 25 | "state" : { 26 | "revision" : "a70840d5fca686ae3bd2fcf8aecc5ded0bd4f125", 27 | "version" : "0.3.6" 28 | } 29 | }, 30 | { 31 | "identity" : "sparkle", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/sparkle-project/Sparkle", 34 | "state" : { 35 | "branch" : "2.x", 36 | "revision" : "b7b858dbf385cdd1fe1ab8a3f3ee8586fa850d5d" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-argument-parser", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-argument-parser.git", 43 | "state" : { 44 | "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", 45 | "version" : "1.2.3" 46 | } 47 | }, 48 | { 49 | "identity" : "swiftsoup", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/scinfu/SwiftSoup", 52 | "state" : { 53 | "revision" : "f707b8680cddb96dc1855632340a572ef37bbb98", 54 | "version" : "2.5.3" 55 | } 56 | }, 57 | { 58 | "identity" : "swiftytexttable", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/scottrhoyt/SwiftyTextTable", 61 | "state" : { 62 | "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", 63 | "version" : "0.9.0" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Whisky 🥃 4 | *Wine but a bit stronger* 5 | 6 | ![](https://img.shields.io/github/actions/workflow/status/IsaacMarovitz/Whisky/SwiftLint.yml?style=for-the-badge) 7 | [![](https://img.shields.io/discord/1115955071549702235?style=for-the-badge)](https://discord.gg/CsqAfs9CnM) 8 |
9 | 10 | Screenshot 2023-03-31 at 17 14 00 11 | 12 | Familiar UI that integrates seamlessly with macOS 13 | 14 |
15 | Screenshot 2023-03-31 at 17 14 22 16 | 17 | One-click bottle creation and management 18 |
19 | 20 | debug 21 | 22 | Debug and profile with ease 23 | 24 | --- 25 | 26 | Whisky provides a clean and easy to use graphical wrapper for Wine built in native SwiftUI. You can make and manage bottles, install and run Windows apps and games, and unlock the full potential of your Mac with no technical knowledge required. Whisky is built on top of CrossOver 22.1.1, and Apple's own `Game Porting Toolkit`. 27 | 28 | Special thanks to [Gcenx](https://github.com/Gcenx), without your amazing work Whisky wouldn't be possible. 29 | 30 | Translated on [Crowdin](https://crowdin.com/project/whisky). 31 | 32 | --- 33 | 34 | # System Requirements 35 | - CPU: Apple Silicon (M1/M2 and its variants) 36 | - OS: macOS Sonoma 14.0 or later 37 | 38 | # Homebrew 39 | 40 | Whisky is on homebrew! Install with 41 | `brew install --cask whisky`. 42 | 43 | # FAQ 44 | 45 | ### My game isn't working 46 | 47 | Check the [wiki](https://github.com/IsaacMarovitz/Whisky/wiki/Game-Support). 48 | 49 | --- 50 | 51 | 52 | 53 | 59 | 62 | 63 |
54 | 55 | 56 | 57 | 58 | 60 | Whisky doesn't exist without CrossOver. Support the work of CodeWeavers using our affiliate link. 61 |
64 | -------------------------------------------------------------------------------- /Whisky/Utils/WhiskyCmd.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WhiskyCmd.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import AppKit 21 | 22 | class WhiskyCmd { 23 | static func install() async { 24 | let whiskyCmdURL = Bundle.main.url(forResource: "WhiskyCmd", withExtension: nil) 25 | 26 | if let whiskyCmdURL = whiskyCmdURL { 27 | // swiftlint:disable line_length 28 | let script = """ 29 | do shell script "ln -fs \(whiskyCmdURL.path(percentEncoded: false)) /usr/local/bin/whisky" with administrator privileges 30 | """ 31 | // swiftlint:enable line_length 32 | 33 | var error: NSDictionary? 34 | // Use AppleScript because somehow in 2023 Apple doesn't have good privileged file ops APIs 35 | if let appleScript = NSAppleScript(source: script) { 36 | appleScript.executeAndReturnError(&error) 37 | 38 | if let error = error { 39 | print(error) 40 | if let description = error["NSAppleScriptErrorMessage"] as? String { 41 | await MainActor.run { 42 | let alert = NSAlert() 43 | alert.messageText = String(localized: "alert.message") 44 | alert.informativeText = String(localized: "alert.info") 45 | + description 46 | alert.alertStyle = .critical 47 | alert.addButton(withTitle: String(localized: "button.ok")) 48 | alert.runModal() 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /WhiskyKit/Tar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tar.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | public class Tar { 22 | static let tarBinary: URL = URL(fileURLWithPath: "/usr/bin/tar") 23 | 24 | public static func tar(folder: URL, toURL: URL) throws { 25 | let process = Process() 26 | let pipe = Pipe() 27 | 28 | process.executableURL = tarBinary 29 | process.arguments = ["-zcf", "\(toURL.path)", "\(folder.path)"] 30 | process.standardOutput = pipe 31 | process.standardError = pipe 32 | 33 | try process.run() 34 | 35 | if let output = try pipe.fileHandleForReading.readToEnd() { 36 | let outputString = String(decoding: output, as: UTF8.self) 37 | process.waitUntilExit() 38 | let status = process.terminationStatus 39 | if status != 0 { 40 | throw outputString 41 | } 42 | } 43 | } 44 | 45 | public static func untar(tarBall: URL, toURL: URL) throws { 46 | let process = Process() 47 | let pipe = Pipe() 48 | 49 | process.executableURL = tarBinary 50 | process.arguments = ["-xzf", "\(tarBall.path)", "-C", "\(toURL.path)"] 51 | process.standardOutput = pipe 52 | process.standardError = pipe 53 | 54 | try process.run() 55 | 56 | if let output = try pipe.fileHandleForReading.readToEnd() { 57 | let outputString = String(decoding: output, as: UTF8.self) 58 | process.waitUntilExit() 59 | let status = process.terminationStatus 60 | if status != 0 { 61 | throw outputString 62 | } 63 | } 64 | } 65 | } 66 | 67 | extension String: Error {} 68 | -------------------------------------------------------------------------------- /Whisky/Views/Setup Views/GPTKInstallView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPTKInstallView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | import WhiskyKit 21 | 22 | struct GPTKInstallView: View { 23 | @State var installing: Bool = true 24 | @Binding var tarLocation: URL 25 | @Binding var path: [SetupStage] 26 | @Binding var showSetup: Bool 27 | 28 | var body: some View { 29 | VStack { 30 | VStack { 31 | Text("setup.gptk.install") 32 | .font(.title) 33 | .fontWeight(.bold) 34 | Text("setup.gptk.install.subtitle") 35 | .font(.subheadline) 36 | .foregroundStyle(.secondary) 37 | Spacer() 38 | if installing { 39 | ProgressView() 40 | .progressViewStyle(.circular) 41 | .frame(width: 80) 42 | } else { 43 | Image(systemName: "checkmark.circle") 44 | .resizable() 45 | .frame(width: 80, height: 80) 46 | .foregroundStyle(.green) 47 | } 48 | Spacer() 49 | } 50 | Spacer() 51 | } 52 | .frame(width: 400, height: 200) 53 | .onAppear { 54 | Task.detached { 55 | GPTKInstaller.install(from: tarLocation) 56 | await MainActor.run { 57 | installing = false 58 | } 59 | sleep(2) 60 | await proceed() 61 | } 62 | } 63 | } 64 | 65 | @MainActor 66 | func proceed() { 67 | showSetup = false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Whisky/Utils/Winetricks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Winetricks.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import AppKit 21 | import WhiskyKit 22 | 23 | class Winetricks { 24 | static let winetricksURL: URL = GPTKInstaller.libraryFolder 25 | .appending(path: "winetricks") 26 | 27 | static func runCommand(command: String, bottle: Bottle) async { 28 | // swiftlint:disable:next line_length 29 | let winetricksCmd = #"PATH=\"\#(Wine.binFolder.path):$PATH\" WINE=wine64 WINEPREFIX=\"\#(bottle.url.path)\" \"\#(winetricksURL.path(percentEncoded: false))\" \#(command)"# 30 | 31 | let script = """ 32 | tell application "Terminal" 33 | activate 34 | do script "\(winetricksCmd)" 35 | end tell 36 | """ 37 | 38 | var error: NSDictionary? 39 | if let appleScript = NSAppleScript(source: script) { 40 | appleScript.executeAndReturnError(&error) 41 | 42 | if let error = error { 43 | print(error) 44 | if let description = error["NSAppleScriptErrorMessage"] as? String { 45 | await MainActor.run { 46 | let alert = NSAlert() 47 | alert.messageText = String(localized: "alert.message") 48 | alert.informativeText = String(localized: "alert.info") 49 | + " \(command): " 50 | + description 51 | alert.alertStyle = .critical 52 | alert.addButton(withTitle: String(localized: "button.ok")) 53 | alert.runModal() 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | -------------------------------------------------------------------------------- /Whisky.xcodeproj/xcshareddata/xcschemes/WhiskyKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Whisky/View Models/BottleVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleVM.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import SemanticVersion 21 | import WhiskyKit 22 | 23 | class BottleVM: ObservableObject { 24 | static let shared = BottleVM() 25 | 26 | var bottlesList = BottleData() 27 | @Published var bottles: [Bottle] = [] 28 | 29 | @MainActor 30 | func loadBottles() { 31 | bottles = bottlesList.loadBottles() 32 | } 33 | 34 | func createNewBottle(bottleName: String, winVersion: WinVersion, bottleURL: URL) -> URL { 35 | let newBottleDir = bottleURL.appending(path: UUID().uuidString) 36 | 37 | Task.detached { @MainActor in 38 | var bottleId: Bottle? = .none 39 | do { 40 | try FileManager.default.createDirectory(atPath: newBottleDir.path(percentEncoded: false), 41 | withIntermediateDirectories: true) 42 | let bottle = Bottle(bottleUrl: newBottleDir, inFlight: true) 43 | bottleId = .some(bottle) 44 | 45 | self.bottles.append(bottle) 46 | self.bottles.sortByName() 47 | 48 | bottle.settings.windowsVersion = winVersion 49 | bottle.settings.name = bottleName 50 | try await Wine.changeWinVersion(bottle: bottle, win: winVersion) 51 | let wineVer = try await Wine.wineVersion() 52 | bottle.settings.wineVersion = SemanticVersion(wineVer) ?? SemanticVersion(0, 0, 0) 53 | // Add record 54 | self.bottlesList.paths.append(newBottleDir) 55 | self.loadBottles() 56 | } catch { 57 | print("Failed to create new bottle: \(error)") 58 | if let bottle = bottleId { 59 | if let index = self.bottles.firstIndex(of: bottle) { 60 | self.bottles.remove(at: index) 61 | } 62 | } 63 | } 64 | } 65 | return newBottleDir 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /WhiskyKit/Extensions/URLExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLExtensions.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | extension String { 22 | public var esc: String { 23 | let esc = ["\\", "\"", "'", " ", "(", ")", "[", "]", "{", "}", "&", "|", 24 | ";", "<", ">", "`", "$", "!", "*", "?", "#", "~", "="] 25 | var str = self 26 | for char in esc { 27 | str = str.replacingOccurrences(of: char, with: "\\" + char) 28 | } 29 | return str 30 | } 31 | } 32 | 33 | extension URL { 34 | public var esc: String { 35 | path.esc 36 | } 37 | 38 | public func prettyPath() -> String { 39 | var prettyPath = path(percentEncoded: false) 40 | prettyPath = prettyPath 41 | .replacingOccurrences(of: Bundle.main.bundleIdentifier ?? "com.isaacmarovitz.Whisky", with: "Whisky") 42 | .replacingOccurrences(of: "/Users/\(NSUserName())", with: "~") 43 | return prettyPath 44 | } 45 | 46 | // NOT to be used for logic only as UI decoration 47 | public func prettyPath(_ bottle: Bottle) -> String { 48 | var prettyPath = path(percentEncoded: false) 49 | prettyPath = prettyPath 50 | .replacingOccurrences(of: bottle.url.path(percentEncoded: false), with: "") 51 | .replacingOccurrences(of: "/drive_c/", with: "C:\\") 52 | .replacingOccurrences(of: "/", with: "\\") 53 | return prettyPath 54 | } 55 | 56 | // There is probably a better way to do this 57 | public func updateParentBottle(old: URL, new: URL) -> URL { 58 | let originalPath = path(percentEncoded: false) 59 | 60 | var oldBottlePath = old.path(percentEncoded: false) 61 | if oldBottlePath.last != "/" { 62 | oldBottlePath += "/" 63 | } 64 | 65 | var newBottlePath = new.path(percentEncoded: false) 66 | if newBottlePath.last != "/" { 67 | newBottlePath += "/" 68 | } 69 | 70 | let newPath = originalPath.replacingOccurrences(of: oldBottlePath, 71 | with: newBottlePath) 72 | return URL(filePath: newPath) 73 | } 74 | } 75 | 76 | extension URL: Identifiable { 77 | public var id: URL { self } 78 | } 79 | -------------------------------------------------------------------------------- /Whisky/Views/Setup Views/RosettaView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RosettaView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | import WhiskyKit 21 | 22 | struct RosettaView: View { 23 | @State var installing: Bool = true 24 | @Binding var path: [SetupStage] 25 | @Binding var showSetup: Bool 26 | 27 | var body: some View { 28 | VStack { 29 | VStack { 30 | Text("setup.rosetta") 31 | .font(.title) 32 | .fontWeight(.bold) 33 | Text("setup.rosetta.subtitle") 34 | .font(.subheadline) 35 | .foregroundStyle(.secondary) 36 | Spacer() 37 | Group { 38 | if installing { 39 | ProgressView() 40 | .scaleEffect(2) 41 | } else { 42 | Image(systemName: "checkmark.circle") 43 | .resizable() 44 | .foregroundStyle(.green) 45 | } 46 | } 47 | .frame(width: 80, height: 80) 48 | Spacer() 49 | } 50 | Spacer() 51 | } 52 | .frame(width: 400, height: 200) 53 | .onAppear { 54 | Rosetta2.launchRosettaInstaller() 55 | var runCount = 0 56 | Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in 57 | runCount += 1 58 | 59 | if Rosetta2.isRosettaInstalled { 60 | timer.invalidate() 61 | installing = false 62 | Task.detached { 63 | sleep(2) 64 | await proceed() 65 | } 66 | } 67 | if runCount >= 300 { 68 | // Timer has run for too long 69 | timer.invalidate() 70 | installing = false 71 | Task.detached { 72 | sleep(2) 73 | await proceed() 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | @MainActor 81 | func proceed() { 82 | if !GPTKInstaller.isGPTKInstalled() { 83 | path.append(.gptkDownload) 84 | return 85 | } 86 | 87 | showSetup = false 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Whisky.xcodeproj/xcshareddata/xcschemes/Whisky.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 | -------------------------------------------------------------------------------- /WhiskyThumbnail/ThumbnailProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbnailProvider.swift 3 | // WhiskyThumbnail 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import QuickLookThumbnailing 21 | import AppKit 22 | import WhiskyKit 23 | 24 | class ThumbnailProvider: QLThumbnailProvider { 25 | override func provideThumbnail(for request: QLFileThumbnailRequest, 26 | _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) { 27 | let thumbnailSize = CGSize(width: request.maximumSize.width, 28 | height: request.maximumSize.height) 29 | 30 | // % of thumbnail occupied by icon 31 | let iconScaleFactor = 0.9 32 | let whiskyIconScaleFactor = 0.4 33 | 34 | // AppKit coordinate system origin is in the bottom-left 35 | // Icon is centered 36 | let iconFrame = CGRect(x: (request.maximumSize.width - request.maximumSize.width * iconScaleFactor) / 2.0, 37 | y: (request.maximumSize.height - request.maximumSize.height * iconScaleFactor) / 2.0, 38 | width: request.maximumSize.width * iconScaleFactor, 39 | height: request.maximumSize.height * iconScaleFactor) 40 | 41 | // Whisky icon is aligned bottom-right 42 | let whiskyIconFrame = CGRect(x: request.maximumSize.width - request.maximumSize.width * whiskyIconScaleFactor, 43 | y: 0, 44 | width: request.maximumSize.width * whiskyIconScaleFactor, 45 | height: request.maximumSize.height * whiskyIconScaleFactor) 46 | do { 47 | var image: NSImage? 48 | 49 | let peFile = try PEFile(data: Data(contentsOf: request.fileURL)) 50 | image = peFile.bestIcon() 51 | 52 | let reply: QLThumbnailReply = QLThumbnailReply.init(contextSize: thumbnailSize) { () -> Bool in 53 | if let image = image { 54 | image.draw(in: iconFrame) 55 | let whiskyIcon = NSImage(named: NSImage.Name("Icon")) 56 | whiskyIcon?.draw(in: whiskyIconFrame, from: .zero, operation: .sourceOver, fraction: 1) 57 | return true 58 | } 59 | 60 | // We didn't draw anything 61 | return false 62 | } 63 | 64 | handler(reply, nil) 65 | } catch { 66 | handler(nil, nil) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Whisky/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeIconFile 9 | 10 | CFBundleTypeIconSystemGenerated 11 | 0 12 | CFBundleTypeName 13 | Microsoft MSI Installer 14 | CFBundleTypeRole 15 | Editor 16 | LSHandlerRank 17 | Owner 18 | LSItemContentTypes 19 | 20 | com.microsoft.msi-installer 21 | 22 | 23 | 24 | CFBundleTypeIconFile 25 | 26 | CFBundleTypeIconSystemGenerated 27 | 0 28 | CFBundleTypeName 29 | Microsoft Batch File 30 | CFBundleTypeRole 31 | Editor 32 | LSHandlerRank 33 | Owner 34 | LSItemContentTypes 35 | 36 | com.microsoft.bat 37 | 38 | 39 | 40 | CFBundleTypeIconFile 41 | 42 | CFBundleTypeIconSystemGenerated 43 | 0 44 | CFBundleTypeName 45 | Microsoft Executable 46 | CFBundleTypeRole 47 | Editor 48 | LSHandlerRank 49 | Owner 50 | LSItemContentTypes 51 | 52 | com.microsoft.windows-executable 53 | 54 | 55 | 56 | SUFeedURL 57 | https://raw.githubusercontent.com/IsaacMarovitz/Whisky/sparkle/appcast.xml 58 | SUPublicEDKey 59 | tnZFAvPUfCpM7Tr7Sx5gKRm6BUQQ6htJQeOMP44evms= 60 | UTExportedTypeDeclarations 61 | 62 | 63 | UTTypeConformsTo 64 | 65 | public.data 66 | 67 | UTTypeDescription 68 | Microsoft MSI Installer 69 | UTTypeIconFile 70 | 71 | UTTypeIcons 72 | 73 | UTTypeIdentifier 74 | com.microsoft.msi-installer 75 | UTTypeTagSpecification 76 | 77 | public.filename-extension 78 | 79 | msi 80 | 81 | 82 | 83 | 84 | UTTypeConformsTo 85 | 86 | public.data 87 | 88 | UTTypeDescription 89 | Microsoft Batch File 90 | UTTypeIconFile 91 | 92 | UTTypeIcons 93 | 94 | UTTypeIdentifier 95 | com.microsoft.bat 96 | UTTypeTagSpecification 97 | 98 | public.filename-extension 99 | 100 | bat 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Whisky/Views/FileOpenView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileOpenView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | import WhiskyKit 21 | 22 | struct FileOpenView: View { 23 | var fileURL: URL 24 | var currentBottle: URL? 25 | var bottles: [Bottle] 26 | @State var selection: URL = URL(filePath: "") 27 | @Environment(\.dismiss) var dismiss 28 | 29 | var body: some View { 30 | VStack { 31 | HStack { 32 | Text(String(format: String(localized: "run.title"), 33 | fileURL.lastPathComponent)) 34 | .bold() 35 | Spacer() 36 | } 37 | Divider() 38 | HStack { 39 | Text("run.bottle") 40 | Spacer() 41 | Picker(String(), selection: $selection) { 42 | ForEach(bottles, id: \.self) { 43 | Text($0.settings.name) 44 | .tag($0.url) 45 | } 46 | } 47 | .pickerStyle(.menu) 48 | .frame(width: 200) 49 | } 50 | Spacer() 51 | HStack { 52 | Spacer() 53 | Button("create.cancel") { 54 | dismiss() 55 | } 56 | .keyboardShortcut(.cancelAction) 57 | Button("button.run") { 58 | if let bottle = bottles.first(where: { $0.url == selection}) { 59 | Task.detached(priority: .userInitiated) { 60 | do { 61 | if fileURL.pathExtension == "bat" { 62 | try await Wine.runBatchFile(url: fileURL, 63 | bottle: bottle) 64 | } else { 65 | try await Wine.runExternalProgram(url: fileURL, 66 | bottle: bottle) 67 | } 68 | } catch { 69 | print(error) 70 | } 71 | } 72 | dismiss() 73 | } 74 | } 75 | .keyboardShortcut(.defaultAction) 76 | } 77 | } 78 | .padding() 79 | .frame(width: 400, height: 180) 80 | .onAppear { 81 | selection = bottles.first(where: {$0.url == currentBottle})?.url ?? bottles[0].url 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /WhiskyKit/GPTK/GPTKDownloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPTKDownloader.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | public class GPTKDownloader { 22 | public static func getLatestGPTKURL() async -> DownloadInfo? { 23 | let githubURL = "https://api.github.com/repos/IsaacMarovitz/WhiskyBuilder/actions/artifacts" 24 | if let artifactsURL = URL(string: githubURL) { 25 | return await withCheckedContinuation { continuation in 26 | URLSession.shared.dataTask(with: URLRequest(url: artifactsURL)) { data, _, error in 27 | do { 28 | if error == nil, let data = data { 29 | let decoder = JSONDecoder() 30 | decoder.keyDecodingStrategy = .convertFromSnakeCase 31 | let artifacts: Artifacts = try decoder.decode(Artifacts.self, from: data) 32 | // We gotta turn the URL into a nightly.link URL 33 | if let latest = artifacts.artifacts.first { 34 | var url = latest.url 35 | url.replace("https://api.github.com/repos/", with: "https://nightly.link/") 36 | let selection = "actions/" 37 | if let range = url.range(of: selection) { 38 | url = String(url[.. 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 66 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /WhiskyKit/Whisky/BottleData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleData.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import SemanticVersion 21 | 22 | public struct BottleData: Codable { 23 | public static let containerDir = FileManager.default.homeDirectoryForCurrentUser 24 | .appending(path: "Library") 25 | .appending(path: "Containers") 26 | .appending(path: Bundle.main.bundleIdentifier ?? "com.isaacmarovitz.Whisky") 27 | 28 | public static let bottleEntriesDir = containerDir 29 | .appending(path: "BottleVM") 30 | .appendingPathExtension("plist") 31 | 32 | public static let defaultBottleDir = containerDir 33 | .appending(path: "Bottles") 34 | 35 | static let currentVersion = SemanticVersion(1, 0, 0) 36 | 37 | private var fileVersion: SemanticVersion 38 | public var paths: [URL] = [] { 39 | didSet { 40 | encode() 41 | } 42 | } 43 | 44 | public init() { 45 | fileVersion = Self.currentVersion 46 | 47 | if !decode() { 48 | encode() 49 | } 50 | } 51 | 52 | public mutating func loadBottles() -> [Bottle] { 53 | var bottles: [Bottle] = [] 54 | 55 | for path in paths { 56 | let bottleMetadata = path 57 | .appending(path: "Metadata") 58 | .appendingPathExtension("plist") 59 | .path(percentEncoded: false) 60 | 61 | if FileManager.default.fileExists(atPath: bottleMetadata) { 62 | bottles.append(Bottle(bottleUrl: path)) 63 | } else { 64 | paths.removeAll(where: { $0 == path }) 65 | } 66 | } 67 | 68 | bottles.sortByName() 69 | return bottles 70 | } 71 | 72 | @discardableResult 73 | private mutating func decode() -> Bool { 74 | let decoder = PropertyListDecoder() 75 | do { 76 | let data = try Data(contentsOf: Self.bottleEntriesDir) 77 | self = try decoder.decode(BottleData.self, from: data) 78 | if self.fileVersion != Self.currentVersion { 79 | print("Invalid file version \(self.fileVersion)") 80 | return false 81 | } 82 | return true 83 | } catch { 84 | return false 85 | } 86 | } 87 | 88 | @discardableResult 89 | private func encode() -> Bool { 90 | let encoder = PropertyListEncoder() 91 | encoder.outputFormat = .xml 92 | 93 | do { 94 | let data = try encoder.encode(self) 95 | try data.write(to: Self.bottleEntriesDir) 96 | return true 97 | } catch { 98 | return false 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Whisky/Utils/ProgramShortcut.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgramShortcut.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import AppKit 21 | import QuickLookThumbnailing 22 | import WhiskyKit 23 | 24 | class ProgramShortcut { 25 | public static func createShortcut(_ program: Program, app: URL, name: String) async { 26 | let contents = app.appending(path: "Contents") 27 | let macos = contents.appending(path: "MacOS") 28 | do { 29 | try FileManager.default.createDirectory(at: macos, withIntermediateDirectories: true) 30 | 31 | // First create shell script 32 | let script = """ 33 | #!/bin/bash 34 | \(program.generateTerminalCommand()) 35 | """ 36 | let scriptUrl = macos.appending(path: "launch") 37 | try script.write(to: scriptUrl, 38 | atomically: false, 39 | encoding: .utf8) 40 | 41 | // Make shell script runable 42 | try FileManager.default.setAttributes([.posixPermissions: 0o777], 43 | ofItemAtPath: scriptUrl.path(percentEncoded: false)) 44 | 45 | // Create Info.plist (set category for Game mode) 46 | let info = """ 47 | 48 | 49 | 50 | 51 | CFBundleExecutable 52 | launch 53 | CFBundleSupportedPlatforms 54 | 55 | MacOSX 56 | 57 | LSMinimumSystemVersion 58 | 14.0 59 | LSApplicationCategoryType 60 | public.app-category.games 61 | 62 | 63 | """ 64 | try info.write(to: contents.appending(path: "Info") 65 | .appendingPathExtension("plist"), 66 | atomically: false, 67 | encoding: .utf8) 68 | 69 | // Set bundle icon 70 | let request = QLThumbnailGenerator.Request(fileAt: program.url, 71 | size: CGSize(width: 512, height: 512), 72 | scale: 2.0, 73 | representationTypes: .thumbnail) 74 | let thumbnail = try await QLThumbnailGenerator.shared.generateBestRepresentation(for: request) 75 | NSWorkspace.shared.setIcon(thumbnail.nsImage, 76 | forFile: app.path(percentEncoded: false), 77 | options: NSWorkspace.IconCreationOptions()) 78 | NSWorkspace.shared.activateFileViewerSelecting([app]) 79 | } catch { 80 | print(error) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Whisky.xcodeproj/xcshareddata/xcschemes/WhiskyThumbnail.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 58 | 60 | 66 | 67 | 68 | 69 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Whisky/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import SwiftUI 21 | 22 | class AppDelegate: NSObject, NSApplicationDelegate { 23 | @AppStorage("hasShownMoveToApplicationsAlert") private var hasShownMoveToApplicationsAlert = false 24 | 25 | func applicationDidFinishLaunching(_ notification: Notification) { 26 | if !hasShownMoveToApplicationsAlert && !AppDelegate.insideAppsFolder { 27 | DispatchQueue.main.asyncAfter(deadline: .now()) { 28 | NSApp.activate(ignoringOtherApps: true) 29 | self.showAlertOnFirstLaunch() 30 | self.hasShownMoveToApplicationsAlert = true 31 | } 32 | } 33 | } 34 | 35 | func applicationWillTerminate(_ notification: Notification) { 36 | WhiskyApp.killBottles() 37 | } 38 | 39 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 40 | return true 41 | } 42 | 43 | private static var appUrl: URL? { 44 | Bundle.main.resourceURL?.deletingLastPathComponent().deletingLastPathComponent() 45 | } 46 | 47 | private static var expectedUrl = URL(fileURLWithPath: "/Applications/Whisky.app") 48 | 49 | private static var insideAppsFolder: Bool { 50 | if let url = appUrl { 51 | return url.path.contains("Xcode") || url.path.contains(expectedUrl.path) 52 | } 53 | return false 54 | } 55 | 56 | private func showAlertOnFirstLaunch() { 57 | let alert = NSAlert() 58 | alert.messageText = String(localized: "showAlertOnFirstLaunch.messageText") 59 | alert.informativeText = String(localized: "showAlertOnFirstLaunch.informativeText") 60 | alert.addButton(withTitle: String(localized: "showAlertOnFirstLaunch.button.moveToApplications")) 61 | alert.addButton(withTitle: String(localized: "showAlertOnFirstLaunch.button.dontMove")) 62 | 63 | let response = alert.runModal() 64 | 65 | if response == .alertFirstButtonReturn { 66 | let appURL = Bundle.main.bundleURL 67 | 68 | do { 69 | _ = try FileManager.default.replaceItemAt(AppDelegate.expectedUrl, withItemAt: appURL) 70 | NSWorkspace.shared.open(AppDelegate.expectedUrl) 71 | } catch { 72 | print("Failed to move the app: \(error)") 73 | } 74 | } 75 | } 76 | 77 | private var aboutBoxWindowController: NSWindowController? 78 | 79 | func showAboutPanel() { 80 | if aboutBoxWindowController == nil { 81 | let styleMask: NSWindow.StyleMask = [.closable, .titled] 82 | let window = NSWindow() 83 | window.styleMask = styleMask 84 | window.title = "About" 85 | window.titlebarAppearsTransparent = true 86 | window.titleVisibility = .hidden 87 | window.contentView = NSHostingView(rootView: AboutView()) 88 | aboutBoxWindowController = NSWindowController(window: window) 89 | } 90 | 91 | aboutBoxWindowController?.showWindow(aboutBoxWindowController?.window) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Whisky/Views/BottleRenameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleRenameView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | import WhiskyKit 21 | 22 | struct BottleRenameView: View { 23 | let bottle: Bottle 24 | @State var newBottleName: String = "" 25 | @State var nameValid: Bool = false 26 | @Binding var name: String 27 | @Environment(\.dismiss) var dismiss 28 | 29 | var body: some View { 30 | VStack { 31 | HStack { 32 | Text("rename.bottle.title") 33 | .bold() 34 | Spacer() 35 | } 36 | Divider() 37 | HStack(alignment: .top) { 38 | Text("rename.name") 39 | Spacer() 40 | TextField(String(), text: $newBottleName) 41 | .frame(width: 180) 42 | .onChange(of: newBottleName) { _, name in 43 | nameValid = !name.isEmpty 44 | } 45 | } 46 | Spacer() 47 | HStack { 48 | Spacer() 49 | Button("create.cancel") { 50 | dismiss() 51 | } 52 | .keyboardShortcut(.cancelAction) 53 | Button("rename.rename") { 54 | name = newBottleName 55 | bottle.rename(newName: newBottleName) 56 | dismiss() 57 | } 58 | .keyboardShortcut(.defaultAction) 59 | .disabled(!nameValid) 60 | } 61 | } 62 | .padding() 63 | .frame(width: 350, height: 150) 64 | .onAppear { 65 | newBottleName = bottle.settings.name 66 | } 67 | } 68 | } 69 | 70 | struct PinRenameView: View { 71 | @State var newBottleName: String = "" 72 | @State var nameValid: Bool = false 73 | @Binding var name: String 74 | @Environment(\.dismiss) var dismiss 75 | 76 | var body: some View { 77 | VStack { 78 | HStack { 79 | Text("rename.pin.title") 80 | .bold() 81 | Spacer() 82 | } 83 | Divider() 84 | HStack(alignment: .top) { 85 | Text("rename.name") 86 | Spacer() 87 | TextField(String(), text: $newBottleName) 88 | .frame(width: 180) 89 | .onChange(of: newBottleName) { _, name in 90 | nameValid = !name.isEmpty 91 | } 92 | } 93 | Spacer() 94 | HStack { 95 | Spacer() 96 | Button("create.cancel") { 97 | dismiss() 98 | } 99 | .keyboardShortcut(.cancelAction) 100 | Button("rename.rename") { 101 | name = newBottleName 102 | dismiss() 103 | } 104 | .keyboardShortcut(.defaultAction) 105 | .disabled(!nameValid) 106 | } 107 | } 108 | .padding() 109 | .frame(width: 350, height: 150) 110 | .onAppear { 111 | newBottleName = name 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Whisky/Models/Program.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Program.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import AppKit 21 | import WhiskyKit 22 | 23 | extension Program { 24 | func run() async { 25 | if NSEvent.modifierFlags.contains(.shift) { 26 | print("Running in terminal...") 27 | await runInTerminal() 28 | } else { 29 | await runInWine() 30 | } 31 | } 32 | 33 | func runInWine() async { 34 | do { 35 | try await Wine.runProgram(program: self) 36 | } catch { 37 | await MainActor.run { 38 | let alert = NSAlert() 39 | alert.messageText = String(localized: "alert.message") 40 | alert.informativeText = String(localized: "alert.info") 41 | + " \(url.lastPathComponent): " 42 | + error.localizedDescription 43 | alert.alertStyle = .critical 44 | alert.addButton(withTitle: String(localized: "button.ok")) 45 | alert.runModal() 46 | } 47 | } 48 | } 49 | 50 | func generateTerminalCommand() -> String { 51 | var wineCmd = "\(Wine.wineBinary.esc) start /unix \(url.esc) \(settings.arguments)" 52 | 53 | let env = Wine.constructEnvironment(bottle: bottle, 54 | programEnv: generateEnvironment()) 55 | for environment in env { 56 | wineCmd = "\(environment.key)=\(environment.value) " + wineCmd 57 | } 58 | 59 | return wineCmd 60 | } 61 | 62 | func runInTerminal() async { 63 | let wineCmd = generateTerminalCommand().replacingOccurrences(of: "\\", with: "\\\\") 64 | 65 | let script = """ 66 | tell application "Terminal" 67 | activate 68 | do script "\(wineCmd)" 69 | end tell 70 | """ 71 | 72 | var error: NSDictionary? 73 | if let appleScript = NSAppleScript(source: script) { 74 | appleScript.executeAndReturnError(&error) 75 | 76 | if let error = error { 77 | print(error) 78 | if let description = error["NSAppleScriptErrorMessage"] as? String { 79 | await MainActor.run { 80 | let alert = NSAlert() 81 | alert.messageText = String(localized: "alert.message") 82 | alert.informativeText = String(localized: "alert.info") 83 | + " \(url.lastPathComponent): " 84 | + description 85 | alert.alertStyle = .critical 86 | alert.addButton(withTitle: String(localized: "button.ok")) 87 | alert.runModal() 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | func togglePinned() -> Bool { 95 | if pinned { 96 | bottle.settings.pins.removeAll(where: { $0.url == url }) 97 | pinned = false 98 | } else { 99 | bottle.settings.pins.append(PinnedProgram(name: name 100 | .replacingOccurrences(of: ".exe", with: ""), 101 | url: url)) 102 | pinned = true 103 | } 104 | 105 | return pinned 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Whisky/Utils/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import OSLog 21 | import WhiskyKit 22 | 23 | class Log { 24 | static let logsFolder = FileManager.default.urls(for: .libraryDirectory, 25 | in: .userDomainMask)[0] 26 | .appending(path: "Logs") 27 | .appending(path: Bundle.main.bundleIdentifier ?? "com.isaacmarovitz.Whisky") 28 | 29 | let fileHandle: FileHandle 30 | let logger: Logger 31 | 32 | init?(bottle: Bottle?, args: [String], environment: [String: String]?) { 33 | do { 34 | if !FileManager.default.fileExists(atPath: Log.logsFolder.path) { 35 | try FileManager.default.createDirectory(at: Log.logsFolder, withIntermediateDirectories: true) 36 | } 37 | 38 | let dateString = Date.now.ISO8601Format() 39 | let fileURL = Log.logsFolder 40 | .appending(path: dateString) 41 | .appendingPathExtension("log") 42 | 43 | try "".write(to: fileURL, atomically: true, encoding: .utf8) 44 | 45 | fileHandle = try FileHandle(forWritingTo: fileURL) 46 | 47 | if let bundleID = Bundle.main.bundleIdentifier { 48 | logger = Logger(subsystem: bundleID, category: "wine") 49 | } else { 50 | throw "Could not get Bundle ID!" 51 | } 52 | 53 | write(line: Log.constructHeader(bottle, args, environment), printLine: false) 54 | } catch { 55 | print("Failed to create logger: \(error)") 56 | return nil 57 | } 58 | } 59 | 60 | static func constructHeader(_ bottle: Bottle?, _ args: [String], _ environment: [String: String]?) -> String { 61 | var header = String() 62 | 63 | header += "Whisky Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "")\n" 64 | header += "Date: \(Date.now.formatted(date: .numeric, time: .standard))\n" 65 | header += "macOS Version: \(ProcessInfo.processInfo.operatingSystemVersionString)\n" 66 | if let bottle = bottle { 67 | header += "Bottle Name: \(bottle.settings.name)\n" 68 | header += "Wine Version: \(bottle.settings.wineVersion)\n" 69 | header += "Windows Version: \(bottle.settings.windowsVersion)\n" 70 | header += "Bottle URL: \(bottle.url.path)\n\n" 71 | } 72 | 73 | header += "Arguments: " 74 | for arg in args { 75 | header += "\(arg) " 76 | } 77 | header += "\n\n" 78 | 79 | if let environment = environment { 80 | if environment.count > 0 { 81 | header += "Environment:\n" 82 | header += "\(environment as AnyObject)\n\n" 83 | } 84 | } 85 | 86 | return header 87 | } 88 | 89 | func write(line: String, printLine: Bool = true) { 90 | if printLine { 91 | logger.log("\(line, privacy: .public)") 92 | } 93 | 94 | if let data = line.data(using: .utf8) { 95 | do { 96 | try fileHandle.write(contentsOf: data) 97 | } catch { 98 | print("Failed to write line to log: \"\(line)\"!") 99 | } 100 | } 101 | } 102 | 103 | deinit { 104 | do { 105 | try fileHandle.close() 106 | } catch { 107 | print("Failed to close log file handle!") 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Whisky/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "从创建第一个容器开始"; 5 | 6 | "button.cDrive" = "Open C: Drive"; 7 | "button.showInFinder" = "在访达中显示"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "运行..."; 10 | "button.ok" = "确定"; 11 | "button.rename" = "Rename..."; 12 | "button.moveBottle" = "Move..."; 13 | "button.exportBottle" = "Export as Archive..."; 14 | "button.removeAlert" = "删除..."; 15 | "button.createBottle" = "创建容器"; 16 | "button.removeAlert.msg" = "删除 %@?"; 17 | "button.removeAlert.info" = "你确定要删除这个容器吗?这将是不可逆的。"; 18 | "button.removeAlert.checkbox" = "从磁盘中删除文件"; 19 | "button.removeAlert.delete" = "删除"; 20 | "button.removeAlert.cancel" = "取消"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows 版本"; 26 | "config.buildVersion" = "构建版本"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal 性能分析"; 30 | "config.retinaMode" = "Retina 模式"; 31 | "config.metalTrace.info" = "允许你在 Xcode 中抓取 GPU 工作负载"; 32 | "config.esync" = "ESync(增强同步)"; 33 | "config.controlPanel" = "打开控制面板"; 34 | "config.regedit" = "打开注册表编辑器"; 35 | "config.winecfg" = "打开 Wine 配置"; 36 | "config.notAvailable" = "不适用"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "全部"; 41 | "config.dxvkHud.partial" = "部分"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "关闭"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI 缩放"; 46 | "config.inspect" = "配置..."; 47 | 48 | "configDpi.title" = "DPI 设置"; 49 | "configDpi.preview" = "文字外观示例:"; 50 | "configDpi.previewText" = "敏捷的棕色狐狸跳过懒狗。"; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "All Programs"; 54 | "program.blocklist" = "Blocklist"; 55 | "program.env" = "环境变量"; 56 | "program.args" = "参数"; 57 | "program.config" = "配置"; 58 | "program.add.blocklist" = "Add to Blocklist"; 59 | "program.remove.blocklist" = "Remove from Blocklist"; 60 | 61 | "alert.message" = "打开软件失败!"; 62 | "alert.info" = "打开失败"; 63 | 64 | "create.title" = "创建新的容器"; 65 | "create.name" = "容器名称:"; 66 | "create.win" = "Windows 版本:"; 67 | "create.path" = "容器路径:"; 68 | "create.browse" = "浏览"; 69 | "create.cancel" = "取消"; 70 | "create.create" = "创建"; 71 | 72 | "rename.bottle.title" = "Rename bottle"; 73 | "rename.pin.title" = "Rename pin"; 74 | "rename.name" = "新名称:"; 75 | "rename.rename" = "重命名"; 76 | 77 | "check.updates" = "检查更新..."; 78 | "kill.bottles" = "停止所有容器"; 79 | "open.logs" = "打开日志文件夹"; 80 | "open.bottle" = "导入容器"; 81 | "open.setup" = "安装..."; 82 | "install.cli" = "安装 Whisky CLI..."; 83 | "wine.clearShaderCaches" = "清空 Shader 缓存"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "你想要将 Whisky 移动到你的应用程序文件夹吗?"; 86 | "showAlertOnFirstLaunch.informativeText" = "在某些情况下,Whisky 可能无法在其他的文件夹中正常工作"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "移动到应用程序文件夹"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "不要移动"; 89 | 90 | "setup.welcome" = "欢迎使用 Whisky"; 91 | "setup.welcome.subtitle" = "让我们帮您安装好,这不会用很长时间。"; 92 | 93 | "setup.title" = "Dependencies Setup"; 94 | "setup.subtitle" = "Manage Whisky's required dependencies."; 95 | 96 | "setup.install.checking" = "正在检查 %@ 是否安装..."; 97 | "setup.install.installed" = "%@ 已安装"; 98 | "setup.install.notInstalled" = "%@ 未安装"; 99 | "setup.uninstall" = "卸载"; 100 | 101 | "setup.rosetta" = "正在安装 Rosetta"; 102 | "setup.rosetta.subtitle" = "Rosetta 让您的 Mac 可以运行 Wine 等 x86 架构的程序。"; 103 | 104 | "setup.gptk.download" = "正在下载 GPTK"; 105 | "setup.gptk.download.subtitle" = "下载速度受您的网络质量影响。"; 106 | "setup.gptk.progress" = "%2$@ 中的 %1$@"; 107 | "setup.gptk.eta" = "- 剩余 %@"; 108 | 109 | "setup.gptk.install" = "正在安装 GPTK"; 110 | "setup.gptk.install.subtitle" = "就快完成了,请不要离开。"; 111 | 112 | "setup.quit" = "退出"; 113 | "setup.done" = "完成"; 114 | "setup.next" = "下一页"; 115 | 116 | "update.gptk.title" = "可用的 GPTK 新版本"; 117 | "update.gptk.description" = "您正在运行 GPTK %@,但 %@ 可用。您想要更新吗?"; 118 | "update.gptk.update" = "更新"; 119 | 120 | "winetricks.title" = "运行 Winetricks 命令"; 121 | "button.winetricks" = "Winetrick..."; 122 | 123 | "run.title" = "运行 \"%@\""; 124 | "run.bottle" = "瓶子:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/ko.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle 설정"; 2 | "tab.programs" = "설치된 프로그램"; 3 | 4 | "main.createFirst" = "첫 번째 Bottle을 만들어 시작하기"; 5 | 6 | "button.cDrive" = "C: 드라이브 열기"; 7 | "button.showInFinder" = "Finder에서 보기"; 8 | "button.createShortcut" = "바로 가기 만들기"; 9 | "button.run" = "실행..."; 10 | "button.ok" = "확인"; 11 | "button.rename" = "이름 변경..."; 12 | "button.moveBottle" = "이동..."; 13 | "button.exportBottle" = "아카이브로 내보내기..."; 14 | "button.removeAlert" = "삭제..."; 15 | "button.createBottle" = "Bottle 생성"; 16 | "button.removeAlert.msg" = "%@을(를) 삭제하시겠습니까?"; 17 | "button.removeAlert.info" = "정말로 이 Bottle을 삭제하시겠습니까? 이 행동은 되돌릴 수 없습니다."; 18 | "button.removeAlert.checkbox" = "디스크에서 파일 삭제"; 19 | "button.removeAlert.delete" = "삭제"; 20 | "button.removeAlert.cancel" = "취소"; 21 | 22 | "pin.unpin" = "프로그램 고정 해제"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows 버전"; 26 | "config.buildVersion" = "빌드 버전"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal Trace"; 30 | "config.retinaMode" = "Retina 모드"; 31 | "config.metalTrace.info" = "Xcode에서 GPU 워크로드를 확인할 수 있습니다."; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "제어판 열기"; 34 | "config.regedit" = "레지스트리 편집기 열기"; 35 | "config.winecfg" = "Wine 설정 열기"; 36 | "config.notAvailable" = "사용 불가"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "전체"; 41 | "config.dxvkHud.partial" = "일부"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "끄기"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI 스케일링"; 46 | "config.inspect" = "구성..."; 47 | 48 | "configDpi.title" = "DPI 설정"; 49 | "configDpi.preview" = "텍스트는 다음과 같이 표시될 것입니다:"; 50 | "configDpi.previewText" = "다람쥐 헌 쳇바퀴에 타고파."; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "모든 프로그램"; 54 | "program.blocklist" = "차단 목록"; 55 | "program.env" = "환경 변수"; 56 | "program.args" = "인자"; 57 | "program.config" = "설정"; 58 | "program.add.blocklist" = "차단 목록에 추가"; 59 | "program.remove.blocklist" = "차단 목록에서 삭제"; 60 | 61 | "alert.message" = "프로그램을 여는 데 실패했습니다!"; 62 | "alert.info" = "열기 실패"; 63 | 64 | "create.title" = "새로운 Bottle 생성"; 65 | "create.name" = "Bottle 이름:"; 66 | "create.win" = "Windows 버전:"; 67 | "create.path" = "Bottle 경로:"; 68 | "create.browse" = "탐색"; 69 | "create.cancel" = "취소"; 70 | "create.create" = "생성"; 71 | 72 | "rename.bottle.title" = "Bottle 이름 변경"; 73 | "rename.pin.title" = "핀 이름 변경"; 74 | "rename.name" = "새 이름:"; 75 | "rename.rename" = "이름 변경"; 76 | 77 | "check.updates" = "업데이트 확인..."; 78 | "kill.bottles" = "모든 Bottle 종료하기"; 79 | "open.logs" = "로그 폴더 열기"; 80 | "open.bottle" = "Bottle 불러오기"; 81 | "open.setup" = "설치..."; 82 | "install.cli" = "Whisky CLI 설치..."; 83 | "wine.clearShaderCaches" = "셰이더 캐시 초기화"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Whisky를 애플리케이션 폴더로 옮기시겠습니까?"; 86 | "showAlertOnFirstLaunch.informativeText" = "경우에 따라 다른 폴더에서는 Whisky가 제대로 동작하지 않을 수 있습니다."; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "애플리케이션 폴더로 옮기기"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "옮기지 않기"; 89 | 90 | "setup.welcome" = "Whisky에 오신 것을 환영합니다"; 91 | "setup.welcome.subtitle" = "설정하는 것을 도와드리겠습니다. 얼마 걸리지 않을 거예요."; 92 | 93 | "setup.title" = "의존성 설정"; 94 | "setup.subtitle" = "Whisky의 필수 의존성 관리하기."; 95 | 96 | "setup.install.checking" = "%@ 설치를 확인하는 중입니다..."; 97 | "setup.install.installed" = "%@ 설치됨"; 98 | "setup.install.notInstalled" = "%@ 설치되지 않음"; 99 | "setup.uninstall" = "제거"; 100 | 101 | "setup.rosetta" = "Rosetta를 설치 중입니다."; 102 | "setup.rosetta.subtitle" = "Rosetta는 Wine과 같은 x86 코드를 당신의 Mac에서 실행할 수 있게 해 줍니다."; 103 | 104 | "setup.gptk.download" = "GPTK를 다운로드 중입니다."; 105 | "setup.gptk.download.subtitle" = "속도는 인터넷 연결 상태에 따라 달라질 수 있습니다."; 106 | "setup.gptk.progress" = "%2$@ 중 %1$@"; 107 | "setup.gptk.eta" = "- %@ 남음"; 108 | 109 | "setup.gptk.install" = "GPTK 설치 중"; 110 | "setup.gptk.install.subtitle" = "거의 다 왔어요. 아직 끄지 마세요."; 111 | 112 | "setup.quit" = "종료"; 113 | "setup.done" = "완료"; 114 | "setup.next" = "다음"; 115 | 116 | "update.gptk.title" = "새로운 버전의 GPTK를 사용할 수 있습니다"; 117 | "update.gptk.description" = "GPTK %@ 버전을 사용 중이지만 %@ 버전을 사용할 수 있습니다. 업데이트하시겠습니까?"; 118 | "update.gptk.update" = "업데이트"; 119 | 120 | "winetricks.title" = "Winetricks 명령어 실행"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "\"%@\" 실행"; 124 | "run.bottle" = "Bottle:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "建立你的第一個容器以開始"; 5 | 6 | "button.cDrive" = "開啟 C: 槽"; 7 | "button.showInFinder" = "在 Finder 中顯示"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "執行..."; 10 | "button.ok" = "確定"; 11 | "button.rename" = "重新命名..."; 12 | "button.moveBottle" = "移動..."; 13 | "button.exportBottle" = "匯出為封存..."; 14 | "button.removeAlert" = "Remove..."; 15 | "button.createBottle" = "建立容器"; 16 | "button.removeAlert.msg" = "Remove %@?"; 17 | "button.removeAlert.info" = "Are you sure you want to remove this bottle? This action is permanent."; 18 | "button.removeAlert.checkbox" = "Delete files from disk"; 19 | "button.removeAlert.delete" = "Remove"; 20 | "button.removeAlert.cancel" = "Cancel"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows 版本"; 26 | "config.buildVersion" = "(由洋甘菊翻譯提供翻譯) 27 | 組建版本"; 28 | "config.title.metal" = "Metal"; 29 | "config.metalHud" = "Metal 抬頭顯示"; 30 | "config.metalTrace" = "Metal 追蹤"; 31 | "config.retinaMode" = "Retina 模式"; 32 | "config.metalTrace.info" = "允許你在 Xcode 中獲取 GPU 工作負載"; 33 | "config.esync" = "增強同步 (ESync)"; 34 | "config.controlPanel" = "開啟控制台"; 35 | "config.regedit" = "開啟註冊表編輯器"; 36 | "config.winecfg" = "開啟Wine設定"; 37 | "config.notAvailable" = "不適用"; 38 | "config.title.dxvk" = "DXVK"; 39 | "config.dxvk" = "DXVK"; 40 | "config.dxvkHud" = "DXVK HUD"; 41 | "config.dxvkHud.full" = "全開"; 42 | "config.dxvkHud.partial" = "部份"; 43 | "config.dxvkHud.fps" = "幀數"; 44 | "config.dxvkHud.off" = "關閉"; 45 | "config.dxvk.async" = "DXVK Async"; 46 | "config.dpi" = "DPI 縮放"; 47 | "config.inspect" = "設定..."; 48 | 49 | "configDpi.title" = "DPI 設定"; 50 | "configDpi.preview" = "文本外觀範例:"; 51 | "configDpi.previewText" = "敏捷的棕色狐狸跳過了懶狗。"; 52 | "configDpi.dpi" = "DPI"; 53 | 54 | "program.title" = "All Programs"; 55 | "program.blocklist" = "Blocklist"; 56 | "program.env" = "環境變數"; 57 | "program.args" = "命令列引數"; 58 | "program.config" = "設定"; 59 | "program.add.blocklist" = "Add to Blocklist"; 60 | "program.remove.blocklist" = "Remove from Blocklist"; 61 | 62 | "alert.message" = "無法開啟程式!"; 63 | "alert.info" = "開啟失敗"; 64 | 65 | "create.title" = "建立一個新的容器"; 66 | "create.name" = "容器名稱:"; 67 | "create.win" = "Windows 版本:"; 68 | "create.path" = "容器路徑:"; 69 | "create.browse" = "瀏覽"; 70 | "create.cancel" = "取消"; 71 | "create.create" = "建立"; 72 | 73 | "rename.bottle.title" = "重新命名容器"; 74 | "rename.pin.title" = "重新命名 Pin"; 75 | "rename.name" = "新名稱:"; 76 | "rename.rename" = "重新命名"; 77 | 78 | "check.updates" = "檢查更新..."; 79 | "kill.bottles" = "終止所有容器"; 80 | "open.logs" = "開啟日誌資料夾"; 81 | "open.bottle" = "导入容器"; 82 | "open.setup" = "設置..."; 83 | "install.cli" = "Install Whisky CLI..."; 84 | "wine.clearShaderCaches" = "Clear Shader Caches"; 85 | 86 | "showAlertOnFirstLaunch.messageText" = "你希望將 Whisky 移動到你的應用程式資料夾嗎?"; 87 | "showAlertOnFirstLaunch.informativeText" = "在某些情況下,Whisky 可能無法在不同的資料夾中正常運作"; 88 | "showAlertOnFirstLaunch.button.moveToApplications" = "移動到應用程式"; 89 | "showAlertOnFirstLaunch.button.dontMove" = "不移動"; 90 | 91 | "setup.welcome" = "歡迎使用 Whisky"; 92 | "setup.welcome.subtitle" = "讓我們為你完成設置。此步驟不會花上一分鐘時間。"; 93 | 94 | "setup.title" = "Dependencies Setup"; 95 | "setup.subtitle" = "Manage Whisky's required dependencies."; 96 | 97 | "setup.install.checking" = "正在檢查 %@ 安裝…"; 98 | "setup.install.installed" = "%@ 已安裝"; 99 | "setup.install.notInstalled" = "%@ 未安裝"; 100 | "setup.uninstall" = "解除安裝"; 101 | 102 | "setup.rosetta" = "正在安裝 Rosetta"; 103 | "setup.rosetta.subtitle" = "Rosetta 允許 x86 代碼(如 Wine)在 Mac 上運行。"; 104 | 105 | "setup.gptk.download" = "正在下載 GPTK"; 106 | "setup.gptk.download.subtitle" = "速度會因你的互聯網連接而有所不同。"; 107 | "setup.gptk.progress" = "%2$@ 中的 %1$@"; 108 | "setup.gptk.eta" = "- 剩餘%@"; 109 | 110 | "setup.gptk.install" = "正在安裝 GPTK"; 111 | "setup.gptk.install.subtitle" = "快完成了。請不要退出。"; 112 | 113 | "setup.quit" = "Quit"; 114 | "setup.done" = "Done"; 115 | "setup.next" = "Next"; 116 | 117 | "update.gptk.title" = "New Version of GPTK Available"; 118 | "update.gptk.description" = "You are running GPTK %@, but %@ is available. Would you like to update?"; 119 | "update.gptk.update" = "Update"; 120 | 121 | "winetricks.title" = "Run Winetricks command"; 122 | "button.winetricks" = "Winetricks..."; 123 | 124 | "run.title" = "執行「%@」"; 125 | "run.bottle" = "容器:"; 126 | 127 | "locale.title" = "Locale"; 128 | "locale.auto" = "Automatic"; 129 | -------------------------------------------------------------------------------- /Whisky/Views/BottleCreationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottleCreationView.swift 3 | // Whisky 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import SwiftUI 20 | import WhiskyKit 21 | 22 | struct BottleCreationView: View { 23 | @State var newBottleName: String = "" 24 | @State var newBottleVersion: WinVersion = .win10 25 | @State var newBottleURL: URL = BottleData.defaultBottleDir 26 | @State var bottlePath: String = "" 27 | @State var nameValid: Bool = false 28 | @Binding var newlyCreatedBottleURL: URL? 29 | @Environment(\.dismiss) var dismiss 30 | 31 | var body: some View { 32 | VStack { 33 | HStack { 34 | Text("create.title") 35 | .bold() 36 | Spacer() 37 | } 38 | Divider() 39 | HStack(alignment: .top) { 40 | Text("create.name") 41 | Spacer() 42 | TextField(String(), text: $newBottleName) 43 | .frame(width: 180) 44 | .onChange(of: newBottleName) { _, name in 45 | nameValid = !name.isEmpty 46 | } 47 | } 48 | HStack { 49 | Text("create.win") 50 | Spacer() 51 | Picker(String(), selection: $newBottleVersion) { 52 | ForEach(WinVersion.allCases.reversed(), id: \.self) { 53 | Text($0.pretty()) 54 | } 55 | } 56 | .labelsHidden() 57 | .frame(width: 180) 58 | } 59 | HStack { 60 | Text("create.path") 61 | Spacer() 62 | Text(bottlePath) 63 | .truncationMode(.middle) 64 | Button("create.browse") { 65 | let panel = NSOpenPanel() 66 | panel.canChooseFiles = false 67 | panel.canChooseDirectories = true 68 | panel.allowsMultipleSelection = false 69 | panel.canCreateDirectories = true 70 | panel.directoryURL = BottleData.containerDir 71 | panel.begin { result in 72 | if result == .OK { 73 | if let url = panel.urls.first { 74 | newBottleURL = url 75 | } 76 | } 77 | } 78 | } 79 | } 80 | Spacer() 81 | HStack { 82 | Spacer() 83 | Button("create.cancel") { 84 | dismiss() 85 | } 86 | .keyboardShortcut(.cancelAction) 87 | Button("create.create") { 88 | newlyCreatedBottleURL = BottleVM.shared.createNewBottle(bottleName: newBottleName, 89 | winVersion: newBottleVersion, 90 | bottleURL: newBottleURL) 91 | dismiss() 92 | } 93 | .keyboardShortcut(.defaultAction) 94 | .disabled(!nameValid) 95 | } 96 | } 97 | .padding() 98 | .onChange(of: newBottleURL) { 99 | bottlePath = newBottleURL.prettyPath() 100 | } 101 | .onAppear { 102 | bottlePath = newBottleURL.prettyPath() 103 | } 104 | .frame(width: 400, height: 180) 105 | } 106 | } 107 | 108 | struct BottleCreationView_Previews: PreviewProvider { 109 | @State private static var newlyCreatedBottleURL: URL? 110 | static var previews: some View { 111 | BottleCreationView(newlyCreatedBottleURL: $newlyCreatedBottleURL) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Whisky/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "ボトルの設定"; 2 | "tab.programs" = "インストール済みのプログラム"; 3 | 4 | "main.createFirst" = "最初の Bottle を作って、使い始めましょう"; 5 | 6 | "button.cDrive" = "C: ドライブを開く"; 7 | "button.showInFinder" = "Finder で表示"; 8 | "button.createShortcut" = "ショートカットを作成"; 9 | "button.run" = "実行..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "名前の変更..."; 12 | "button.moveBottle" = "移動..."; 13 | "button.exportBottle" = "アーカイブとしてエクスポート..."; 14 | "button.removeAlert" = "削除..."; 15 | "button.createBottle" = "Bottle を作成"; 16 | "button.removeAlert.msg" = "%@ を削除しますか?"; 17 | "button.removeAlert.info" = "この Bottle を削除してもよろしいですか? この操作は取り消せません。"; 18 | "button.removeAlert.checkbox" = "ディスクからも削除"; 19 | "button.removeAlert.delete" = "削除"; 20 | "button.removeAlert.cancel" = "キャンセル"; 21 | 22 | "pin.unpin" = "ピン留めを解除"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows のバージョン"; 26 | "config.buildVersion" = "ビルド番号"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal トレース"; 30 | "config.retinaMode" = "Retina モード"; 31 | "config.metalTrace.info" = "Xcode で GPU ワークロードをキャプチャできるようにします"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "コントロール パネルを開く"; 34 | "config.regedit" = "レジストリ エディターを開く"; 35 | "config.winecfg" = "Wine 設定を開く"; 36 | "config.notAvailable" = "N/A"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK を使用する"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "完全"; 41 | "config.dxvkHud.partial" = "一部"; 42 | "config.dxvkHud.fps" = "FPS のみ"; 43 | "config.dxvkHud.off" = "オフ"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI スケーリング"; 46 | "config.inspect" = "設定..."; 47 | 48 | "configDpi.title" = "DPI 設定"; 49 | "configDpi.preview" = "テキストはこのように見えます:"; 50 | "configDpi.previewText" = "色は匂へど 散りぬるを 我が世誰ぞ 常ならむ 有為の奥山 今日越えて 浅き夢見じ 酔ひもせず。"; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "すべてのプログラム"; 54 | "program.blocklist" = "Blocklist"; 55 | "program.env" = "環境変数"; 56 | "program.args" = "引数"; 57 | "program.config" = "設定"; 58 | "program.add.blocklist" = "Add to Blocklist"; 59 | "program.remove.blocklist" = "Remove from Blocklist"; 60 | 61 | "alert.message" = "プログラムの起動に失敗しました!"; 62 | "alert.info" = "起動に失敗しました"; 63 | 64 | "create.title" = "新しい Bottle を作成"; 65 | "create.name" = "Bottle 名:"; 66 | "create.win" = "Windows バージョン:"; 67 | "create.path" = "Bottle の場所:"; 68 | "create.browse" = "変更..."; 69 | "create.cancel" = "キャンセル"; 70 | "create.create" = "作成"; 71 | 72 | "rename.bottle.title" = "Bottle の名前を変更"; 73 | "rename.pin.title" = "ピン留めの名前を変更"; 74 | "rename.name" = "新しい名前:"; 75 | "rename.rename" = "変更"; 76 | 77 | "check.updates" = "更新を確認..."; 78 | "kill.bottles" = "すべての Bottle を終了"; 79 | "open.logs" = "ログフォルダを開く"; 80 | "open.bottle" = "Bottle をインポート"; 81 | "open.setup" = "セットアップ..."; 82 | "install.cli" = "Whisky CLI をインストール..."; 83 | "wine.clearShaderCaches" = "シェーダーのキャッシュを消去"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Whisky を「アプリケーション」フォルダに移動しますか?"; 86 | "showAlertOnFirstLaunch.informativeText" = "場合によっては、異なるフォルダから Whisky を適切に使用できない場合があります"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "「アプリケーション」に移動"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "移動しない"; 89 | 90 | "setup.welcome" = "Whisky へようこそ"; 91 | "setup.welcome.subtitle" = "セットアップをしましょう。1分以内に終わります。"; 92 | 93 | "setup.title" = "依存関係のセットアップ"; 94 | "setup.subtitle" = "Whisky に必要な依存関係を管理します。"; 95 | 96 | "setup.install.checking" = "%@ のインストールを確認しています..."; 97 | "setup.install.installed" = "%@ がインストールされました"; 98 | "setup.install.notInstalled" = "%@ が未インストール"; 99 | "setup.uninstall" = "アンインストール"; 100 | 101 | "setup.rosetta" = "Rosetta をインストール中"; 102 | "setup.rosetta.subtitle" = "Rosetta は、Wine のような x86 バイナリを Mac 上で実行できるようにします。"; 103 | 104 | "setup.gptk.download" = "GPTK をダウンロード中"; 105 | "setup.gptk.download.subtitle" = "速度はインターネット接続によって異なります。"; 106 | "setup.gptk.progress" = "%@ / %@"; 107 | "setup.gptk.eta" = "- 残り %@"; 108 | 109 | "setup.gptk.install" = "GPTK をインストール中"; 110 | "setup.gptk.install.subtitle" = "あと少しです。そのままお待ち下さい。"; 111 | 112 | "setup.quit" = "終了"; 113 | "setup.done" = "完了"; 114 | "setup.next" = "次へ"; 115 | 116 | "update.gptk.title" = "GPTK の新しいバージョンがあります"; 117 | "update.gptk.description" = "現在 GPTK %@ を実行していますが、 %@ を利用可能です。アップデートしますか?"; 118 | "update.gptk.update" = "アップデート"; 119 | 120 | "winetricks.title" = "Winetricks コマンドを実行"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "\"%@\" を実行"; 124 | "run.bottle" = "Bottle:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/vi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "Tạo bottle đầu tiên để bắt đầu"; 5 | 6 | "button.cDrive" = "Open C: Drive"; 7 | "button.showInFinder" = "Mở trong Finder"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "Chạy..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "Rename..."; 12 | "button.moveBottle" = "Move..."; 13 | "button.exportBottle" = "Export as Archive..."; 14 | "button.removeAlert" = "Remove..."; 15 | "button.createBottle" = "Tạo Bottle"; 16 | "button.removeAlert.msg" = "Remove %@?"; 17 | "button.removeAlert.info" = "Are you sure you want to remove this bottle? This action is permanent."; 18 | "button.removeAlert.checkbox" = "Delete files from disk"; 19 | "button.removeAlert.delete" = "Remove"; 20 | "button.removeAlert.cancel" = "Cancel"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Phiên bản Windows"; 26 | "config.buildVersion" = "Bản dựng"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Trace Metal"; 30 | "config.retinaMode" = "Chế độ Retina"; 31 | "config.metalTrace.info" = "Cho phép lấy thông tin GPU trong Xcode"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "Mở Control Panel"; 34 | "config.regedit" = "Mở Regedit"; 35 | "config.winecfg" = "Mở cấu hình Wine"; 36 | "config.notAvailable" = "Không có sẵn"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "HUD DXVK"; 40 | "config.dxvkHud.full" = "Đầy đủ"; 41 | "config.dxvkHud.partial" = "Một phần"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "Tắt"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI"; 46 | "config.inspect" = "Cấu hình..."; 47 | 48 | "configDpi.title" = "Cài đặt DPI"; 49 | "configDpi.preview" = "Chữ sẽ trông như thế này:"; 50 | "configDpi.previewText" = "The quick brown fox jumps over the lazy dog."; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "All Programs"; 54 | "program.blocklist" = "Blocklist"; 55 | "program.env" = "Biến môi trường"; 56 | "program.args" = "Thông số"; 57 | "program.config" = "Cấu hình"; 58 | "program.add.blocklist" = "Add to Blocklist"; 59 | "program.remove.blocklist" = "Remove from Blocklist"; 60 | 61 | "alert.message" = "Không thể mở chương trình này!"; 62 | "alert.info" = "Không thể mỏ"; 63 | 64 | "create.title" = "Tạo bottle mới"; 65 | "create.name" = "Tên Bottle:"; 66 | "create.win" = "Phiển bản Windows:"; 67 | "create.path" = "Đường dẫn bottle:"; 68 | "create.browse" = "Duyệt"; 69 | "create.cancel" = "Huỷ"; 70 | "create.create" = "Tạo"; 71 | 72 | "rename.bottle.title" = "Rename bottle"; 73 | "rename.pin.title" = "Rename pin"; 74 | "rename.name" = "Tên mới:"; 75 | "rename.rename" = "Đổi tên"; 76 | 77 | "check.updates" = "Kiểm tra Cập nhật..."; 78 | "kill.bottles" = "Dừng tất cả Bottle"; 79 | "open.logs" = "Mở Folder Log"; 80 | "open.bottle" = "Nhập Bottle"; 81 | "open.setup" = "Thiết lập..."; 82 | "install.cli" = "Install Whisky CLI..."; 83 | "wine.clearShaderCaches" = "Xoá cache shader"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Bạn có muốn chuyển Whisky tới folder Applications?"; 86 | "showAlertOnFirstLaunch.informativeText" = "Trong một số trường hợp, Whisky sẽ không hoạt động đúng nếu không nằm trong Applications"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "Chuyển tới Applications"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "Không Chuyển"; 89 | 90 | "setup.welcome" = "Chào mừng đến với Whisky"; 91 | "setup.welcome.subtitle" = "Let's get you set up. This won't take a minute."; 92 | 93 | "setup.title" = "Dependencies Setup"; 94 | "setup.subtitle" = "Manage Whisky's required dependencies."; 95 | 96 | "setup.install.checking" = "Đang kiểm tra cài đặt %@..."; 97 | "setup.install.installed" = "%@ đã được cài đặt"; 98 | "setup.install.notInstalled" = "%@ chưa được cài đặt"; 99 | "setup.uninstall" = "Gỡ cài đặt"; 100 | 101 | "setup.rosetta" = "Đang cài đặt Rosetta"; 102 | "setup.rosetta.subtitle" = "Rosetta cho phép các ứng dụng x86 như Wine chạy trên máy Mac của bạn."; 103 | 104 | "setup.gptk.download" = "Đang tải xuống Wine"; 105 | "setup.gptk.download.subtitle" = "Tốc độ tải xuống sẽ phụ thuộc vào kết nối Internet của bạn."; 106 | "setup.gptk.progress" = "%@ của %@"; 107 | "setup.gptk.eta" = "- %@ còn lại"; 108 | 109 | "setup.gptk.install" = "Đang cài đặt Wine"; 110 | "setup.gptk.install.subtitle" = "Cài đặt sắp hoàn tất."; 111 | 112 | "setup.quit" = "Quit"; 113 | "setup.done" = "Done"; 114 | "setup.next" = "Next"; 115 | 116 | "update.gptk.title" = "New Version of GPTK Available"; 117 | "update.gptk.description" = "You are running GPTK %@, but %@ is available. Would you like to update?"; 118 | "update.gptk.update" = "Update"; 119 | 120 | "winetricks.title" = "Chạy lệnh Winetricks"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "Run \"%@\""; 124 | "run.bottle" = "Bottle:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "Create your first bottle to get started"; 5 | 6 | "button.cDrive" = "Open C: Drive"; 7 | "button.showInFinder" = "Show in Finder"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "Run..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "Rename..."; 12 | "button.moveBottle" = "Move..."; 13 | "button.exportBottle" = "Export as Archive..."; 14 | "button.removeAlert" = "Remove..."; 15 | "button.createBottle" = "Create Bottle"; 16 | "button.removeAlert.msg" = "Remove %@?"; 17 | "button.removeAlert.info" = "Are you sure you want to remove this bottle? This action is permanent."; 18 | "button.removeAlert.checkbox" = "Delete files from disk"; 19 | "button.removeAlert.delete" = "Remove"; 20 | "button.removeAlert.cancel" = "Cancel"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows Version"; 26 | "config.buildVersion" = "Build Version"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal Trace"; 30 | "config.retinaMode" = "Retina Mode"; 31 | "config.metalTrace.info" = "Allows you to capture GPU workload in Xcode"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "Open Control Panel"; 34 | "config.regedit" = "Open Registry Editor"; 35 | "config.winecfg" = "Open Wine Configuration"; 36 | "config.notAvailable" = "N/A"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "Full"; 41 | "config.dxvkHud.partial" = "Partial"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "Off"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dxvk.info" = "Enable this if you encounter the \"Unsupported Environment\" error."; 46 | "config.dpi" = "DPI Scaling"; 47 | "config.inspect" = "Configure..."; 48 | 49 | "configDpi.title" = "DPI Settings"; 50 | "configDpi.preview" = "Text will look like this:"; 51 | "configDpi.previewText" = "The quick brown fox jumps over the lazy dog."; 52 | "configDpi.dpi" = "DPI"; 53 | 54 | "program.title" = "All Programs"; 55 | "program.blocklist" = "Blocklist"; 56 | "program.env" = "Environment Variables"; 57 | "program.args" = "Arguments"; 58 | "program.config" = "Config"; 59 | "program.add.blocklist" = "Add to Blocklist"; 60 | "program.remove.blocklist" = "Remove from Blocklist"; 61 | 62 | "alert.message" = "Failed to open program!"; 63 | "alert.info" = "Failed to open"; 64 | 65 | "create.title" = "Create a new bottle"; 66 | "create.name" = "Bottle name:"; 67 | "create.win" = "Windows version:"; 68 | "create.path" = "Bottle path:"; 69 | "create.browse" = "Browse"; 70 | "create.cancel" = "Cancel"; 71 | "create.create" = "Create"; 72 | 73 | "rename.bottle.title" = "Rename bottle"; 74 | "rename.pin.title" = "Rename pin"; 75 | "rename.name" = "New name:"; 76 | "rename.rename" = "Rename"; 77 | 78 | "check.updates" = "Check for Updates..."; 79 | "kill.bottles" = "Kill All Bottles"; 80 | "open.logs" = "Open Logs Folder"; 81 | "open.bottle" = "Import Bottle"; 82 | "open.setup" = "Setup..."; 83 | "install.cli" = "Install Whisky CLI..."; 84 | "wine.clearShaderCaches" = "Clear Shader Caches"; 85 | 86 | "showAlertOnFirstLaunch.messageText" = "Would you like to move Whisky to your Applications folder?"; 87 | "showAlertOnFirstLaunch.informativeText" = "In some cases, Whisky wont't be able to function properly from a different folder"; 88 | "showAlertOnFirstLaunch.button.moveToApplications" = "Move to Applications"; 89 | "showAlertOnFirstLaunch.button.dontMove" = "Don't Move"; 90 | 91 | "setup.welcome" = "Welcome to Whisky"; 92 | "setup.welcome.subtitle" = "Let's get you set up. This won't take a minute."; 93 | 94 | "setup.title" = "Dependencies Setup"; 95 | "setup.subtitle" = "Manage Whisky's required dependencies."; 96 | 97 | "setup.install.checking" = "Checking %@ installation..."; 98 | "setup.install.installed" = "%@ installed"; 99 | "setup.install.notInstalled" = "%@ not installed"; 100 | "setup.uninstall" = "Uninstall"; 101 | 102 | "setup.rosetta" = "Installing Rosetta"; 103 | "setup.rosetta.subtitle" = "Rosetta allows x86 code, like Wine, to run on your Mac."; 104 | 105 | "setup.gptk.download" = "Downloading GPTK"; 106 | "setup.gptk.download.subtitle" = "Speeds will vary on your internet connection."; 107 | "setup.gptk.progress" = "%@ of %@"; 108 | "setup.gptk.eta" = "- %@ remaining"; 109 | 110 | "setup.gptk.install" = "Installing GPTK"; 111 | "setup.gptk.install.subtitle" = "Almost there. Don't tune out yet."; 112 | 113 | "setup.quit" = "Quit"; 114 | "setup.done" = "Done"; 115 | "setup.next" = "Next"; 116 | 117 | "update.gptk.title" = "New Version of GPTK Available"; 118 | "update.gptk.description" = "You are running GPTK %@, but %@ is available. Would you like to update?"; 119 | "update.gptk.update" = "Update"; 120 | 121 | "winetricks.title" = "Run Winetricks command"; 122 | "button.winetricks" = "Winetricks..."; 123 | 124 | "run.title" = "Run \"%@\""; 125 | "run.bottle" = "Bottle:"; 126 | 127 | "locale.title" = "Locale"; 128 | "locale.auto" = "Automatic"; 129 | -------------------------------------------------------------------------------- /WhiskyKit/Whisky/ProgramSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgramSettings.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | 21 | public enum Locales: String, Codable, CaseIterable { 22 | case auto = "" 23 | case german = "de_DE.UTF-8" 24 | case english = "en_US" 25 | case spanish = "es_ES.UTF-8" 26 | case french = "fr_FR.UTF-8" 27 | case italian = "it_IT.UTF-8" 28 | case japanese = "ja_JP.UTF-8" 29 | case korean = "ko_KR.UTF-8" 30 | case russian = "ru_RU.UTF-8" 31 | case ukranian = "uk_UA.UTF-8" 32 | case thai = "th_TH.UTF-8" 33 | case chineseSimplified = "zh_CN.UTF-8" 34 | case chineseTraditional = "zh_TW.UTF-8" 35 | 36 | // swiftlint:disable:next cyclomatic_complexity 37 | public func pretty() -> String { 38 | switch self { 39 | case .auto: 40 | return String(localized: "locale.auto") 41 | case .german: 42 | return "Deutsch" 43 | case .english: 44 | return "English" 45 | case .spanish: 46 | return "Español" 47 | case .french: 48 | return "Français" 49 | case .italian: 50 | return "Italiano" 51 | case .japanese: 52 | return "日本語" 53 | case .korean: 54 | return "한국어" 55 | case .russian: 56 | return "Русский" 57 | case .ukranian: 58 | return "Українська" 59 | case .thai: 60 | return "ไทย" 61 | case .chineseSimplified: 62 | return "简体中文" 63 | case .chineseTraditional: 64 | return "繁體中文" 65 | } 66 | } 67 | } 68 | 69 | public struct ProgramSettingsData: Codable { 70 | var locale: Locales = .auto 71 | var environment: [String: String] = [:] 72 | var arguments: String = "" 73 | } 74 | 75 | public class ProgramSettings { 76 | public var settings: ProgramSettingsData { 77 | didSet { 78 | encode() 79 | } 80 | } 81 | 82 | public var locale: Locales { 83 | get { 84 | return settings.locale 85 | } 86 | set { 87 | settings.locale = newValue 88 | } 89 | } 90 | 91 | public var environment: [String: String] { 92 | get { 93 | return settings.environment 94 | } 95 | set { 96 | settings.environment = newValue 97 | } 98 | } 99 | 100 | public var arguments: String { 101 | get { 102 | return settings.arguments 103 | } 104 | set { 105 | settings.arguments = newValue 106 | } 107 | } 108 | 109 | let settingsUrl: URL 110 | 111 | init(bottleUrl: URL, name: String) { 112 | let settingsFolder = bottleUrl.appending(path: "Program Settings") 113 | self.settingsUrl = settingsFolder 114 | .appending(path: name) 115 | .appendingPathExtension("plist") 116 | 117 | if !FileManager.default.fileExists(atPath: settingsFolder.path(percentEncoded: false)) { 118 | do { 119 | try FileManager.default.createDirectory(at: settingsFolder, withIntermediateDirectories: true) 120 | } catch { 121 | print(error) 122 | } 123 | } 124 | 125 | settings = ProgramSettingsData() 126 | if !decode() { 127 | encode() 128 | } 129 | 130 | // Dirty 'fix' for Steam with DXVK 131 | if name.contains("steam") { 132 | environment["WINEDLLOVERRIDES"] = "dxgi,d3d9,d3d10core,d3d11=b" 133 | } 134 | } 135 | 136 | @discardableResult 137 | private func decode() -> Bool { 138 | do { 139 | let data = try Data(contentsOf: settingsUrl) 140 | settings = try PropertyListDecoder().decode(ProgramSettingsData.self, from: data) 141 | return true 142 | } catch { 143 | return false 144 | } 145 | } 146 | 147 | @discardableResult 148 | private func encode() -> Bool { 149 | let encoder = PropertyListEncoder() 150 | encoder.outputFormat = .xml 151 | 152 | do { 153 | let data = try encoder.encode(settings) 154 | try data.write(to: settingsUrl) 155 | return true 156 | } catch { 157 | return false 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Whisky/nl.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "Maak je eerste fles om te beginnen"; 5 | 6 | "button.cDrive" = "Open C: Drive"; 7 | "button.showInFinder" = "Toon in Finder"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "Starten..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "Rename..."; 12 | "button.moveBottle" = "Move..."; 13 | "button.exportBottle" = "Export as Archive..."; 14 | "button.removeAlert" = "Remove..."; 15 | "button.createBottle" = "Fles maken"; 16 | "button.removeAlert.msg" = "Remove %@?"; 17 | "button.removeAlert.info" = "Are you sure you want to remove this bottle? This action is permanent."; 18 | "button.removeAlert.checkbox" = "Delete files from disk"; 19 | "button.removeAlert.delete" = "Remove"; 20 | "button.removeAlert.cancel" = "Cancel"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows-versie"; 26 | "config.buildVersion" = "Build-versie"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal Trace"; 30 | "config.retinaMode" = "Retina Modus"; 31 | "config.metalTrace.info" = "Hiermee kunt u GPU-workload vastleggen in Xcode"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "Open controlepaneel"; 34 | "config.regedit" = "Open Register Editor"; 35 | "config.winecfg" = "Open Wine Configuratie"; 36 | "config.notAvailable" = "N/A"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "Volledig"; 41 | "config.dxvkHud.partial" = "Gedeeltelijk"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "Uit"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI Schalen"; 46 | "config.inspect" = "Configureer..."; 47 | 48 | "configDpi.title" = "DPI-instellingen"; 49 | "configDpi.preview" = "Tekst ziet er als volgt uit:"; 50 | "configDpi.previewText" = "Pa’s wijze lynx bezag vroom het fikse aquaduct."; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "All Programs"; 54 | "program.blocklist" = "Blocklist"; 55 | "program.env" = "Omgevingsvariabelen"; 56 | "program.args" = "Argumenten"; 57 | "program.config" = "Configuratie"; 58 | "program.add.blocklist" = "Add to Blocklist"; 59 | "program.remove.blocklist" = "Remove from Blocklist"; 60 | 61 | "alert.message" = "Kan het programma niet openen!"; 62 | "alert.info" = "Niet geslaagd te openen"; 63 | 64 | "create.title" = "Maak een nieuwe fles"; 65 | "create.name" = "Fles naam:"; 66 | "create.win" = "Windows versie:"; 67 | "create.path" = "Fles pad:"; 68 | "create.browse" = "Zoek"; 69 | "create.cancel" = "Annuleren"; 70 | "create.create" = "Aanmaken"; 71 | 72 | "rename.bottle.title" = "Rename bottle"; 73 | "rename.pin.title" = "Rename pin"; 74 | "rename.name" = "Nieuwe naam:"; 75 | "rename.rename" = "Hernoem"; 76 | 77 | "check.updates" = "Controleer op updates..."; 78 | "kill.bottles" = "Stop alle flessen"; 79 | "open.logs" = "Open Logs map"; 80 | "open.bottle" = "Importeer Fles"; 81 | "open.setup" = "Installatie..."; 82 | "install.cli" = "Install Whisky CLI..."; 83 | "wine.clearShaderCaches" = "Verwijder shader caches"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Wilt u Whiskey verplaatsen naar uw map Toepassingen?"; 86 | "showAlertOnFirstLaunch.informativeText" = "In sommige gevallen zal Whiskey niet goed kunnen functioneren vanuit een andere map"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "Ga naar Toepassingen"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "Niet verplaatsen"; 89 | 90 | "setup.welcome" = "Welkom bij Whisky"; 91 | "setup.welcome.subtitle" = "Laten we ervoor zorgen dat alles klaarstaat. Dit duurt een ogenblik."; 92 | 93 | "setup.title" = "Dependencies Setup"; 94 | "setup.subtitle" = "Manage Whisky's required dependencies."; 95 | 96 | "setup.install.checking" = "De %@ installatie controleren..."; 97 | "setup.install.installed" = "%@ geïnstalleerd"; 98 | "setup.install.notInstalled" = "%@ niet geïnstalleerd"; 99 | "setup.uninstall" = "Deïnstalleren"; 100 | 101 | "setup.rosetta" = "Rosetta installeren"; 102 | "setup.rosetta.subtitle" = "Rosetta laat x86 code, zoals Wine, uitgevoerd worden op je Mac."; 103 | 104 | "setup.gptk.download" = "Wine downloaden"; 105 | "setup.gptk.download.subtitle" = "Snelheden zullen variëren gebaseerd op je internet connectie."; 106 | "setup.gptk.progress" = "%@ van de %@"; 107 | "setup.gptk.eta" = "- %@ resterend"; 108 | 109 | "setup.gptk.install" = "Wine installeren"; 110 | "setup.gptk.install.subtitle" = "Bijna daar. Nog niet weg gaan."; 111 | 112 | "setup.quit" = "Quit"; 113 | "setup.done" = "Done"; 114 | "setup.next" = "Next"; 115 | 116 | "update.gptk.title" = "New Version of GPTK Available"; 117 | "update.gptk.description" = "You are running GPTK %@, but %@ is available. Would you like to update?"; 118 | "update.gptk.update" = "Update"; 119 | 120 | "winetricks.title" = "Voer Winetricks commando uit"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "Run \"%@\""; 124 | "run.bottle" = "Bottle:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/da.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "Opret din første bottle for at komme igang"; 5 | 6 | "button.cDrive" = "Open C: Drive"; 7 | "button.showInFinder" = "Vis i Finder"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "Kør program..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "Rename..."; 12 | "button.moveBottle" = "Move..."; 13 | "button.exportBottle" = "Export as Archive..."; 14 | "button.removeAlert" = "Fjern..."; 15 | "button.createBottle" = "Opret Bottle"; 16 | "button.removeAlert.msg" = "Fjern %@?"; 17 | "button.removeAlert.info" = "Er du sikker på, at du vil fjerne denne flaske? Denne handling er permanent."; 18 | "button.removeAlert.checkbox" = "Slet filer fra disk"; 19 | "button.removeAlert.delete" = "Fjern"; 20 | "button.removeAlert.cancel" = "Annuller"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows Version"; 26 | "config.buildVersion" = "Build Version"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal Trace"; 30 | "config.retinaMode" = "Retina Tilstand"; 31 | "config.metalTrace.info" = "Giver dig mulighed for at fange GPU-workload i Xcode"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "Åbn Kontrolpanel"; 34 | "config.regedit" = "Åbn Registry Editor"; 35 | "config.winecfg" = "Åben Wine Konfiguration"; 36 | "config.notAvailable" = "Ikke tilgængelig"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "Fuld"; 41 | "config.dxvkHud.partial" = "Delvis"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "Fra"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI Skalering"; 46 | "config.inspect" = "Konfigurer..."; 47 | 48 | "configDpi.title" = "DPI Indstillinger"; 49 | "configDpi.preview" = "Teksten vil se sådan ud:"; 50 | "configDpi.previewText" = "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "All Programs"; 54 | "program.blocklist" = "Blocklist"; 55 | "program.env" = "Miljøvariabler"; 56 | "program.args" = "Argumenter"; 57 | "program.config" = "Konfiguration"; 58 | "program.add.blocklist" = "Add to Blocklist"; 59 | "program.remove.blocklist" = "Remove from Blocklist"; 60 | 61 | "alert.message" = "Programmet kunne ikke åbnes!"; 62 | "alert.info" = "Kunne ikke åbne"; 63 | 64 | "create.title" = "Opret en ny bottle"; 65 | "create.name" = "Bottle navn:"; 66 | "create.win" = "Windows version:"; 67 | "create.path" = "Bottle sti:"; 68 | "create.browse" = "Gennemse"; 69 | "create.cancel" = "Annuller"; 70 | "create.create" = "Opret"; 71 | 72 | "rename.bottle.title" = "Rename bottle"; 73 | "rename.pin.title" = "Rename pin"; 74 | "rename.name" = "Nyt navn:"; 75 | "rename.rename" = "Omdøb"; 76 | 77 | "check.updates" = "Tjek for Opdateringer..."; 78 | "kill.bottles" = "Dræb Alle Bottles"; 79 | "open.logs" = "Åbn Logmappe"; 80 | "open.bottle" = "Importér Bottle"; 81 | "open.setup" = "Opsætning..."; 82 | "install.cli" = "Installer Whisky CLI..."; 83 | "wine.clearShaderCaches" = "Ryd Shader Caches"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Vil du flytte Whisky til Applications mappen?"; 86 | "showAlertOnFirstLaunch.informativeText" = "I nogle tilfælde vil Whisky ikke være i stand til at fungere korrekt fra en anden mappe"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "Flyt til mappen Applications"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "Flyt Ikke"; 89 | 90 | "setup.welcome" = "Velkommen til Whisky"; 91 | "setup.welcome.subtitle" = "Lad os få dig sat op. Dette vil ikke tage et minut."; 92 | 93 | "setup.title" = "Opsætning af afhængigheder"; 94 | "setup.subtitle" = "Administrer Whisky's nødvendige afhængigheder."; 95 | 96 | "setup.install.checking" = "Tjekker %@ Installation..."; 97 | "setup.install.installed" = "%@ installeret"; 98 | "setup.install.notInstalled" = "%@ ikke installeret"; 99 | "setup.uninstall" = "Afinstaller"; 100 | 101 | "setup.rosetta" = "Installere Rosetta"; 102 | "setup.rosetta.subtitle" = "Rosetta tillader x86 kode, som Wine, til at køre på din Mac."; 103 | 104 | "setup.gptk.download" = "Downloader Wine"; 105 | "setup.gptk.download.subtitle" = "Hastigheden vil variere udfra din internet hastighed."; 106 | "setup.gptk.progress" = "%@ af %@"; 107 | "setup.gptk.eta" = "- %@ tilbage"; 108 | 109 | "setup.gptk.install" = "Installere Wine"; 110 | "setup.gptk.install.subtitle" = "Er der næsten. Luk ikke programmet endnu."; 111 | 112 | "setup.quit" = "Afslut"; 113 | "setup.done" = "Udført"; 114 | "setup.next" = "Næste"; 115 | 116 | "update.gptk.title" = "Ny version af GPTK tilgængelig"; 117 | "update.gptk.description" = "Du kører GPTK %@, men %@ er tilgængelig. Ønsker du at opdatere?"; 118 | "update.gptk.update" = "Opdater"; 119 | 120 | "winetricks.title" = "Kør kommandoen Winetricks"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "Run \"%@\""; 124 | "run.bottle" = "Bottle:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/pt-PT.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Bottle Configuration"; 2 | "tab.programs" = "Installed Programs"; 3 | 4 | "main.createFirst" = "Crie a sua primeira bottle para começar"; 5 | 6 | "button.cDrive" = "Open C: Drive"; 7 | "button.showInFinder" = "Abrir no Finder"; 8 | "button.createShortcut" = "Create Shortcut"; 9 | "button.run" = "Executar..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "Rename..."; 12 | "button.moveBottle" = "Move..."; 13 | "button.exportBottle" = "Export as Archive..."; 14 | "button.removeAlert" = "Remove..."; 15 | "button.createBottle" = "Criar uma Bottle"; 16 | "button.removeAlert.msg" = "Remove %@?"; 17 | "button.removeAlert.info" = "Are you sure you want to remove this bottle? This action is permanent."; 18 | "button.removeAlert.checkbox" = "Delete files from disk"; 19 | "button.removeAlert.delete" = "Remove"; 20 | "button.removeAlert.cancel" = "Cancel"; 21 | 22 | "pin.unpin" = "Unpin Program"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Versão do Windows"; 26 | "config.buildVersion" = "Versão da compilação"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Hud Metal"; 29 | "config.metalTrace" = "Diagnóstico Metal"; 30 | "config.retinaMode" = "Modo Retina"; 31 | "config.metalTrace.info" = "Permite capturar a carga de trabalho da GPU no Xcode"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "Abrir Painel de Controlo"; 34 | "config.regedit" = "Abrir Editor de Registo"; 35 | "config.winecfg" = "Abrir Configuração do Wine"; 36 | "config.notAvailable" = "N/D"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "HUD DXVK"; 40 | "config.dxvkHud.full" = "Completo"; 41 | "config.dxvkHud.partial" = "Parcial"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "Desligado"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "Escala de DPI"; 46 | "config.inspect" = "Configurar..."; 47 | 48 | "configDpi.title" = "Configurações de DPI"; 49 | "configDpi.preview" = "O texto ficará assim:"; 50 | "configDpi.previewText" = "The quick brown fox jumps over the lazy dog."; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "All Programs"; 54 | "program.blocklist" = "Blocklist"; 55 | "program.env" = "Variáveis de Ambiente"; 56 | "program.args" = "Argumentos"; 57 | "program.config" = "Configuração"; 58 | "program.add.blocklist" = "Add to Blocklist"; 59 | "program.remove.blocklist" = "Remove from Blocklist"; 60 | 61 | "alert.message" = "Falha ao abrir o programa!"; 62 | "alert.info" = "Falha ao abrir"; 63 | 64 | "create.title" = "Criar uma nova Bottle"; 65 | "create.name" = "Nome da Bottle:"; 66 | "create.win" = "Versão do Windows:"; 67 | "create.path" = "Caminho do Bottle:"; 68 | "create.browse" = "Explorar"; 69 | "create.cancel" = "Cancelar"; 70 | "create.create" = "Criar"; 71 | 72 | "rename.bottle.title" = "Rename bottle"; 73 | "rename.pin.title" = "Rename pin"; 74 | "rename.name" = "Novo nome:"; 75 | "rename.rename" = "Alterar nome"; 76 | 77 | "check.updates" = "Verificar Actualizações..."; 78 | "kill.bottles" = "Encerrar todas as Bottles..."; 79 | "open.logs" = "Abrir Pasta dos Logs"; 80 | "open.bottle" = "Importar Bottle"; 81 | "open.setup" = "Configurar..."; 82 | "install.cli" = "Install Whisky CLI..."; 83 | "wine.clearShaderCaches" = "Limpar Cache de Shader"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Gostaria de mover o Whisky para a pasta das Aplicações ?"; 86 | "showAlertOnFirstLaunch.informativeText" = "Em alguns casos, o Whisky pode não funcionar corretamente numa pasta diferente"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "Mover para Aplicações"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "Não Mover"; 89 | 90 | "setup.welcome" = "Bem-vindo ao Whisky"; 91 | "setup.welcome.subtitle" = "Vamos prepará-lo. Isto não irá demorar um minuto."; 92 | 93 | "setup.title" = "Dependencies Setup"; 94 | "setup.subtitle" = "Manage Whisky's required dependencies."; 95 | 96 | "setup.install.checking" = "Verificando a instalação de %@..."; 97 | "setup.install.installed" = "%@ instalado"; 98 | "setup.install.notInstalled" = "%@ não instalado"; 99 | "setup.uninstall" = "Remover"; 100 | 101 | "setup.rosetta" = "Instalando Rosetta"; 102 | "setup.rosetta.subtitle" = "Rosetta permite que código x86, como o Wine, execute no seu Mac."; 103 | 104 | "setup.gptk.download" = "Transferindo Wine"; 105 | "setup.gptk.download.subtitle" = "As velocidades variam de acordo com a sua ligação de Internet."; 106 | "setup.gptk.progress" = "%@ de %@"; 107 | "setup.gptk.eta" = "- %@ em falta"; 108 | 109 | "setup.gptk.install" = "Instalando Wine"; 110 | "setup.gptk.install.subtitle" = "Quase lá. Não desligue ainda."; 111 | 112 | "setup.quit" = "Quit"; 113 | "setup.done" = "Done"; 114 | "setup.next" = "Next"; 115 | 116 | "update.gptk.title" = "New Version of GPTK Available"; 117 | "update.gptk.description" = "You are running GPTK %@, but %@ is available. Would you like to update?"; 118 | "update.gptk.update" = "Update"; 119 | 120 | "winetricks.title" = "Executar o comando Winetricks"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "Run \"%@\""; 124 | "run.bottle" = "Bottle:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /Whisky/fi.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "tab.config" = "Pullon konfiguraatio"; 2 | "tab.programs" = "Asennetut ohjelmat"; 3 | 4 | "main.createFirst" = "Luo ensimmäinen pullo, jotta voit aloittaa"; 5 | 6 | "button.cDrive" = "Avaa C: Levy"; 7 | "button.showInFinder" = "Näytä Finderissa"; 8 | "button.createShortcut" = "Luo pikakomento"; 9 | "button.run" = "Käynnistä..."; 10 | "button.ok" = "OK"; 11 | "button.rename" = "Nimeä uudelleen..."; 12 | "button.moveBottle" = "Siirrä..."; 13 | "button.exportBottle" = "Vie arkistona..."; 14 | "button.removeAlert" = "Poista..."; 15 | "button.createBottle" = "Luo pullo"; 16 | "button.removeAlert.msg" = "Poista %@?"; 17 | "button.removeAlert.info" = "Oletko varma, että haluat poistaa tämän pullon? Tämä toiminto on pysyvä."; 18 | "button.removeAlert.checkbox" = "Poista tiedostoja levyltä"; 19 | "button.removeAlert.delete" = "Poista"; 20 | "button.removeAlert.cancel" = "Peru"; 21 | 22 | "pin.unpin" = "Poista ohjelman kiinnitys"; 23 | 24 | "config.title.wine" = "Wine"; 25 | "config.winVersion" = "Windows versio"; 26 | "config.buildVersion" = "Ohjelmistoversio"; 27 | "config.title.metal" = "Metal"; 28 | "config.metalHud" = "Metal HUD"; 29 | "config.metalTrace" = "Metal Jäljitin"; 30 | "config.retinaMode" = "Retina-tila"; 31 | "config.metalTrace.info" = "Mahdollistaa GPU työn nappaamisen Xcodessa"; 32 | "config.esync" = "ESync"; 33 | "config.controlPanel" = "Avaa ohjauspaneeli"; 34 | "config.regedit" = "Avaa rekisterin muokkain"; 35 | "config.winecfg" = "Avaa Wine konfiguraatio"; 36 | "config.notAvailable" = "Ei saatavilla"; 37 | "config.title.dxvk" = "DXVK"; 38 | "config.dxvk" = "DXVK"; 39 | "config.dxvkHud" = "DXVK HUD"; 40 | "config.dxvkHud.full" = "Koko"; 41 | "config.dxvkHud.partial" = "Osittainen"; 42 | "config.dxvkHud.fps" = "FPS"; 43 | "config.dxvkHud.off" = "Pois"; 44 | "config.dxvk.async" = "DXVK Async"; 45 | "config.dpi" = "DPI skaalaus"; 46 | "config.inspect" = "Konfiguroi..."; 47 | 48 | "configDpi.title" = "DPI asetukset"; 49 | "configDpi.preview" = "Teksti tulee näyttämään tältä:"; 50 | "configDpi.previewText" = "Albert osti fagotin ja töräytti puhkuvan melodian."; 51 | "configDpi.dpi" = "DPI"; 52 | 53 | "program.title" = "Kaikki ohjelmat"; 54 | "program.blocklist" = "Estolista"; 55 | "program.env" = "Ympäristön muuttujat"; 56 | "program.args" = "Argumentit"; 57 | "program.config" = "Konfiguraatio"; 58 | "program.add.blocklist" = "Lisää estolistaan"; 59 | "program.remove.blocklist" = "Poista estolistalta"; 60 | 61 | "alert.message" = "Ohjelman käynnistäminen epäonnistui!"; 62 | "alert.info" = "Avaaminen epäonnistui"; 63 | 64 | "create.title" = "Luo uusi pullo"; 65 | "create.name" = "Pullon nimi:"; 66 | "create.win" = "Windows versio:"; 67 | "create.path" = "Pullon polku:"; 68 | "create.browse" = "Selaa"; 69 | "create.cancel" = "Peru"; 70 | "create.create" = "Luo"; 71 | 72 | "rename.bottle.title" = "Uudelleennimeä pullo"; 73 | "rename.pin.title" = "Uudelleennimeä kiinnike"; 74 | "rename.name" = "Uusi nimi:"; 75 | "rename.rename" = "Uudelleen nimeä"; 76 | 77 | "check.updates" = "Tarkista päivitykset..."; 78 | "kill.bottles" = "Tapa kaikki pullot"; 79 | "open.logs" = "Avaa log kansio"; 80 | "open.bottle" = "Tuo pullo"; 81 | "open.setup" = "Asennus..."; 82 | "install.cli" = "Asenna Whisky CLI..."; 83 | "wine.clearShaderCaches" = "Tyhjennä Varjostin-välimuistit"; 84 | 85 | "showAlertOnFirstLaunch.messageText" = "Haluatko siirtää Whiskyn Applikaatio-kansioon?"; 86 | "showAlertOnFirstLaunch.informativeText" = "Joissakin tapauksissa, Whiskyn ei ole mahdollista toimia kunnolla muista kansioista"; 87 | "showAlertOnFirstLaunch.button.moveToApplications" = "Siirrä Applikaatioihin"; 88 | "showAlertOnFirstLaunch.button.dontMove" = "Älä siirrä"; 89 | 90 | "setup.welcome" = "Tervetuloa käyttämään Whiskyä"; 91 | "setup.welcome.subtitle" = "Valmistellaan perusominaisuudet. Tämä vie vain hetken."; 92 | 93 | "setup.title" = "Riippuvuuksien valmistelu"; 94 | "setup.subtitle" = "Hallitse Whiskyn tarvittavia riippuvuuksia."; 95 | 96 | "setup.install.checking" = "Tarkistetaan %@ asennusta..."; 97 | "setup.install.installed" = "%@ asennettu"; 98 | "setup.install.notInstalled" = "%@ ei asennettu"; 99 | "setup.uninstall" = "Poista asennus"; 100 | 101 | "setup.rosetta" = "Asennetaan Rosettaa"; 102 | "setup.rosetta.subtitle" = "Rosetta, kuten Wine, mahdollistaa x86 koodin käyttämisen Macilläsi."; 103 | 104 | "setup.gptk.download" = "Ladataan GPTK"; 105 | "setup.gptk.download.subtitle" = "Nopeus vaihtelee internetyhteytesi mukaan."; 106 | "setup.gptk.progress" = "%@ / %@"; 107 | "setup.gptk.eta" = "- %@ jäljellä"; 108 | 109 | "setup.gptk.install" = "Asennetaan GPTK"; 110 | "setup.gptk.install.subtitle" = "Melkein valmista. Älä karkaa vielä."; 111 | 112 | "setup.quit" = "Sulje"; 113 | "setup.done" = "Valmis"; 114 | "setup.next" = "Seuraava"; 115 | 116 | "update.gptk.title" = "Uusi GPTK versio on saatavilla"; 117 | "update.gptk.description" = "Käytössäsi on GPTK %@, mutta %@ on saatavilla. Haluatko päivittää sen?"; 118 | "update.gptk.update" = "Päivitä"; 119 | 120 | "winetricks.title" = "Suorita Winetricks komento"; 121 | "button.winetricks" = "Winetricks..."; 122 | 123 | "run.title" = "Suorita \"%@\""; 124 | "run.bottle" = "Pullo:"; 125 | 126 | "locale.title" = "Locale"; 127 | "locale.auto" = "Automatic"; 128 | -------------------------------------------------------------------------------- /WhiskyKit/PE/ShellLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShellLink.swift 3 | // WhiskyKit 4 | // 5 | // This file is part of Whisky. 6 | // 7 | // Whisky is free software: you can redistribute it and/or modify it under the terms 8 | // of the GNU General Public License as published by the Free Software Foundation, 9 | // either version 3 of the License, or (at your option) any later version. 10 | // 11 | // Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 12 | // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License along with Whisky. 16 | // If not, see https://www.gnu.org/licenses/. 17 | // 18 | 19 | import Foundation 20 | import AppKit 21 | 22 | public struct ShellLinkHeader { 23 | public static func getProgram(url: URL, data: Data, bottle: Bottle) -> Program? { 24 | var offset: Int = 0 25 | let headerSize = data.extract(UInt32.self) ?? 0 26 | // Move past headerSize and linkCLSID 27 | offset += 4 + 16 28 | let rawLinkFlags = data.extract(UInt32.self, offset: offset) ?? 0 29 | let linkFlags = LinkFlags(rawValue: rawLinkFlags) 30 | 31 | offset = Int(headerSize) 32 | if linkFlags.contains(.hasLinkTargetIDList) { 33 | // We don't need this section so just get the size, and skip ahead 34 | offset += Int(data.extract(UInt16.self, offset: offset) ?? 0) + 2 35 | } 36 | 37 | if linkFlags.contains(.hasLinkInfo) { 38 | let linkInfo = LinkInfo(data: data, 39 | bottle: bottle, 40 | offset: &offset) 41 | return linkInfo.program 42 | } else { 43 | return nil 44 | } 45 | } 46 | } 47 | 48 | public struct LinkFlags: OptionSet, Hashable { 49 | public let rawValue: UInt32 50 | 51 | public init(rawValue: UInt32) { 52 | self.rawValue = rawValue 53 | } 54 | 55 | static let hasLinkTargetIDList = LinkFlags(rawValue: 1 << 0) 56 | static let hasLinkInfo = LinkFlags(rawValue: 1 << 1) 57 | static let hasIconLocation = LinkFlags(rawValue: 1 << 6) 58 | } 59 | 60 | public struct LinkInfo: Hashable { 61 | public var linkInfoFlags: LinkInfoFlags 62 | public var program: Program? 63 | 64 | public init(data: Data, bottle: Bottle, offset: inout Int) { 65 | let startOfSection = offset 66 | 67 | let linkInfoSize = data.extract(UInt32.self, offset: offset) ?? 0 68 | 69 | offset += 4 70 | let linkInfoHeaderSize = data.extract(UInt32.self, offset: offset) ?? 0 71 | 72 | offset += 4 73 | let rawLinkInfoFlags = data.extract(UInt32.self, offset: offset) ?? 0 74 | linkInfoFlags = LinkInfoFlags(rawValue: rawLinkInfoFlags) 75 | 76 | if linkInfoFlags.contains(.volumeIDAndLocalBasePath) { 77 | if linkInfoHeaderSize >= 0x00000024 { 78 | offset += 20 79 | let localBasePathOffsetUnicode = data.extract(UInt32.self, offset: offset) ?? 0 80 | let localPathOffset = startOfSection + Int(localBasePathOffsetUnicode) 81 | 82 | program = getProgram(data: data, 83 | offset: localPathOffset, 84 | bottle: bottle, 85 | unicode: true) 86 | } else { 87 | offset += 8 88 | let localBasePathOffset = data.extract(UInt32.self, offset: offset) ?? 0 89 | let localPathOffset = startOfSection + Int(localBasePathOffset) 90 | 91 | program = getProgram(data: data, 92 | offset: localPathOffset, 93 | bottle: bottle, 94 | unicode: false) 95 | } 96 | } 97 | 98 | offset = startOfSection + Int(linkInfoSize) 99 | } 100 | 101 | func getProgram(data: Data, offset: Int, bottle: Bottle, unicode: Bool) -> Program? { 102 | let pathData = data[offset...] 103 | if let nullRange = pathData.firstIndex(of: 0) { 104 | let encoding: String.Encoding = unicode ? .utf16 : .windowsCP1254 105 | if var string = String(data: pathData[..