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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
either position (#216).

### Fixed
- Five labels showed their raw localization key instead of text: the
"currently using" line on the Recommended graphics card and the helper
under it, the detected display size next to Virtual Desktop, and the exit
code badge and footer in the console. Each interpolates a value, and the
catalog only carried the plain key.
- "Analyze last run" and "View Latest Diagnosis" no longer open a small empty
sheet that only cmd-period could dismiss. The sheet was presented on a flag
while its content read a separate optional, which SwiftUI can evaluate
before the value lands; both are presented from the value itself now.
- The bottle's "Export Diagnostic Report" and "View Latest Diagnosis" buttons
can enable. A recorded crash diagnosis never stamped the program's last
diagnosis date, so the buttons stayed disabled forever and the ZIP export
was unreachable.
- The crash diagnosis sheet has a Done button. It had no control of its own,
so the only way out was cmd-period or closing the window behind it.
- "Analyze last run" is disabled until the program has a run to analyze.
Before the first run it clicked through to nothing.
- The crash diagnosis history on a program's page refreshes when a diagnosis
is recorded while the page is open, instead of waiting for it to be reopened.
- A program pinned in a bottle created during the same session now shows up
in the library, the Dock menu, and the menu bar extra right away. The
bottle list was rebuilt at the end of creation, so the bottle page kept
writing to an instance the rest of the app no longer read.
- "Terminate Wine processes when Whisky closes" (and a bottle's Always Kill
policy) now actually ends the bottle's processes on quit. Two things kept
it from working: the setting read as off until the toggle had been touched
once, because its default was never written to disk, and the kill was
queued asynchronously from the termination handler, so the app exited
before it ran.
- "Audio Troubleshooting" no longer opens as a small empty sheet. Same cause
as the diagnosis sheet: presented on a flag while the content read a
separate optional; it is presented from the engine itself now.
- Diagnostic exports with "Include sensitive details" off no longer carry
credentials in plain sight. Launch arguments were written to the archive
verbatim regardless of the toggle, and log redaction only rewrote the home
Expand Down
17 changes: 14 additions & 3 deletions Whisky/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}

/// The "Terminate Wine processes when Whisky closes" setting.
///
/// Read as an optional: the Settings toggle is an `@AppStorage` that
/// defaults to on without ever writing the key, and `bool(forKey:)`
/// reports an absent key as off, which silently disabled kill-on-quit
/// for anyone who never touched the toggle.
static var killOnTerminate: Bool {
UserDefaults.standard.object(forKey: "killOnTerminate") as? Bool ?? true
}

