summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitchell Hashimoto <m@mitchellh.com>2025-10-08 21:29:14 -0700
committerMitchell Hashimoto <m@mitchellh.com>2025-10-08 21:41:18 -0700
commitbce49a08438b39bbc699348d8308e724f6334f75 (patch)
treeec8f7c758ad168415069a1c5c0bdde426734a846
parentb4ab1cc1edd273577449c9ced8f713c4293134b7 (diff)
macos: hook up our new update controller
-rw-r--r--macos/Sources/App/macOS/AppDelegate.swift31
-rw-r--r--macos/Sources/Features/Update/UpdateController.swift55
-rw-r--r--macos/Sources/Features/Update/UpdateDriver.swift20
3 files changed, 80 insertions, 26 deletions
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index 62c5c0316..216373e7e 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -99,11 +99,10 @@ class AppDelegate: NSObject,
)
/// Manages updates
- let updaterController: SPUStandardUpdaterController
- let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
-
- /// Update view model for UI display
- @Published private(set) var updateViewModel = UpdateViewModel()
+ let updateController = UpdateController()
+ var updateViewModel: UpdateViewModel {
+ updateController.viewModel
+ }
/// The elapsed time since the process was started
var timeSinceLaunch: TimeInterval {
@@ -130,15 +129,6 @@ class AppDelegate: NSObject,
}
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()
ghostty.delegate = self
@@ -183,7 +173,7 @@ class AppDelegate: NSObject,
ghosttyConfigDidChange(config: ghostty.config)
// Start our update checker.
- updaterController.startUpdater()
+ updateController.startUpdater()
// Register our service provider. This must happen after everything is initialized.
NSApp.servicesProvider = ServiceProvider()
@@ -810,12 +800,12 @@ class AppDelegate: NSObject,
// defined by our "auto-update" configuration (if set) or fall back to Sparkle
// user-based defaults.
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
- updaterController.updater.automaticallyChecksForUpdates = false
- updaterController.updater.automaticallyDownloadsUpdates = false
+ updateController.updater.automaticallyChecksForUpdates = false
+ updateController.updater.automaticallyDownloadsUpdates = false
} else if let autoUpdate = config.autoUpdate {
- updaterController.updater.automaticallyChecksForUpdates =
+ updateController.updater.automaticallyChecksForUpdates =
autoUpdate == .check || autoUpdate == .download
- updaterController.updater.automaticallyDownloadsUpdates =
+ updateController.updater.automaticallyDownloadsUpdates =
autoUpdate == .download
}
@@ -1008,7 +998,8 @@ class AppDelegate: NSObject,
}
@IBAction func checkForUpdates(_ sender: Any?) {
- UpdateSimulator.permissionRequest.simulate(with: updateViewModel)
+ updateController.checkForUpdates()
+ //UpdateSimulator.permissionRequest.simulate(with: updateViewModel)
}
diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift
new file mode 100644
index 000000000..47e6c8def
--- /dev/null
+++ b/macos/Sources/Features/Update/UpdateController.swift
@@ -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
+ }
+}
diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift
index 6627559e8..70f9341a6 100644
--- a/macos/Sources/Features/Update/UpdateDriver.swift
+++ b/macos/Sources/Features/Update/UpdateDriver.swift
@@ -1,13 +1,12 @@
+import Cocoa
import Sparkle
/// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation.
class UpdateDriver: NSObject, SPUUserDriver {
let viewModel: UpdateViewModel
- let retryHandler: () -> Void
- init(viewModel: UpdateViewModel, retryHandler: @escaping () -> Void) {
+ init(viewModel: UpdateViewModel) {
self.viewModel = viewModel
- self.retryHandler = retryHandler
super.init()
}
@@ -38,9 +37,18 @@ class UpdateDriver: NSObject, SPUUserDriver {
}
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) {
- viewModel.state = .error(.init(error: error, retry: retryHandler, dismiss: { [weak viewModel] in
- viewModel?.state = .idle
- }))
+ viewModel.state = .error(.init(
+ 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) {