Create UpdateController class for custom updater
The user wants me to create an UpdateController class similar to Sparkle's SPUStandardUpdaterController.m but for Ghostty's updater types.
Let me first:
- Read the Sparkle reference file to understand what it does
- Find Ghostty's existing updater types
- Create a similar controller class
Let me start by reading the Sparkle file from the URL provided.
I'll create an UpdateController class similar to Sparkle's SPUStandardUpdaterController. Let me first examine the reference implementation and find your existing updater types.
Ghostty's updater implementation and types in the macOS app directory are located at:
Main Implementation Files:
/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateDelegate.swift- ImplementsSPUUpdaterDelegateto handle feed URLs for different update channels (tip/stable)/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateDriver.swift- ImplementsSPUUserDriverto customize the update UI presentation/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateViewModel.swift- Contains theUpdateViewModelclass andUpdateStateenum with all update state types (idle, checking, downloading, installing, etc.)/Users/mitchellh/Documents/ghostty/macos/Sources/App/macOS/AppDelegate.swift- Initializes and manages theSPUStandardUpdaterController
Supporting Files:
/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateSimulator.swift- Testing utility for simulating various update scenarios/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateBadge.swift- UI component for update badge/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdatePill.swift- UI component for update pill display/Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdatePopoverView.swift- UI component for update popover
The implementation uses Sparkle framework for auto-updates with custom drivers and delegates to integrate with Ghostty's UI.
curl -s "https://raw.githubusercontent.com/sparkle-project/Sparkle/2.x/Sparkle/SPUStandardUpdaterController.m"
//
// SPUStandardUpdaterController.m
// Sparkle
//
// Created by Mayur Pawashe on 2/28/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#if SPARKLE_BUILD_UI_BITS
#import "SPUStandardUpdaterController.h"
#import "SPUUpdater.h"
#import "SUHost.h"
#import "SPUStandardUserDriver.h"
#import "SUConstants.h"
#import "SULog.h"
#import "SULocalizations.h"
#import <AppKit/AppKit.h>
// We use public instance variables instead of properties for the updater / user driver delegates
// because we want them to be connectable outlets from Interface Builder, but we do not want their setters to be invoked
// programmatically.
@interface SPUStandardUpdaterController () <NSMenuItemValidation>
// Needed for KVO
@property (nonatomic) SPUUpdater *updater;
@end
@implementation SPUStandardUpdaterController
@synthesize updater = _updater;
@synthesize userDriver = _userDriver;
- (void)awakeFromNib
{
// Note: awakeFromNib might be called more than once
// We have to use awakeFromNib otherwise the delegate outlets may not be connected yet,
// and we aren't a proper window or view controller, so we don't have a proper "did load" point
if (_updater == nil) {
[self _initUpdater];
[self startUpdater];
}
}
- (void)_initUpdater SPU_OBJC_DIRECT
{
NSBundle *hostBundle = [NSBundle mainBundle];
SPUStandardUserDriver *userDriver = [[SPUStandardUserDriver alloc] initWithHostBundle:hostBundle delegate:self->userDriverDelegate];
SPUUpdater *updater = [[SPUUpdater alloc] initWithHostBundle:hostBundle applicationBundle:hostBundle userDriver:userDriver delegate:self->updaterDelegate];
[self setUpdater:updater];
_userDriver = userDriver;
}
- (instancetype)initWithUpdaterDelegate:(nullable id<SPUUpdaterDelegate>)theUpdaterDelegate userDriverDelegate:(nullable id<SPUStandardUserDriverDelegate>)theUserDriverDelegate
{
return [self initWithStartingUpdater:YES updaterDelegate:theUpdaterDelegate userDriverDelegate:theUserDriverDelegate];
}
- (instancetype)initWithStartingUpdater:(BOOL)startUpdater updaterDelegate:(nullable id<SPUUpdaterDelegate>)theUpdaterDelegate userDriverDelegate:(nullable id<SPUStandardUserDriverDelegate>)theUserDriverDelegate
{
if ((self = [super init])) {
self->updaterDelegate = theUpdaterDelegate;
self->userDriverDelegate = theUserDriverDelegate;
[self _initUpdater];
if (startUpdater) {
[self startUpdater];
}
}
return self;
}
- (void)startUpdater
{
NSError *updaterError = nil;
if (![_updater startUpdater:&updaterError]) {
SULog(SULogLevelError, @"Fatal updater error (%ld): %@", updaterError.code, updaterError.localizedDescription);
// Delay the alert four seconds so it doesn't show RIGHT as the app launches, but also doesn't interrupt the user once they really get to work.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
#if SPARKLE_COPY_LOCALIZATIONS
NSBundle *sparkleBundle = SUSparkleBundle();
#endif
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = SULocalizedStringFromTableInBundle(@"Unable to Check For Updates", SPARKLE_TABLE, sparkleBundle, nil);
alert.informativeText = SULocalizedStringFromTableInBundle(@"The update checker failed to start correctly. You should contact the app developer to report this issue and verify that you have the latest version.", SPARKLE_TABLE, sparkleBundle, nil);
[alert runModal];
});
}
}
- (IBAction)checkForUpdates:(nullable id)__unused sender
{
[_updater checkForUpdates];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if ([item action] == @selector(checkForUpdates:)) {
return _updater.canCheckForUpdates;
}
return YES;
}
@end
#endif
Now I'll create an UpdateController class for Ghostty that wraps the Sparkle updater initialization and management:
@@ -1,0 +1,98 @@
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: NSObject {
/// The underlying Sparkle updater
private(set) var updater: SPUUpdater
/// The custom user driver for Ghostty's update UI
private let userDriver: UpdateDriver
/// The delegate for handling feed URLs and other updater events
private let updaterDelegate: UpdaterDelegate
/// Initialize a new update controller.
///
/// - Parameters:
/// - viewModel: The view model for managing update UI state
/// - updaterDelegate: The delegate for handling updater events
/// - startingUpdater: Whether to start the updater immediately (default: false)
init(viewModel: UpdateViewModel, updaterDelegate: UpdaterDelegate, startingUpdater: Bool = false) {
self.updaterDelegate = updaterDelegate
let hostBundle = Bundle.main
self.userDriver = UpdateDriver(viewModel: viewModel, retryHandler: {
// Will be set after init
})
self.updater = SPUUpdater(
hostBundle: hostBundle,
applicationBundle: hostBundle,
userDriver: userDriver,
delegate: updaterDelegate
)
super.init()
// Set the retry handler now that we have self
let retryUserDriver = UpdateDriver(viewModel: viewModel, retryHandler: { [weak self] in
self?.checkForUpdates()
})
// Replace the user driver with one that has the correct retry handler
self.updater = SPUUpdater(
hostBundle: hostBundle,
applicationBundle: hostBundle,
userDriver: retryUserDriver,
delegate: updaterDelegate
)
if startingUpdater {
startUpdater()
}
}
/// 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() {
var error: NSError?
if !updater.startUpdater(&error) {
let errorCode = error?.code ?? -1
let errorDescription = error?.localizedDescription ?? "Unknown error"
NSLog("Fatal updater error (\(errorCode)): \(errorDescription)")
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
let alert = NSAlert()
alert.messageText = "Unable to Check For Updates"
alert.informativeText = "The update checker failed to start correctly. You should contact the app developer to report this issue and verify that you have the latest version."
alert.runModal()
}
}
}
/// 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
}
}
Created UpdateController.swift modeled after SPUStandardUpdaterController. It wraps Sparkle's updater with Ghostty's custom UpdateDriver, UpdaterDelegate, and UpdateViewModel types.
The user made some manual changes and wants me to build the project and fix any issues. Let me first try to build it and see what errors come up.
According to the AGENTS.md, the build command is zig build.
zig build
install
+- copy app bundle
+- xcodebuild failure
** BUILD FAILED **
The following build commands failed:
SwiftCompile normal x86_64 Compiling\ UpdateController.swift /Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateController.swift (in target 'Ghostty' from project 'Ghostty')
SwiftCompile normal x86_64 /Users/mitchellh/Documents/ghostty/macos/Sources/Features/Update/UpdateController.swift (in target 'Ghostty' from project 'Ghostty')
(2 failures)
error: the following command exited with code 65 (expected exited with code 0):
cd /Users/mitchellh/Documents/ghostty/macos && xcodebuild -target Ghostty -configuration Debug
Build Summary: 275/278 steps succeeded; 1 failed
install transitive failure
+- copy app bundle transitive failure
+- xcodebuild failure
error: the following build command failed with exit code 1:
.zig-cache/o/ee7f7ad04b834e264cd6403d333044c7/build /nix/store/4y9lx4a29dshij997i4i7j2kxwqk2zs2-zig-0.15.1/bin/zig /nix/store/4y9lx4a29dshij997i4i7j2kxwqk2zs2-zig-0.15.1/lib /Users/mitchellh/Documents/ghostty .zig-cache /Users/mitchellh/.cache/zig --seed 0xddb8ba7e -Z672ee90837605f82
Process exited with code 1
cd /Users/mitchellh/Documents/ghostty/macos && xcodebuild -target Ghostty -configuration Debug -showBuildSettings 2>&1 | head -20
Command line invocation:
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -target Ghostty -configuration Debug -showBuildSettings
Resolve Package Graph
Resolved source packages:
Sparkle: https://github.com/sparkle-project/Sparkle @ 2.7.3
Build settings for action build and target Ghostty:
ACTION = build
AD_HOC_CODE_SIGNING_ALLOWED = YES
AGGREGATE_TRACKED_DOMAINS = YES
ALLOW_BUILD_REQUEST_OVERRIDES = NO
ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO
ALTERNATE_GROUP = staff
ALTERNATE_MODE = u+w,go-w,a+rX
ALTERNATE_OWNER = mitchellh
ALTERNATIVE_DISTRIBUTION_WEB = NO
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO
Thread
Reference Thread
T-6968a022-f1a2-4011-806a-1060e8e8fd81