func applicationWillTerminate(_ notification: Notification) {
let globalKill = UserDefaults.standard.bool(forKey: "killOnTerminate")
let globalKill = Self.killOnTerminate

// Per-bottle kill-on-quit with policy overrides
for bottle in BottleVM.shared.bottles {
Expand All @@ -75,7 +85,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

if shouldKill {
Wine.killBottle(bottle: bottle)
// Synchronous: the app exits when this delegate returns.
Wine.killBottleAndWait(bottle: bottle)
ProcessRegistry.shared.clearRegistry(for: bottle.url)
logger.info(
"Killing bottle '\(bottle.settings.name)' on quit (policy: \(String(describing: bottlePolicy)))"
Expand Down Expand Up @@ -164,7 +175,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

let bottleName = bottle.settings.name
let policy = bottle.settings.killOnQuit
let globalKill = UserDefaults.standard.bool(forKey: "killOnTerminate")
let globalKill = Self.killOnTerminate
let shouldAutoClean: Bool = switch policy {
case .inherit: globalKill
case .alwaysKill: true
Expand Down
80 changes: 80 additions & 0 deletions Whisky/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -60213,6 +60213,38 @@
}
}
},
"config.graphics.currentlyUsing %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Currently using %@"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Currently using %@"
}
}
}
},
"config.graphics.helperCurrently %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Recommended currently resolves to %@."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Recommended currently resolves to %@."
}
}
}
},
"config.graphics.nextLaunch" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -60293,6 +60325,38 @@
}
}
},
"console.exitCode %lld" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Exit code %lld"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Exit code %lld"
}
}
}
},
"console.exited %lld" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Exited with code %lld"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Exited with code %lld"
}
}
}
},
"console.lastRun" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -104261,6 +104325,22 @@
}
}
},
"config.virtualDesktop.matchDisplay %lld %lld" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Detected display: %lld x %lld pixels"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Detected display: %lld x %lld pixels"
}
}
}
},
"config.virtualDesktop.matchDisplay.label" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
8 changes: 7 additions & 1 deletion Whisky/View Models/BottleVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,14 @@ final class BottleVM: ObservableObject {
createdBottle.saveBottleSettings()

try persistBottleCreation(request: request)
createdBottle.inFlight = false
// Reload while the bottle is still in flight so the reload keeps
// this instance. Reloading after clearing the flag replaced it,
// and the selected bottle page kept writing to the old one: its
// first pin never reached the library, the Dock menu, or the menu
// bar extra until the next reload.
loadBottles()
createdBottle.isAvailable = true
createdBottle.inFlight = false
Telemetry.capture(.firstBottleCreated)
} catch {
handleBottleCreationFailure(error, request: request, bottle: bottle)
Expand Down
39 changes: 22 additions & 17 deletions Whisky/Views/Bottle/AudioConfigSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ struct AudioConfigSection: View {
@State private var audioStatus: AudioStatus = .unknown
@State private var probeResults: [AudioProbeResult] = []
@State private var lastTestedDate: Date?
@State private var showTroubleshootingWizard: Bool = false
@State private var deviceHistory = AudioDeviceHistory()

/// Debounce timer for Bluetooth device change events.
@State private var debounceTask: Task<Void, Never>?

/// The troubleshooting engine, created when the wizard opens.
@State private var troubleshootingEngine: AudioTroubleshootingEngine?
/// The wizard and its engine, created when it opens. Item-based so the
/// sheet is built from the engine that presents it; with a flag plus a
/// separate optional the content closure could run before the engine
/// landed and show an empty sheet.
@State private var wizardPresentation: AudioWizardPresentation?

var body: some View {
Section("Audio") {
Expand Down Expand Up @@ -104,18 +106,16 @@ struct AudioConfigSection: View {
.onReceive(NotificationCenter.default.publisher(for: .openAudioTroubleshooting)) { _ in
openTroubleshootingWizard()
}
.sheet(isPresented: $showTroubleshootingWizard) {
if let engine = troubleshootingEngine {
AudioTroubleshootingWizardView(
engine: engine,
onDismiss: {
showTroubleshootingWizard = false
},
onOpenAdvanced: {
advancedMode = true
}
)
}
.sheet(item: $wizardPresentation) { presentation in
AudioTroubleshootingWizardView(
engine: presentation.engine,
onDismiss: {
wizardPresentation = nil
},
onOpenAdvanced: {
advancedMode = true
}
)
}
}
}
Expand Down Expand Up @@ -207,8 +207,7 @@ extension AudioConfigSection {
testExeURL: Bundle.main.url(forResource: "WhiskyAudioTest", withExtension: "exe")
)
]
troubleshootingEngine = AudioTroubleshootingEngine(probes: probes)
showTroubleshootingWizard = true
wizardPresentation = AudioWizardPresentation(engine: AudioTroubleshootingEngine(probes: probes))
}
}

Expand All @@ -234,3 +233,9 @@ extension AudioConfigSection {
}
}
}

