import AppKit import Sparkle @MainActor final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate { static let shared = Updater() private var updater: SPUUpdater? private var now = false private var staged: (item: SUAppcastItem, install: () -> Void)? func start() { let updater = SPUUpdater(hostBundle: .main, applicationBundle: .main, userDriver: self, delegate: self) self.updater = updater do { try updater.start() } catch { tell("Updates are off", error.localizedDescription) } } func check() { if let staged { if ask(staged.item) { staged.install() } return } updater?.checkForUpdates() } private func ask(_ item: SUAppcastItem) -> Bool { let current = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" let alert = NSAlert() alert.messageText = "Byre \(item.displayVersionString) is available" alert.informativeText = "You have \(current)." alert.addButton(withTitle: "Update Now") alert.addButton(withTitle: "Update on Quit") NSApp.activate() return alert.runModal() == .alertFirstButtonReturn } private func tell(_ message: String, _ info: String) { let alert = NSAlert() alert.messageText = message alert.informativeText = info NSApp.activate() alert.runModal() } func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool { staged = (item, immediateInstallHandler) if ask(item) { immediateInstallHandler() } return true } func show(_ request: SPUUpdatePermissionRequest, reply: @escaping (SUUpdatePermissionResponse) -> Void) { reply(SUUpdatePermissionResponse(automaticUpdateChecks: true, sendSystemProfile: false)) } func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {} func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) { now = ask(appcastItem) if !now && state.stage == .installing { reply(.dismiss) return } reply(.install) } func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {} func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) {} func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { tell(error.localizedDescription, (error as NSError).localizedRecoverySuggestion ?? "") acknowledgement() } func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { tell("Update failed", error.localizedDescription) acknowledgement() } func showDownloadInitiated(cancellation: @escaping () -> Void) {} func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {} func showDownloadDidReceiveData(ofLength length: UInt64) {} func showDownloadDidStartExtractingUpdate() {} func showExtractionReceivedProgress(_ progress: Double) {} func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { reply(now ? .install : .dismiss) } func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {} func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { acknowledgement() } func dismissUpdateInstallation() {} }