Sign In Sign Up

Refactor update simulation into dedicated file

Extract the update simulation code in @macos/Sources/App/macOS/AppDelegate.swift into a dedicated file in @macos/Sources/Features/Update . This should contain multiple simulation scenarios (happy path, not found, errors, etc.) so that we can easily try different demos.

The user wants me to extract the update simulation code from AppDelegate.swift into a dedicated file in the macos/Sources/Features/Update directory. The code should contain multiple simulation scenarios.

Let me first search for the update simulation code in the AppDelegate.swift file to see what currently exists.

I should:

  1. Find the update simulation code in AppDelegate.swift
  2. Create a new file in macos/Sources/Features/Update for the simulation scenarios
  3. Extract the simulation code and organize it with multiple scenarios
  4. Update AppDelegate.swift to use the new simulation code

Let me first read more of the AppDelegate.swift file to find the update simulation code, and also check what's in the Update directory.

I'll help you extract the update simulation code into a dedicated file. Let me first examine the rest of AppDelegate.swift and the existing Update directory structure.

Let me search for simulation-related code more broadly:

simulate|Simulate 6 matches

Now let me read the simulation code from AppDelegate.swift:

Perfect! Now I can see the simulation code. Let me create a dedicated file with multiple simulation scenarios:

@@ -1,0 +1,256 @@
import Foundation
import Sparkle

enum UpdateSimulator {
    enum Scenario {
        case happyPath
        case notFound
        case error
        case slowDownload
        case permissionRequest
        case cancelDuringDownload
        case cancelDuringChecking
    }
    
    static func simulate(_ scenario: Scenario, with viewModel: UpdateViewModel) {
        switch scenario {
        case .happyPath:
            simulateHappyPath(viewModel)
        case .notFound:
            simulateNotFound(viewModel)
        case .error:
            simulateError(viewModel)
        case .slowDownload:
            simulateSlowDownload(viewModel)
        case .permissionRequest:
            simulatePermissionRequest(viewModel)
        case .cancelDuringDownload:
            simulateCancelDuringDownload(viewModel)
        case .cancelDuringChecking:
            simulateCancelDuringChecking(viewModel)
        }
    }
    
    private static func simulateHappyPath(_ viewModel: UpdateViewModel) {
        viewModel.state = .checking(.init(cancel: {
            viewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            viewModel.state = .updateAvailable(.init(
                appcastItem: SUAppcastItem.empty(),
                reply: { choice in
                    if choice == .install {
                        simulateDownload(viewModel)
                    } else {
                        viewModel.state = .idle
                    }
                }
            ))
        }
    }
    
    private static func simulateNotFound(_ viewModel: UpdateViewModel) {
        viewModel.state = .checking(.init(cancel: {
            viewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            viewModel.state = .notFound
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
                viewModel.state = .idle
            }
        }
    }
    
    private static func simulateError(_ viewModel: UpdateViewModel) {
        viewModel.state = .checking(.init(cancel: {
            viewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            viewModel.state = .error(.init(
                error: NSError(domain: "UpdateError", code: 1, userInfo: [
                    NSLocalizedDescriptionKey: "Failed to check for updates"
                ]),
                retry: {
                    simulateHappyPath(viewModel)
                }
            ))
        }
    }
    
    private static func simulateSlowDownload(_ viewModel: UpdateViewModel) {
        viewModel.state = .checking(.init(cancel: {
            viewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            viewModel.state = .updateAvailable(.init(
                appcastItem: SUAppcastItem.empty(),
                reply: { choice in
                    if choice == .install {
                        simulateSlowDownloadProgress(viewModel)
                    } else {
                        viewModel.state = .idle
                    }
                }
            ))
        }
    }
    
    private static func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) {
        let download = UpdateState.Downloading(
            cancel: {
                viewModel.state = .idle
            },
            expectedLength: nil,
            progress: 0
        )
        viewModel.state = .downloading(download)
        
        for i in 1...20 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) {
                let updatedDownload = UpdateState.Downloading(
                    cancel: download.cancel,
                    expectedLength: 2000,
                    progress: UInt64(i * 100)
                )
                viewModel.state = .downloading(updatedDownload)
                
                if i == 20 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        simulateExtract(viewModel)
                    }
                }
            }
        }
    }
    
    private static func simulatePermissionRequest(_ viewModel: UpdateViewModel) {
        let request = SPUUpdatePermissionRequest(systemProfile: [])
        viewModel.state = .permissionRequest(.init(
            request: request,
            reply: { response in
                if response.automaticUpdateChecks {
                    simulateHappyPath(viewModel)
                } else {
                    viewModel.state = .idle
                }
            }
        ))
    }
    
    private static func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) {
        viewModel.state = .checking(.init(cancel: {
            viewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            viewModel.state = .updateAvailable(.init(
                appcastItem: SUAppcastItem.empty(),
                reply: { choice in
                    if choice == .install {
                        simulateDownloadThenCancel(viewModel)
                    } else {
                        viewModel.state = .idle
                    }
                }
            ))
        }
    }
    
    private static func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) {
        let download = UpdateState.Downloading(
            cancel: {
                viewModel.state = .idle
            },
            expectedLength: nil,
            progress: 0
        )
        viewModel.state = .downloading(download)
        
        for i in 1...5 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) {
                let updatedDownload = UpdateState.Downloading(
                    cancel: download.cancel,
                    expectedLength: 1000,
                    progress: UInt64(i * 100)
                )
                viewModel.state = .downloading(updatedDownload)
                
                if i == 5 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        viewModel.state = .idle
                    }
                }
            }
        }
    }
    
    private static func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) {
        viewModel.state = .checking(.init(cancel: {
            viewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            viewModel.state = .idle
        }
    }
    
    private static func simulateDownload(_ viewModel: UpdateViewModel) {
        let download = UpdateState.Downloading(
            cancel: {
                viewModel.state = .idle
            },
            expectedLength: nil,
            progress: 0
        )
        viewModel.state = .downloading(download)
        
        for i in 1...10 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) {
                let updatedDownload = UpdateState.Downloading(
                    cancel: download.cancel,
                    expectedLength: 1000,
                    progress: UInt64(i * 100)
                )
                viewModel.state = .downloading(updatedDownload)
                
                if i == 10 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        simulateExtract(viewModel)
                    }
                }
            }
        }
    }
    
    private static func simulateExtract(_ viewModel: UpdateViewModel) {
        viewModel.state = .extracting(.init(progress: 0.0))
        
        for j in 1...5 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) {
                viewModel.state = .extracting(.init(progress: Double(j) / 5.0))
                
                if j == 5 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        viewModel.state = .readyToInstall(.init(
                            reply: { choice in
                                if choice == .install {
                                    viewModel.state = .installing
                                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                                        viewModel.state = .idle
                                    }
                                } else {
                                    viewModel.state = .idle
                                }
                            }
                        ))
                    }
                }
            }
        }
    }
}

