mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-10 11:56:33 +00:00
198 lines
6.2 KiB
Swift
198 lines
6.2 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 "Enable Automatic Updates?"
|
|
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:
|
|
return .white
|
|
case .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 .permissionRequest:
|
|
return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.3, of: .black) ?? .systemBlue)
|
|
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 .permissionRequest:
|
|
return .white
|
|
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
|
|
}
|
|
}
|