macos: hook up our new update controller

This commit is contained in:
Mitchell Hashimoto
2025-10-08 21:29:14 -07:00
parent b4ab1cc1ed
commit bce49a0843
3 changed files with 80 additions and 26 deletions

View File

@@ -99,11 +99,10 @@ class AppDelegate: NSObject,
) )
/// Manages updates /// Manages updates
let updaterController: SPUStandardUpdaterController let updateController = UpdateController()
let updaterDelegate: UpdaterDelegate = UpdaterDelegate() var updateViewModel: UpdateViewModel {
updateController.viewModel
/// Update view model for UI display }
@Published private(set) var updateViewModel = UpdateViewModel()
/// The elapsed time since the process was started /// The elapsed time since the process was started
var timeSinceLaunch: TimeInterval { var timeSinceLaunch: TimeInterval {
@@ -130,15 +129,6 @@ class AppDelegate: NSObject,
} }
override init() { override init() {
updaterController = SPUStandardUpdaterController(
// Important: we must not start the updater here because we need to read our configuration
// first to determine whether we're automatically checking, downloading, etc. The updater
// is started later in applicationDidFinishLaunching
startingUpdater: false,
updaterDelegate: updaterDelegate,
userDriverDelegate: nil
)
super.init() super.init()
ghostty.delegate = self ghostty.delegate = self
@@ -183,7 +173,7 @@ class AppDelegate: NSObject,
ghosttyConfigDidChange(config: ghostty.config) ghosttyConfigDidChange(config: ghostty.config)
// Start our update checker. // Start our update checker.
updaterController.startUpdater() updateController.startUpdater()
// Register our service provider. This must happen after everything is initialized. // Register our service provider. This must happen after everything is initialized.
NSApp.servicesProvider = ServiceProvider() NSApp.servicesProvider = ServiceProvider()
@@ -810,12 +800,12 @@ class AppDelegate: NSObject,
// defined by our "auto-update" configuration (if set) or fall back to Sparkle // defined by our "auto-update" configuration (if set) or fall back to Sparkle
// user-based defaults. // user-based defaults.
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false { if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
updaterController.updater.automaticallyChecksForUpdates = false updateController.updater.automaticallyChecksForUpdates = false
updaterController.updater.automaticallyDownloadsUpdates = false updateController.updater.automaticallyDownloadsUpdates = false
} else if let autoUpdate = config.autoUpdate { } else if let autoUpdate = config.autoUpdate {
updaterController.updater.automaticallyChecksForUpdates = updateController.updater.automaticallyChecksForUpdates =
autoUpdate == .check || autoUpdate == .download autoUpdate == .check || autoUpdate == .download
updaterController.updater.automaticallyDownloadsUpdates = updateController.updater.automaticallyDownloadsUpdates =
autoUpdate == .download autoUpdate == .download
} }
@@ -1008,7 +998,8 @@ class AppDelegate: NSObject,
} }
@IBAction func checkForUpdates(_ sender: Any?) { @IBAction func checkForUpdates(_ sender: Any?) {
UpdateSimulator.permissionRequest.simulate(with: updateViewModel) updateController.checkForUpdates()
//UpdateSimulator.permissionRequest.simulate(with: updateViewModel)
} }

View File

@@ -0,0 +1,55 @@
import Sparkle
import Cocoa
/// Standard controller for managing Sparkle updates in Ghostty.
///
/// This controller wraps SPUStandardUpdaterController to provide a simpler interface
/// for managing updates with Ghostty's custom driver and delegate. It handles
/// initialization, starting the updater, and provides the check for updates action.
class UpdateController {
private(set) var updater: SPUUpdater
private let userDriver: UpdateDriver
private let updaterDelegate = UpdaterDelegate()
var viewModel: UpdateViewModel {
userDriver.viewModel
}
/// Initialize a new update controller.
init() {
let hostBundle = Bundle.main
self.userDriver = UpdateDriver(viewModel: .init())
self.updater = SPUUpdater(
hostBundle: hostBundle,
applicationBundle: hostBundle,
userDriver: userDriver,
delegate: updaterDelegate
)
}
/// Start the updater.
///
/// This must be called before the updater can check for updates. If starting fails,
/// an error alert will be shown after a short delay.
func startUpdater() {
try? updater.start()
}
/// Check for updates.
///
/// This is typically connected to a menu item action.
@objc func checkForUpdates() {
updater.checkForUpdates()
}
/// Validate the check for updates menu item.
///
/// - Parameter item: The menu item to validate
/// - Returns: Whether the menu item should be enabled
func validateMenuItem(_ item: NSMenuItem) -> Bool {
if item.action == #selector(checkForUpdates) {
return updater.canCheckForUpdates
}
return true
}
}

View File

@@ -1,13 +1,12 @@
import Cocoa
import Sparkle import Sparkle
/// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation. /// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation.
class UpdateDriver: NSObject, SPUUserDriver { class UpdateDriver: NSObject, SPUUserDriver {
let viewModel: UpdateViewModel let viewModel: UpdateViewModel
let retryHandler: () -> Void
init(viewModel: UpdateViewModel, retryHandler: @escaping () -> Void) { init(viewModel: UpdateViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
self.retryHandler = retryHandler
super.init() super.init()
} }
@@ -38,9 +37,18 @@ class UpdateDriver: NSObject, SPUUserDriver {
} }
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) {
viewModel.state = .error(.init(error: error, retry: retryHandler, dismiss: { [weak viewModel] in viewModel.state = .error(.init(
viewModel?.state = .idle error: error,
})) retry: {
guard let delegate = NSApp.delegate as? AppDelegate else {
return
}
// TODO fill this in
},
dismiss: { [weak viewModel] in
viewModel?.state = .idle
}))
} }
func showDownloadInitiated(cancellation: @escaping () -> Void) { func showDownloadInitiated(cancellation: @escaping () -> Void) {