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 /// When `auto-update = check`, Sparkle will call the corresponding
/// delegate method on the responsible driver instead. /// delegate method on the responsible driver instead.
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool { 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 return true
} }

View File

@ -172,7 +172,12 @@ class UpdateDriver: NSObject, SPUUserDriver {
} }
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { 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 { if !hasUnobtrusiveTarget {
standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication) standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication)

View File

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

View File

@ -31,6 +31,9 @@ enum UpdateSimulator {
/// Shows the installing state with restart button: installing (stays until dismissed) /// Shows the installing state with restart button: installing (stays until dismissed)
case installing case installing
/// Simulates auto-update flow: goes directly to installing state without showing intermediate UI
case autoUpdate
func simulate(with viewModel: UpdateViewModel) { func simulate(with viewModel: UpdateViewModel) {
switch self { switch self {
case .happyPath: case .happyPath:
@ -49,6 +52,8 @@ enum UpdateSimulator {
simulateCancelDuringChecking(viewModel) simulateCancelDuringChecking(viewModel)
case .installing: case .installing:
simulateInstalling(viewModel) simulateInstalling(viewModel)
case .autoUpdate:
simulateAutoUpdate(viewModel)
} }
} }
@ -270,9 +275,27 @@ enum UpdateSimulator {
} }
private func simulateInstalling(_ viewModel: UpdateViewModel) { private func simulateInstalling(_ viewModel: UpdateViewModel) {
viewModel.state = .installing(.init(retryTerminatingApplication: { viewModel.state = .installing(.init(
print("Restart button clicked in simulator - resetting to idle") retryTerminatingApplication: {
viewModel.state = .idle 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:)`` /// True if this state is triggered by ``Ghostty/UpdateDriver/updater(_:willInstallUpdateOnQuit:immediateInstallationBlock:)``
var isAutoUpdate = false var isAutoUpdate = false
let retryTerminatingApplication: () -> Void let retryTerminatingApplication: () -> Void
let dismiss: () -> Void
} }
} }

View File

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

View File

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