macos: add a "restart later" option to the installing state

pull/9562/head
Mitchell Hashimoto 2025-11-11 07:05:20 -08:00
parent bed219c132
commit 791d8f8200
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
7 changed files with 54 additions and 11 deletions

View File

@ -22,7 +22,13 @@ extension UpdateDriver: SPUUpdaterDelegate {
/// When `auto-update = check`, Sparkle will call the corresponding
/// delegate method on the responsible driver instead.
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool {
viewModel.state = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: immediateInstallHandler))
viewModel.state = .installing(.init(
isAutoUpdate: true,
retryTerminatingApplication: immediateInstallHandler,
dismiss: { [weak viewModel] in
viewModel?.state = .idle
}
))
return true
}

View File

@ -172,7 +172,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
}
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
viewModel.state = .installing(.init(retryTerminatingApplication: retryTerminatingApplication))
viewModel.state = .installing(.init(
retryTerminatingApplication: retryTerminatingApplication,
dismiss: { [weak viewModel] in
viewModel?.state = .idle
}
))
if !hasUnobtrusiveTarget {
standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication)

View File

@ -291,7 +291,15 @@ fileprivate struct InstallingView: View {
}
HStack {
Button("Restart Later") {
installing.dismiss()
dismiss()
}
.keyboardShortcut(.cancelAction)
.controlSize(.small)
Spacer()
Button("Restart Now") {
installing.retryTerminatingApplication()
dismiss()

View File

@ -31,6 +31,9 @@ enum UpdateSimulator {
/// Shows the installing state with restart button: installing (stays until dismissed)
case installing
/// Simulates auto-update flow: goes directly to installing state without showing intermediate UI
case autoUpdate
func simulate(with viewModel: UpdateViewModel) {
switch self {
case .happyPath:
@ -49,6 +52,8 @@ enum UpdateSimulator {
simulateCancelDuringChecking(viewModel)
case .installing:
simulateInstalling(viewModel)
case .autoUpdate:
simulateAutoUpdate(viewModel)
}
}
@ -270,9 +275,27 @@ enum UpdateSimulator {
}
private func simulateInstalling(_ viewModel: UpdateViewModel) {
viewModel.state = .installing(.init(retryTerminatingApplication: {
print("Restart button clicked in simulator - resetting to idle")
viewModel.state = .idle
}))
viewModel.state = .installing(.init(
retryTerminatingApplication: {
print("Restart button clicked in simulator - resetting to idle")
viewModel.state = .idle
},
dismiss: {
viewModel.state = .idle
}
))
}
private func simulateAutoUpdate(_ viewModel: UpdateViewModel) {
viewModel.state = .installing(.init(
isAutoUpdate: true,
retryTerminatingApplication: {
print("Restart button clicked in simulator - resetting to idle")
viewModel.state = .idle
},
dismiss: {
viewModel.state = .idle
}
))
}
}

View File

@ -367,5 +367,6 @@ enum UpdateState: Equatable {
/// True if this state is triggered by ``Ghostty/UpdateDriver/updater(_:willInstallUpdateOnQuit:immediateInstallationBlock:)``
var isAutoUpdate = false
let retryTerminatingApplication: () -> Void
let dismiss: () -> Void
}
}

View File

@ -25,10 +25,10 @@ struct UpdateStateTests {
}
@Test func testInstallingEquality() {
let state1: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}))
let state2: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}))
let state1: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {}))
let state2: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {}))
#expect(state1 == state2)
let state3: UpdateState = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}))
let state3: UpdateState = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {}))
#expect(state3 != state2)
}

View File

@ -52,9 +52,9 @@ struct UpdateViewModelTests {
@Test func testInstallingText() {
let viewModel = UpdateViewModel()
viewModel.state = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}))
viewModel.state = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {}))
#expect(viewModel.text == "Installing…")
viewModel.state = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}))
viewModel.state = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {}))
#expect(viewModel.text == "Restart to Complete Update")
}