mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-11 20:36:39 +00:00
207 lines
7.2 KiB
Swift
207 lines
7.2 KiB
Swift
import Cocoa
|
|
import Sparkle
|
|
|
|
/// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation.
|
|
class UpdateDriver: NSObject, SPUUserDriver {
|
|
let viewModel: UpdateViewModel
|
|
let standard: SPUStandardUserDriver
|
|
|
|
init(viewModel: UpdateViewModel, hostBundle: Bundle) {
|
|
self.viewModel = viewModel
|
|
self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil)
|
|
super.init()
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(handleTerminalWindowWillClose),
|
|
name: TerminalWindow.terminalWillCloseNotification,
|
|
object: nil)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
@objc private func handleTerminalWindowWillClose() {
|
|
// If we lost the ability to show unobtrusive states, cancel whatever
|
|
// update state we're in. This will allow the manual `check for updates`
|
|
// call to initialize the standard driver.
|
|
//
|
|
// We have to do this after a short delay so that the window can fully
|
|
// close.
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in
|
|
guard let self else { return }
|
|
guard !hasUnobtrusiveTarget else { return }
|
|
viewModel.state.cancel()
|
|
viewModel.state = .idle
|
|
}
|
|
}
|
|
|
|
func show(_ request: SPUUpdatePermissionRequest,
|
|
reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) {
|
|
viewModel.state = .permissionRequest(.init(request: request, reply: reply))
|
|
if !hasUnobtrusiveTarget {
|
|
standard.show(request, reply: reply)
|
|
}
|
|
}
|
|
|
|
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {
|
|
viewModel.state = .checking(.init(cancel: cancellation))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showUserInitiatedUpdateCheck(cancellation: cancellation)
|
|
}
|
|
}
|
|
|
|
func showUpdateFound(with appcastItem: SUAppcastItem,
|
|
state: SPUUserUpdateState,
|
|
reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
|
viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply))
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showUpdateFound(with: appcastItem, state: state, reply: reply)
|
|
}
|
|
}
|
|
|
|
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
|
// We don't do anything with the release notes here because Ghostty
|
|
// doesn't use the release notes feature of Sparkle currently.
|
|
}
|
|
|
|
func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) {
|
|
// We don't do anything with release notes. See `showUpdateReleaseNotes`
|
|
}
|
|
|
|
func showUpdateNotFoundWithError(_ error: any Error,
|
|
acknowledgement: @escaping () -> Void) {
|
|
viewModel.state = .notFound
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement)
|
|
} else {
|
|
acknowledgement()
|
|
}
|
|
}
|
|
|
|
func showUpdaterError(_ error: any Error,
|
|
acknowledgement: @escaping () -> Void) {
|
|
viewModel.state = .error(.init(
|
|
error: error,
|
|
retry: { [weak self, weak viewModel] in
|
|
viewModel?.state = .idle
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else { return }
|
|
guard let delegate = NSApp.delegate as? AppDelegate else { return }
|
|
delegate.checkForUpdates(self)
|
|
}
|
|
},
|
|
dismiss: { [weak viewModel] in
|
|
viewModel?.state = .idle
|
|
}))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showUpdaterError(error, acknowledgement: acknowledgement)
|
|
} else {
|
|
acknowledgement()
|
|
}
|
|
}
|
|
|
|
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
|
viewModel.state = .downloading(.init(
|
|
cancel: cancellation,
|
|
expectedLength: nil,
|
|
progress: 0))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showDownloadInitiated(cancellation: cancellation)
|
|
}
|
|
}
|
|
|
|
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
|
guard case let .downloading(downloading) = viewModel.state else {
|
|
return
|
|
}
|
|
|
|
viewModel.state = .downloading(.init(
|
|
cancel: downloading.cancel,
|
|
expectedLength: expectedContentLength,
|
|
progress: 0))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength)
|
|
}
|
|
}
|
|
|
|
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
|
guard case let .downloading(downloading) = viewModel.state else {
|
|
return
|
|
}
|
|
|
|
viewModel.state = .downloading(.init(
|
|
cancel: downloading.cancel,
|
|
expectedLength: downloading.expectedLength,
|
|
progress: downloading.progress + length))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showDownloadDidReceiveData(ofLength: length)
|
|
}
|
|
}
|
|
|
|
func showDownloadDidStartExtractingUpdate() {
|
|
viewModel.state = .extracting(.init(progress: 0))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showDownloadDidStartExtractingUpdate()
|
|
}
|
|
}
|
|
|
|
func showExtractionReceivedProgress(_ progress: Double) {
|
|
viewModel.state = .extracting(.init(progress: progress))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showExtractionReceivedProgress(progress)
|
|
}
|
|
}
|
|
|
|
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
|
viewModel.state = .readyToInstall(.init(reply: reply))
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showReady(toInstallAndRelaunch: reply)
|
|
}
|
|
}
|
|
|
|
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
|
viewModel.state = .installing
|
|
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication)
|
|
}
|
|
}
|
|
|
|
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
|
standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement)
|
|
viewModel.state = .idle
|
|
}
|
|
|
|
func showUpdateInFocus() {
|
|
if !hasUnobtrusiveTarget {
|
|
standard.showUpdateInFocus()
|
|
}
|
|
}
|
|
|
|
func dismissUpdateInstallation() {
|
|
viewModel.state = .idle
|
|
standard.dismissUpdateInstallation()
|
|
}
|
|
|
|
// MARK: No-Window Fallback
|
|
|
|
/// True if there is a target that can render our unobtrusive update checker.
|
|
var hasUnobtrusiveTarget: Bool {
|
|
NSApp.windows.contains { window in
|
|
window is TerminalWindow &&
|
|
window.isVisible
|
|
}
|
|
}
|
|
}
|