/// What the audio troubleshooting sheet is built from.
struct AudioWizardPresentation: Identifiable {
let id = UUID()
let engine: AudioTroubleshootingEngine
}
78 changes: 44 additions & 34 deletions Whisky/Views/Bottle/ConfigView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,8 @@ struct ConfigView: View {
@State private var dpiSheetPresented: Bool = false
@State private var showStabilityDiagnostics: Bool = false
@State private var stabilityDiagnosticReport: String = ""
@State private var showDiagnosticExportSheet: Bool = false
@State private var showCrashDiagnosticsSheet: Bool = false
@State private var latestDiagnosis: CrashDiagnosis?
@State private var latestDiagnosisLogText: String = ""
@State private var latestDiagnosisProgram: Program?
@State private var exportPresentation: ExportPresentation?
@State private var crashPresentation: BottleDiagnosisPresentation?
@State private var isRepairingPrefix: Bool = false
@State private var prefixRepairResult: PrefixRepairResult?
@State private var gameConfigSnapshot: GameConfigSnapshot?
Expand Down Expand Up @@ -118,7 +115,7 @@ struct ConfigView: View {
Button("Export Diagnostic Report\u{2026}") {
loadLatestDiagnosisAndExport()
}
.disabled(latestDiagnosis == nil && mostRecentlyDiagnosedProgram == nil)
.disabled(mostRecentlyDiagnosedProgram == nil)

Button("View Latest Diagnosis") {
loadLatestDiagnosisAndView()
Expand Down Expand Up @@ -197,28 +194,26 @@ struct ConfigView: View {
defaultFilenamePrefix: "whisky-stability-diagnostics"
)
}
.sheet(isPresented: $showDiagnosticExportSheet) {
if let diagnosis = latestDiagnosis, let program = latestDiagnosisProgram {
DiagnosticExportSheet(
diagnosis: diagnosis,
bottle: bottle,
program: program,
logFileURL: program.settings.lastLogFileURL
)
}
// Both item-based: presenting on a flag while the content reads a
// separate optional can build the sheet before the diagnosis lands.
.sheet(item: $exportPresentation) { presentation in
DiagnosticExportSheet(
diagnosis: presentation.diagnosis,
bottle: bottle,
program: presentation.program,
logFileURL: presentation.program.settings.lastLogFileURL
)
}
.sheet(isPresented: $showCrashDiagnosticsSheet) {
if let diagnosis = latestDiagnosis, let program = latestDiagnosisProgram {
DiagnosticsView(
diagnosis: diagnosis,
logText: latestDiagnosisLogText,
programName: program.name,
bottleName: bottle.settings.name,
timestamp: program.settings.lastDiagnosisDate ?? Date(),
applyBottle: bottle
)
.frame(minWidth: 600, minHeight: 400)
}
.sheet(item: $crashPresentation) { presentation in
DiagnosticsView(
diagnosis: presentation.diagnosis,
logText: presentation.logText,
programName: presentation.program.name,
bottleName: bottle.settings.name,
timestamp: presentation.program.settings.lastDiagnosisDate ?? Date(),
applyBottle: bottle
)
.frame(minWidth: 600, minHeight: 400)
}
.alert(item: $prefixRepairResult) { result in
switch result {
Expand Down Expand Up @@ -447,9 +442,7 @@ extension ConfigView {
else { return }
Task {
guard let diagnosis = await Wine.classifyLastRun(logFileURL: logURL, exitCode: 1) else { return }
latestDiagnosis = diagnosis
latestDiagnosisProgram = program
showDiagnosticExportSheet = true
exportPresentation = ExportPresentation(diagnosis: diagnosis, program: program)
}
}

Expand All @@ -459,12 +452,29 @@ extension ConfigView {
else { return }
Task {
guard let diagnosis = await Wine.classifyLastRun(logFileURL: logURL, exitCode: 1) else { return }
latestDiagnosis = diagnosis
latestDiagnosisProgram = program
latestDiagnosisLogText = (try? String(contentsOf: logURL, encoding: .utf8)) ?? ""
showCrashDiagnosticsSheet = true
crashPresentation = BottleDiagnosisPresentation(
diagnosis: diagnosis,
program: program,
logText: (try? String(contentsOf: logURL, encoding: .utf8)) ?? ""
)
}
}
}

/// What the export sheet is exporting; `Identifiable` so `.sheet(item:)`
/// never presents without both a diagnosis and its program.
struct ExportPresentation: Identifiable {
let id = UUID()
let diagnosis: CrashDiagnosis
let program: Program
}

/// What the bottle-level diagnostics sheet is showing.
struct BottleDiagnosisPresentation: Identifiable {
let id = UUID()
let diagnosis: CrashDiagnosis
let program: Program
let logText: String
}

// swiftlint:enable file_length
Loading
Loading