Files
ghostty/macos/Sources/Features/Update/UpdateViewModel.swift
2025-10-08 21:16:07 -07:00

192 lines
5.9 KiB
Swift

import Foundation
import SwiftUI
import Sparkle
class UpdateViewModel: ObservableObject {
@Published var state: UpdateState = .idle
/// The text to display for the current update state.
/// Returns an empty string for idle state, progress percentages for downloading/extracting,
/// or descriptive text for other states.
var text: String {
switch state {
case .idle:
return ""
case .permissionRequest:
return "Update Permission"
case .checking:
return "Checking for Updates…"
case .updateAvailable(let update):
return "Update Available: \(update.appcastItem.displayVersionString)"
case .downloading(let download):
if let expectedLength = download.expectedLength, expectedLength > 0 {
let progress = Double(download.progress) / Double(expectedLength)
return String(format: "Downloading: %.0f%%", progress * 100)
}
return "Downloading…"
case .extracting(let extracting):
return String(format: "Preparing: %.0f%%", extracting.progress * 100)
case .readyToInstall:
return "Install Update"
case .installing:
return "Installing…"
case .notFound:
return "No Updates Available"
case .error(let err):
return err.error.localizedDescription
}
}
/// The SF Symbol icon name for the current update state.
/// Returns nil for idle, downloading, and extracting states.
var iconName: String? {
switch state {
case .idle:
return nil
case .permissionRequest:
return "questionmark.circle"
case .checking:
return "arrow.triangle.2.circlepath"
case .updateAvailable:
return "arrow.down.circle.fill"
case .downloading, .extracting:
return nil
case .readyToInstall:
return "checkmark.circle.fill"
case .installing:
return "gear"
case .notFound:
return "info.circle"
case .error:
return "exclamationmark.triangle.fill"
}
}
/// The color to apply to the icon for the current update state.
var iconColor: Color {
switch state {
case .idle:
return .secondary
case .permissionRequest, .checking:
return .secondary
case .updateAvailable, .readyToInstall:
return .accentColor
case .downloading, .extracting, .installing:
return .secondary
case .notFound:
return .secondary
case .error:
return .orange
}
}
/// The background color for the update pill.
var backgroundColor: Color {
switch state {
case .updateAvailable:
return .accentColor
case .readyToInstall:
return Color(nsColor: NSColor.systemGreen.blended(withFraction: 0.3, of: .black) ?? .systemGreen)
case .notFound:
return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.5, of: .black) ?? .systemBlue)
case .error:
return .orange.opacity(0.2)
default:
return Color(nsColor: .controlBackgroundColor)
}
}
/// The foreground (text) color for the update pill.
var foregroundColor: Color {
switch state {
case .updateAvailable, .readyToInstall:
return .white
case .notFound:
return .white
case .error:
return .orange
default:
return .primary
}
}
}
enum UpdateState: Equatable {
case idle
case permissionRequest(PermissionRequest)
case checking(Checking)
case updateAvailable(UpdateAvailable)
case notFound
case error(Error)
case downloading(Downloading)
case extracting(Extracting)
case readyToInstall(ReadyToInstall)
case installing
var isIdle: Bool {
if case .idle = self { return true }
return false
}
static func == (lhs: UpdateState, rhs: UpdateState) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle):
return true
case (.permissionRequest, .permissionRequest):
return true
case (.checking, .checking):
return true
case (.updateAvailable(let lUpdate), .updateAvailable(let rUpdate)):
return lUpdate.appcastItem.displayVersionString == rUpdate.appcastItem.displayVersionString
case (.notFound, .notFound):
return true
case (.error(let lErr), .error(let rErr)):
return lErr.error.localizedDescription == rErr.error.localizedDescription
case (.downloading(let lDown), .downloading(let rDown)):
return lDown.progress == rDown.progress && lDown.expectedLength == rDown.expectedLength
case (.extracting(let lExt), .extracting(let rExt)):
return lExt.progress == rExt.progress
case (.readyToInstall, .readyToInstall):
return true
case (.installing, .installing):
return true
default:
return false
}
}
struct PermissionRequest {
let request: SPUUpdatePermissionRequest
let reply: @Sendable (SUUpdatePermissionResponse) -> Void
}
struct Checking {
let cancel: () -> Void
}
struct UpdateAvailable {
let appcastItem: SUAppcastItem
let reply: @Sendable (SPUUserUpdateChoice) -> Void
}
struct Error {
let error: any Swift.Error
let retry: () -> Void
let dismiss: () -> Void
}
struct Downloading {
let cancel: () -> Void
let expectedLength: UInt64?
let progress: UInt64
}
struct Extracting {
let progress: Double
}
struct ReadyToInstall {
let reply: @Sendable (SPUUserUpdateChoice) -> Void
}
}