Sign In Sign Up

Create UpdateController class for custom updater

Make an `UpdateController` class the same as https://github.com/sparkle-project/Sparkle/blob/2.x/Sparkle/SPUStandardUpdaterController.m but for our updater types.

The user wants me to create an UpdateController class similar to Sparkle's SPUStandardUpdaterController.m but for Ghostty's updater types.

Let me first:

  1. Read the Sparkle reference file to understand what it does
  2. Find Ghostty's existing updater types
  3. 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.

Find Ghostty's updater implementation and types in the macOS app directory
updater|Updater 25 matches
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.

I made some manual changes, try to build and fix it up.

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