Now let's update AppDelegate.swift to use the new simulator:

@@ -1007,84 +1007,10 @@
        ghostty.reloadConfig()
    }

    @IBAction func checkForUpdates(_ sender: Any?) {
        // Demo mode: simulate update check with new UpdateState
        updateViewModel.state = .checking(.init(cancel: { [weak self] in
            self?.updateViewModel.state = .idle
        }))
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
            guard let self else { return }
            
            self.updateViewModel.state = .updateAvailable(.init(
                appcastItem: SUAppcastItem.empty(),
                reply: { [weak self] choice in
                    if choice == .install {
                        self?.simulateDownload()
                    } else {
                        self?.updateViewModel.state = .idle
                    }
                }
            ))
        }
        UpdateSimulator.simulate(.happyPath, with: updateViewModel)
    }
    
    private func simulateDownload() {
        let download = UpdateState.Downloading(
            cancel: { [weak self] in
                self?.updateViewModel.state = .idle
            },
            expectedLength: nil,
            progress: 0,
        )
        updateViewModel.state = .downloading(download)
        
        for i in 1...10 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { [weak self] in
                let updatedDownload = UpdateState.Downloading(
                    cancel: download.cancel,
                    expectedLength: 1000,
                    progress: UInt64(i * 100)
                )
                self?.updateViewModel.state = .downloading(updatedDownload)
                
                if i == 10 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                        self?.simulateExtract()
                    }
                }
            }
        }
    }
    
    private func simulateExtract() {
        updateViewModel.state = .extracting(.init(progress: 0.0))
        
        for j in 1...5 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { [weak self] in
                self?.updateViewModel.state = .extracting(.init(progress: Double(j) / 5.0))
                
                if j == 5 {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
                        self?.updateViewModel.state = .readyToInstall(.init(
                            reply: { [weak self] choice in
                                if choice == .install {
                                    self?.updateViewModel.state = .installing
                                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
                                        self?.updateViewModel.state = .idle
                                    }
                                } else {
                                    self?.updateViewModel.state = .idle
                                }
                            }
                        ))
                    }
                }
            }
        }
    }
    


    @IBAction func newWindow(_ sender: Any?) {
        _ = TerminalController.newWindow(ghostty)

Perfect! Now let's verify the build works:

zig build -Dapp-runtime=macos 2>&1 | head -50
error: Expected -Dapp-runtime to be of type src.apprt.runtime.Runtime.
error: access the help menu with 'zig build -h'
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 0x238f650f -Zdf0b46e06ff574e7 -Dapp-runtime=macos

Let me check the correct build command:

zig build 2>&1 | head -50