grammargem/Sources/GrammarGem/System/UpdaterManager.swift
BigMarc 3d749e8139 feat: Implement LiveMonitor for real-time grammar and spelling checks across applications
- Added LiveMonitor class to scan text areas in the frontmost window for grammar and spelling issues.
- Introduced DetectedField struct to represent text areas with issues, including suggestions for corrections.
- Implemented functionality to apply corrections and dismiss suggestions.
- Added Permissions class to manage Accessibility permissions required for monitoring.
- Created SnippetStore for managing text-expansion snippets, including persistence to UserDefaults.
- Developed TextCapture class to read and write text from the frontmost application using Accessibility API and clipboard fallback.
- Implemented TextFieldScanner to traverse the Accessibility tree and collect editable text elements.
- Added TextReplacementCoordinator to orchestrate the text correction process.
- Integrated UpdaterManager for handling application updates using Sparkle.
- Created comprehensive unit tests for various components, including grammar corrections and entitlement checks.
2026-06-18 16:49:55 +02:00

82 lines
3.1 KiB
Swift

import Foundation
import Sparkle
/// Wraps Sparkle's updater. It background-checks the appcast on grammagem.app and
/// publishes `updateAvailableVersion` when a newer, EdDSA-verified build exists, so
/// the menu bar can show an "Update available" button. Triggering an update runs
/// Sparkle's verified download + install flow.
///
/// The updater is only started inside the packaged `.app` (where the Info.plist
/// carries `SUFeedURL` + `SUPublicEDKey`); under `swift run`, tests, or the
/// self-test binaries it stays inert so nothing crashes.
@MainActor
final class UpdaterManager: NSObject, ObservableObject {
@Published private(set) var updateAvailableVersion: String?
private var controller: SPUStandardUpdaterController?
override init() {
super.init()
guard Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") != nil else {
return // not the packaged app — no updater
}
controller = SPUStandardUpdaterController(
startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil)
}
var canCheckForUpdates: Bool { controller?.updater.canCheckForUpdates ?? false }
/// User-initiated check — shows Sparkle's UI (used by the menu item and the
/// "Update" button on the availability banner).
func checkForUpdates() {
controller?.updater.checkForUpdates()
}
}
extension UpdaterManager: SPUUpdaterDelegate {
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
updateAvailableVersion = item.displayVersionString
}
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
updateAvailableVersion = nil
}
}
// MARK: - Headless update self-test (build pipeline / CI)
/// Checks `feedURL` for a newer version with no UI and prints the result, then
/// exits. Used to verify the appcast + signature + version-comparison path.
enum UpdaterSelfTest {
static func run(feedURL: String) {
let delegate = UpdateProbeDelegate(feed: feedURL)
UpdateProbeDelegate.retained = delegate
UpdateProbeDelegate.controller = SPUStandardUpdaterController(
startingUpdater: true, updaterDelegate: delegate, userDriverDelegate: nil)
UpdateProbeDelegate.controller?.updater.checkForUpdateInformation()
RunLoop.main.run() // Sparkle's network check needs the run loop; delegate exits.
}
}
private final class UpdateProbeDelegate: NSObject, SPUUpdaterDelegate {
static var controller: SPUStandardUpdaterController?
static var retained: UpdateProbeDelegate?
let feed: String
init(feed: String) { self.feed = feed; super.init() }
func feedURLString(for updater: SPUUpdater) -> String? { feed.isEmpty ? nil : feed }
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
print("UPDATE_FOUND: \(item.displayVersionString)")
exit(0)
}
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
print("UPDATE_NONE")
exit(0)
}
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
print("UPDATE_ERROR: \(error.localizedDescription)")
exit(1)
}
}