Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions Whisky.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
7A14B0D5D96E14098322DFC0 /* WhiskyWineUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E5CD4F34E72EE880BF131 /* WhiskyWineUpdate.swift */; };
7AA2C89FB1934624B6FFA190 /* SteamSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACB6C31E47142A637015EA4 /* SteamSession.swift */; };
7A5E34BEDDA87936C50FB0A2 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5CAFC811A7E1E60978D49E /* ErrorAlert.swift */; };
7C475A0F130182E5715D2E4C /* RunningApplications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C30F92290954D9FE1EE36A8 /* RunningApplications.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -164,6 +165,7 @@
7A3E5CD4F34E72EE880BF131 /* WhiskyWineUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyWineUpdate.swift; sourceTree = "<group>"; };
7ACB6C31E47142A637015EA4 /* SteamSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteamSession.swift; sourceTree = "<group>"; };
7A5CAFC811A7E1E60978D49E /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
1C30F92290954D9FE1EE36A8 /* RunningApplications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningApplications.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -357,6 +359,7 @@
6763D8F52BC6314100651D27 /* Constants.swift */,
7A3E5CD4F34E72EE880BF131 /* WhiskyWineUpdate.swift */,
7A5CAFC811A7E1E60978D49E /* ErrorAlert.swift */,
1C30F92290954D9FE1EE36A8 /* RunningApplications.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -634,6 +637,7 @@
7A14B0D5D96E14098322DFC0 /* WhiskyWineUpdate.swift in Sources */,
7AA2C89FB1934624B6FFA190 /* SteamSession.swift in Sources */,
7A5E34BEDDA87936C50FB0A2 /* ErrorAlert.swift in Sources */,
7C475A0F130182E5715D2E4C /* RunningApplications.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -799,7 +803,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\"";
DEVELOPMENT_TEAM = "";
Expand All @@ -818,7 +822,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy;
PRODUCT_MODULE_NAME = Whisky;
PRODUCT_NAME = Brandy;
Expand All @@ -839,7 +843,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\"";
DEVELOPMENT_TEAM = "";
Expand All @@ -858,7 +862,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy;
PRODUCT_MODULE_NAME = Whisky;
PRODUCT_NAME = Brandy;
Expand Down Expand Up @@ -928,7 +932,7 @@
CODE_SIGN_ENTITLEMENTS = WhiskyThumbnail/WhiskyThumbnail.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -944,7 +948,7 @@
"@executable_path/../../../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy.Thumbnail;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -963,7 +967,7 @@
CODE_SIGN_ENTITLEMENTS = WhiskyThumbnail/WhiskyThumbnail.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 44;
CURRENT_PROJECT_VERSION = 45;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -979,7 +983,7 @@
"@executable_path/../../../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy.Thumbnail;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
47 changes: 36 additions & 11 deletions Whisky/Extensions/Bottle+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ extension Bottle {
Self.executables(in: driveC, excluding: hidden)
}.value

let pinnedElsewhere = settings.pins.compactMap(\.url).filter { !installed.contains($0) }
let installedKeys = Set(installed.map(\.windowsPathKey))
let pinnedElsewhere = settings.pins.compactMap(\.url).filter { !installedKeys.contains($0.windowsPathKey) }
programs = (installed + pinnedElsewhere)
.map { Program(url: $0, bottle: self) }
.sorted { $0.name.lowercased() < $1.name.lowercased() }
Expand All @@ -138,11 +139,11 @@ extension Bottle {
@MainActor
func move(destination: URL) {
guard let bottle = BottleVM.shared.bottles.first(where: { $0.url == url }) else { return }
bottle.inFlight = true
bottle.activity = .moving
do {
try FileManager.default.moveItem(at: url, to: destination)
} catch {
bottle.inFlight = false
bottle.activity = nil
ErrorAlert.show(String(localized: "error.moveBottle"), error: error)
return
}
Expand All @@ -167,23 +168,36 @@ extension Bottle {
try Tar.tar(folder: url, toURL: destination)
}

/// Takes the bottle off the list and, if `delete`, deletes its folder first. A bottle with games
/// can be many gigabytes, so the folder is deleted in the background while the row shows it.
@MainActor
func remove(delete: Bool) {
let bottle = BottleVM.shared.bottles.first(where: { $0.url == url })
bottle?.inFlight = true
if delete {
guard delete else {
removeFromBottleList()
return
}

activity = .deleting
Task {
do {
try FileManager.default.removeItem(at: url)
if !WineProcesses.processIDs(in: self).isEmpty {
try await Wine.stopAllProcesses(in: self)
}
try await Task.detached(priority: .userInitiated) { [url] in
try FileManager.default.removeItem(at: url)
}.value
} catch {
bottle?.inFlight = false
activity = nil
ErrorAlert.show(String(localized: "error.removeBottle"), error: error)
return
}
removeFromBottleList()
}
}

if let path = BottleVM.shared.bottlesList.paths.firstIndex(of: url) {
BottleVM.shared.bottlesList.paths.remove(at: path)
}
@MainActor
private func removeFromBottleList() {
BottleVM.shared.bottlesList.paths.removeAll { $0 == url }
BottleVM.shared.loadBottles()
}

Expand All @@ -205,3 +219,14 @@ extension BottleSettings {
return enabled.formatted(.list(type: .and, width: .narrow))
}
}

extension BottleActivity {
/// Shown in the sidebar row in place of the bottle's details
var statusText: String {
switch self {
case .creating: String(localized: "main.creatingBottle")
case .moving: String(localized: "main.movingBottle")
case .deleting: String(localized: "main.deletingBottle")
}
}
}
30 changes: 30 additions & 0 deletions Whisky/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -9307,6 +9307,16 @@
}
}
},
"main.deletingBottle" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Deleting…"
}
}
}
},
"main.empty.title" : {
"localizations" : {
"en" : {
Expand All @@ -9317,6 +9327,16 @@
}
}
},
"main.movingBottle" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Moving…"
}
}
}
},
"main.noSelection" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -10802,6 +10822,16 @@
}
}
},
"pin.running" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Running"
}
}
}
},
"pin.title" : {
"localizations" : {
"ar" : {
Expand Down
36 changes: 36 additions & 0 deletions Whisky/Utils/RunningApplications.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// RunningApplications.swift
// Whisky
//
// This file is part of Whisky.
//
// Whisky is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Whisky.
// If not, see https://www.gnu.org/licenses/.
//

import AppKit
import Combine

/// Wine shows each Windows program that opens a window as a Mac app, so its launch and exit
/// arrive as workspace notifications; Brandy coming forward covers programs without windows.
enum RunningApplications {
static var changes: AnyPublisher<Void, Never> {
let workspace = NSWorkspace.shared.notificationCenter
return Publishers.MergeMany(
workspace.publisher(for: NSWorkspace.didLaunchApplicationNotification),
workspace.publisher(for: NSWorkspace.didTerminateApplicationNotification),
NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)
)
.map { _ in }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
2 changes: 1 addition & 1 deletion Whisky/View Models/BottleVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ final class BottleVM: ObservableObject, @unchecked Sendable {
/// Bottles still being created are not in the bottle list yet, so they are kept across reloads
@MainActor
func loadBottles() {
let bottlesInCreation = bottles.filter { $0.inFlight && !bottlesList.paths.contains($0.url) }
let bottlesInCreation = bottles.filter { $0.activity == .creating && !bottlesList.paths.contains($0.url) }
bottles = bottlesList.loadBottles() + bottlesInCreation

for bottle in bottles where bottle.isAvailable && bottle.settings.hasLegacyWindowsVersion {
Expand Down
9 changes: 1 addition & 8 deletions Whisky/Views/Bottle/BottleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,8 @@ struct BottleView: View {

let startMenuPrograms = bottle.getStartMenuPrograms()
for startMenuProgram in startMenuPrograms {
for program in bottle.programs where
// For some godforsaken reason "foo/bar" != "foo/Bar" so...
program.url.path().caseInsensitiveCompare(startMenuProgram.url.path()) == .orderedSame {
for program in bottle.programs where program.url.isSameWindowsPath(as: startMenuProgram.url) {
program.pinned = true
guard !bottle.settings.pins.contains(where: { $0.url == program.url }) else { continue }
bottle.settings.pins.append(PinnedProgram(
name: program.url.deletingPathExtension().lastPathComponent,
url: program.url
))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Whisky/Views/Bottle/Pins/PinCreationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct PinCreationView: View {
guard let newPinURL else { return }

// Ensure this pin doesn't already exist
guard !bottle.settings.pins.contains(where: { $0.url == newPinURL })
guard !bottle.settings.pins.contains(where: { $0.opens(newPinURL) })
else {
isDuplicate = true
return
Expand Down
28 changes: 27 additions & 1 deletion Whisky/Views/Bottle/Pins/PinView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ struct PinView: View {
@State private var name: String = ""
@State private var opening: Bool = false
@State private var hovered: Bool = false
@State private var isRunning: Bool = false

private var isSteam: Bool {
SteamInstaller.isSteam(program.url, in: bottle)
}

var body: some View {
PinTile(title: name, isHighlighted: hovered) {
Expand All @@ -48,9 +53,22 @@ struct PinView: View {
.opacity(hovered ? 1 : 0)
.help("button.run")
}
.overlay(alignment: .bottom) {
if isRunning {
Circle()
.fill(.secondary)
.frame(width: 5, height: 5)
.offset(y: 8)
.help("pin.running")
}
}
}
.onHover { hovered = $0 }
.help(program.displayLocation)
.onAppear(perform: refreshRunning)
.onReceive(RunningApplications.changes) { _ in
refreshRunning()
}
.contextMenu {
ProgramMenuView(program: program, path: $path)

Expand Down Expand Up @@ -93,6 +111,14 @@ struct PinView: View {
}
}

program.run()
if isSteam {
SteamSession.session(for: bottle).open()
} else {
program.run()
}
}

private func refreshRunning() {
isRunning = program.isRunning
}
}
6 changes: 3 additions & 3 deletions Whisky/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ struct ContentView: View {
Section("main.bottles") {
ForEach(filteredBottles) { bottle in
Group {
if bottle.inFlight {
if let activity = bottle.activity {
BottleRowLabel(name: bottle.settings.name,
subtitle: String(localized: "main.creatingBottle"),
subtitle: activity.statusText,
systemImage: "wineglass")
.badge(Text(Image(systemName: "hourglass")))
.foregroundStyle(.secondary)
Expand Down Expand Up @@ -168,7 +168,7 @@ struct ContentView: View {
if let bottle = selected {
if let bottle = bottleVM.bottles.first(where: { $0.url == bottle }) {
BottleView(bottle: bottle)
.disabled(bottle.inFlight)
.disabled(bottle.activity != nil)
.id(bottle.url)
}
} else if bottlesLoaded {
Expand Down
Loading
Loading