macOS: install update with same code path (#13562)

Previously if you select "**Install and Relaunch**" in the update pill,
there's still a confirmation alert about killing active process, but it
will just relaunch regardlessly(which is the intended behaviour) when
you do it in the command palette.

Using UpdateState to check so Ghostty will relaunch immediately when
user chooses "Install and Relaunch".

> For the auto update case, this will be handled a bit differently in
the future. If an update is already installed and waiting for relaunch
(that the user is not aware of), quitting Ghostty will still remind them
if there's active processes.
This commit is contained in:
Mitchell Hashimoto
2026-08-02 14:28:37 -07:00
committed by GitHub
2 changed files with 6 additions and 36 deletions

View File

@@ -104,7 +104,7 @@ struct TerminalCommandPaletteView: View {
badge: updateViewModel.badge,
emphasis: true
) {
(NSApp.delegate as? AppDelegate)?.updateController.installUpdate()
(NSApp.delegate as? AppDelegate)?.updateController.viewModel.state.confirm()
})
options.append(CommandOption(

View File

@@ -10,7 +10,6 @@ import Combine
class UpdateController {
private(set) var updater: SPUUpdater
private let userDriver: UpdateDriver
private var installCancellable: AnyCancellable?
var viewModel: UpdateViewModel {
userDriver.viewModel
@@ -18,7 +17,11 @@ class UpdateController {
/// True if we're installing an update.
var isInstalling: Bool {
installCancellable != nil
if case .installing = viewModel.state {
return true
} else {
return false
}
}
/// Initialize a new update controller.
@@ -35,10 +38,6 @@ class UpdateController {
)
}
deinit {
installCancellable?.cancel()
}
/// Start the updater.
///
/// This must be called before the updater can check for updates. If starting fails,
@@ -60,34 +59,6 @@ class UpdateController {
}
}
/// Force install the current update. As long as we're in some "update available" state this will
/// trigger all the steps necessary to complete the update.
func installUpdate() {
// Must be in an installable state
guard viewModel.state.isInstallable else { return }
// If we're already force installing then do nothing.
guard installCancellable == nil else { return }
// Setup a combine listener to listen for state changes and to always
// confirm them. If we go to a non-installable state, cancel the listener.
// The sink runs immediately with the current state, so we don't need to
// manually confirm the first state.
installCancellable = viewModel.$state.sink { [weak self] state in
guard let self else { return }
// If we move to a non-installable state (error, idle, etc.) then we
// stop force installing.
guard state.isInstallable else {
self.installCancellable = nil
return
}
// Continue the `yes` chain!
state.confirm()
}
}
/// Check for updates.
///
/// This is typically connected to a menu item action.
@@ -99,7 +70,6 @@ class UpdateController {
}
// If we're not idle then we need to cancel any prior state.
installCancellable?.cancel()
viewModel.state.cancel()
// The above will take time to settle, so we delay the check for some time.