macOS: Move update view model over to App scope

This commit is contained in:
Mitchell Hashimoto
2025-10-08 12:40:42 -07:00
parent 09ba5a27a2
commit fc347a6040
2 changed files with 50 additions and 45 deletions

View File

@@ -101,6 +101,9 @@ class AppDelegate: NSObject,
/// Manages updates /// Manages updates
let updaterController: SPUStandardUpdaterController let updaterController: SPUStandardUpdaterController
let updaterDelegate: UpdaterDelegate = UpdaterDelegate() let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
/// Update view model for UI display
@Published private(set) var updateUIModel = UpdateViewModel()
/// The elapsed time since the process was started /// The elapsed time since the process was started
var timeSinceLaunch: TimeInterval { var timeSinceLaunch: TimeInterval {
@@ -1008,24 +1011,16 @@ class AppDelegate: NSObject,
// Demo mode: simulate update check instead of real Sparkle check // Demo mode: simulate update check instead of real Sparkle check
// TODO: Replace with real updaterController.checkForUpdates(sender) when SPUUserDriver is implemented // TODO: Replace with real updaterController.checkForUpdates(sender) when SPUUserDriver is implemented
guard let terminalWindow = NSApp.keyWindow as? TerminalWindow else {
// Fallback to real update check if no terminal window
updaterController.checkForUpdates(sender)
return
}
let model = terminalWindow.updateUIModel
// Simulate the full update check flow // Simulate the full update check flow
model.state = .checking updateUIModel.state = .checking
model.progress = nil updateUIModel.progress = nil
model.details = nil updateUIModel.details = nil
model.error = nil updateUIModel.error = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// Simulate finding an update // Simulate finding an update
model.state = .updateAvailable self.updateUIModel.state = .updateAvailable
model.details = .init( self.updateUIModel.details = .init(
version: "1.2.0", version: "1.2.0",
build: "demo", build: "demo",
size: "42 MB", size: "42 MB",

View File

@@ -17,7 +17,6 @@ class TerminalWindow: NSWindow {
/// Update notification UI in titlebar /// Update notification UI in titlebar
private let updateAccessory = NSTitlebarAccessoryViewController() private let updateAccessory = NSTitlebarAccessoryViewController()
private(set) var updateUIModel = UpdateViewModel()
/// The configuration derived from the Ghostty config so we don't need to rely on references. /// The configuration derived from the Ghostty config so we don't need to rely on references.
private(set) var derivedConfig: DerivedConfig = .init() private(set) var derivedConfig: DerivedConfig = .init()
@@ -94,7 +93,7 @@ class TerminalWindow: NSWindow {
updateAccessory.layoutAttribute = .right updateAccessory.layoutAttribute = .right
updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView( updateAccessory.view = NSHostingView(rootView: UpdateAccessoryView(
viewModel: viewModel, viewModel: viewModel,
model: updateUIModel, model: appDelegate.updateUIModel,
actions: createUpdateActions() actions: createUpdateActions()
)) ))
addTitlebarAccessoryViewController(updateAccessory) addTitlebarAccessoryViewController(updateAccessory)
@@ -457,48 +456,60 @@ class TerminalWindow: NSWindow {
// MARK: Update UI // MARK: Update UI
private func createUpdateActions() -> UpdateUIActions { private func createUpdateActions() -> UpdateUIActions {
UpdateUIActions( guard let appDelegate = NSApp.delegate as? AppDelegate else {
allowAutoChecks: { [weak self] in return UpdateUIActions(
allowAutoChecks: {},
denyAutoChecks: {},
cancel: {},
install: {},
remindLater: {},
skipThisVersion: {},
showReleaseNotes: {},
retry: {}
)
}
return UpdateUIActions(
allowAutoChecks: {
print("Demo: Allow auto checks") print("Demo: Allow auto checks")
self?.updateUIModel.state = .idle appDelegate.updateUIModel.state = .idle
}, },
denyAutoChecks: { [weak self] in denyAutoChecks: {
print("Demo: Deny auto checks") print("Demo: Deny auto checks")
self?.updateUIModel.state = .idle appDelegate.updateUIModel.state = .idle
}, },
cancel: { [weak self] in cancel: {
print("Demo: Cancel") print("Demo: Cancel")
self?.updateUIModel.state = .idle appDelegate.updateUIModel.state = .idle
}, },
install: { [weak self] in install: {
guard let self else { return }
print("Demo: Install - simulating download and install flow") print("Demo: Install - simulating download and install flow")
// Start downloading // Start downloading
self.updateUIModel.state = .downloading appDelegate.updateUIModel.state = .downloading
self.updateUIModel.progress = 0.0 appDelegate.updateUIModel.progress = 0.0
// Simulate download progress // Simulate download progress
for i in 1...10 { for i in 1...10 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) {
self.updateUIModel.progress = Double(i) / 10.0 appDelegate.updateUIModel.progress = Double(i) / 10.0
if i == 10 { if i == 10 {
// Move to extraction // Move to extraction
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.updateUIModel.state = .extracting appDelegate.updateUIModel.state = .extracting
self.updateUIModel.progress = 0.0 appDelegate.updateUIModel.progress = 0.0
// Simulate extraction progress // Simulate extraction progress
for j in 1...5 { for j in 1...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) {
self.updateUIModel.progress = Double(j) / 5.0 appDelegate.updateUIModel.progress = Double(j) / 5.0
if j == 5 { if j == 5 {
// Move to ready to install // Move to ready to install
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.updateUIModel.state = .readyToInstall appDelegate.updateUIModel.state = .readyToInstall
self.updateUIModel.progress = nil appDelegate.updateUIModel.progress = nil
} }
} }
} }
@@ -508,29 +519,28 @@ class TerminalWindow: NSWindow {
} }
} }
}, },
remindLater: { [weak self] in remindLater: {
print("Demo: Remind later") print("Demo: Remind later")
self?.updateUIModel.state = .idle appDelegate.updateUIModel.state = .idle
}, },
skipThisVersion: { [weak self] in skipThisVersion: {
print("Demo: Skip version") print("Demo: Skip version")
self?.updateUIModel.state = .idle appDelegate.updateUIModel.state = .idle
}, },
showReleaseNotes: { [weak self] in showReleaseNotes: {
print("Demo: Show release notes") print("Demo: Show release notes")
guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return } guard let url = URL(string: "https://github.com/ghostty-org/ghostty/releases") else { return }
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
}, },
retry: { [weak self] in retry: {
guard let self else { return }
print("Demo: Retry - simulating update check") print("Demo: Retry - simulating update check")
self.updateUIModel.state = .checking appDelegate.updateUIModel.state = .checking
self.updateUIModel.progress = nil appDelegate.updateUIModel.progress = nil
self.updateUIModel.error = nil appDelegate.updateUIModel.error = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.updateUIModel.state = .updateAvailable appDelegate.updateUIModel.state = .updateAvailable
self.updateUIModel.details = .init( appDelegate.updateUIModel.details = .init(
version: "1.2.0", version: "1.2.0",
build: "demo", build: "demo",
size: "42 MB", size: "42 MB",