diff --git a/Whisky/Extensions/Bottle+Extensions.swift b/Whisky/Extensions/Bottle+Extensions.swift index e50d614d8..06bb41fba 100644 --- a/Whisky/Extensions/Bottle+Extensions.swift +++ b/Whisky/Extensions/Bottle+Extensions.swift @@ -19,12 +19,40 @@ import Foundation import AppKit import WhiskyKit +import os.log extension Bottle { func openCDrive() { NSWorkspace.shared.open(url.appending(path: "drive_c")) } + func openTerminal() { + let whiskyCmdURL = Bundle.main.url(forResource: "WhiskyCmd", withExtension: nil) + if let whiskyCmdURL = whiskyCmdURL { + let whiskyCmd = whiskyCmdURL.path(percentEncoded: false) + let cmd = "eval \\\"$(\\\"\(whiskyCmd)\\\" shellenv \\\"\(settings.name)\\\")\\\"" + + let script = """ + tell application "Terminal" + activate + do script "\(cmd)" + end tell + """ + + Task.detached(priority: .userInitiated) { + var error: NSDictionary? + guard let appleScript = NSAppleScript(source: script) else { return } + appleScript.executeAndReturnError(&error) + + if let error = error { + Logger.wineKit.error("Failed to run terminal script \(error)") + guard let description = error["NSAppleScriptErrorMessage"] as? String else { return } + await self.showRunError(message: String(describing: description)) + } + } + } + } + @discardableResult func getStartMenuPrograms() -> [Program] { let globalStartMenu = url @@ -174,4 +202,15 @@ extension Bottle { func rename(newName: String) { settings.name = newName } + + @MainActor private func showRunError(message: String) { + let alert = NSAlert() + alert.messageText = String(localized: "alert.message") + alert.informativeText = String(localized: "alert.info") + + " \(self.url.lastPathComponent): " + + message + alert.alertStyle = .critical + alert.addButton(withTitle: String(localized: "button.ok")) + alert.runModal() + } } diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 62cfd582e..0f21fd8a4 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -2327,6 +2327,17 @@ } } }, + "button.terminal" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminal" + } + } + } + }, "button.unpin" : { "localizations" : { "da" : { @@ -16655,4 +16666,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Whisky/Views/Bottle/BottleView.swift b/Whisky/Views/Bottle/BottleView.swift index 24b10ff9b..25e34f40f 100644 --- a/Whisky/Views/Bottle/BottleView.swift +++ b/Whisky/Views/Bottle/BottleView.swift @@ -66,6 +66,9 @@ struct BottleView: View { Button("button.cDrive") { bottle.openCDrive() } + Button("button.terminal") { + bottle.openTerminal() + } Button("button.winetricks") { showWinetricksSheet.toggle() } diff --git a/WhiskyCmd/Main.swift b/WhiskyCmd/Main.swift index c76ad83d4..718eef983 100644 --- a/WhiskyCmd/Main.swift +++ b/WhiskyCmd/Main.swift @@ -33,7 +33,8 @@ struct Whisky: ParsableCommand { // Export.self, Delete.self, Remove.self, - Run.self + Run.self, + Shellenv.self /*Install.self, Uninstall.self*/]) } @@ -178,6 +179,25 @@ extension Whisky { } } + struct Shellenv: ParsableCommand { + static var configuration = CommandConfiguration(abstract: "Prints export statements for a Bottle for eval.") + + @Argument var bottleName: String + + mutating func run() throws { + var bottlesList = BottleData() + let bottles = bottlesList.loadBottles() + + guard let bottle = bottles.first(where: { $0.settings.name == bottleName }) else { + throw ValidationError("A bottle with that name doesn't exist.") + } + + let envCmd = Wine.generateTerminalEnvironmentCommand(bottle: bottle) + print(envCmd) + + } + } + struct Install: ParsableCommand { static var configuration = CommandConfiguration(abstract: "Install Whisky dependencies.") diff --git a/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift b/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift index 092e4cd57..c7617f214 100644 --- a/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift +++ b/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift @@ -117,12 +117,36 @@ public class Wine { var wineCmd = "\(wineBinary.esc) start /unix \(url.esc) \(args)" let env = constructWineEnvironment(for: bottle, environment: environment) for environment in env { - wineCmd = "\(environment.key)=\(environment.value) " + wineCmd + wineCmd = "\(environment.key)=\"\(environment.value)\" " + wineCmd } return wineCmd } + public static func generateTerminalEnvironmentCommand(bottle: Bottle) -> String { + var cmd = """ + export PATH=\"\(GPTKInstaller.binFolder.path):$PATH\" + export WINE=\"wine64\" + alias wine=\"wine64\" + alias winecfg=\"wine64 winecfg\" + alias msiexec=\"wine64 msiexec\" + alias regedit=\"wine64 regedit\" + alias regsvr32=\"wine64 regsvr32\" + alias wineboot=\"wine64 wineboot\" + alias wineconsole=\"wine64 wineconsole\" + alias winedbg=\"wine64 winedbg\" + alias winefile=\"wine64 winefile\" + alias winepath=\"wine64 winepath\" + """ + + let env = constructWineEnvironment(for: bottle, environment: constructWineEnvironment(for: bottle)) + for environment in env { + cmd += "\nexport \(environment.key)=\"\(environment.value)\"" + } + + return cmd + } + /// Run a `wineserver` command with the given arguments and return the output result private static func runWineserver(_ args: [String], bottle: Bottle) async throws -> String { var result: [ProcessOutput] = []