Add UpdateCheck and some supporting code

This commit is contained in:
2024-05-31 00:30:47 +02:00
parent 2e77fa0608
commit 9acf308ea6
8 changed files with 473 additions and 33 deletions

View File

@ -9,21 +9,55 @@ import Cocoa
class Alert {
public static var appName: String = ""
public static func show(description: String, shouldExit: Bool = true) async {
await withUnsafeContinuation { continuation in
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "\(Alert.appName) could not be updated."
alert.informativeText = description
alert.addButton(withTitle: "OK")
alert.alertStyle = .critical
alert.runModal()
if shouldExit {
exit(0)
}
continuation.resume()
// MARK: - Specific Cases
@MainActor
public static func upgradeFailure(description: String, shouldExit: Bool = true) async {
await confirm(
title: "\(Alert.appName) could not be updated.",
description: description,
alertStyle: .critical,
callback: {
exit(0)
}
)
}
// MARK: - Generic
@MainActor
public static func confirm(
title: String,
description: String,
alertStyle: NSAlert.Style = .informational,
callback: (() -> Void)? = nil
) async {
let alert = await NSAlert()
alert.messageText = title
alert.informativeText = description
await alert.addButton(withTitle: "OK")
alert.alertStyle = alertStyle
await alert.runModal()
if callback != nil {
callback!()
}
}
@MainActor
public static func choose(
title: String,
description: String,
options: [String],
cancel: Bool = false
) async -> NSApplication.ModalResponse {
let alert = await NSAlert()
alert.messageText = title
alert.informativeText = description
for option in options {
await alert.addButton(withTitle: option)
}
alert.alertStyle = .informational
return await alert.runModal()
}
}

View File

@ -0,0 +1,120 @@
//
// Created by Nico Verbruggen on 26/05/2024.
// Copyright © 2024 Nico Verbruggen. All rights reserved.
//
import Foundation
class AppVersion: Comparable {
var version: String
var build: Int?
var suffix: String?
init(version: String, build: String?, suffix: String? = nil) {
self.version = version
self.build = build == nil ? nil : Int(build!)
self.suffix = suffix
}
public static func from(_ string: String) -> AppVersion? {
do {
let regex = try NSRegularExpression(
pattern: #"(?<version>(\d+)[.](\d+)([.](\d+))?)(-(?<suffix>[a-z]+)){0,1}((,|_)(?<build>\d+)){0,1}"#,
options: []
)
let match = regex.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)
).first
guard let match = match else {
return nil
}
var version: String = ""
var build: String?
var suffix: String?
if let versionRange = Range(match.range(withName: "version"), in: string) {
version = String(string[versionRange])
}
if let buildRange = Range(match.range(withName: "build"), in: string) {
build = String(string[buildRange])
}
if let suffixRange = Range(match.range(withName: "suffix"), in: string) {
suffix = String(string[suffixRange])
}
return AppVersion(
version: version,
build: build,
suffix: suffix
)
} catch {
return nil
}
}
public static func fromCurrentVersion() -> AppVersion {
return AppVersion.from("\(Executable.shortVersion)_\(Executable.bundleVersion)")!
}
public var tagged: String {
if version.suffix(2) == ".0" && version.count > 3 {
return String(version.dropLast(2))
}
return version
}
public var computerReadable: String {
return "\(version)_\(build ?? 0)"
}
public var humanReadable: String {
return "\(version) (\(build ?? 0))"
}
// MARK: - Comparable Protocol
static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {
if lhs.version < rhs.version {
return true
}
return lhs.build ?? 0 < rhs.build ?? 0
}
static func == (lhs: AppVersion, rhs: AppVersion) -> Bool {
lhs.version == rhs.version
&& lhs.build == rhs.build
}
}
extension String {
static func ==(lhs: String, rhs: String) -> Bool {
return lhs.compare(rhs, options: .numeric) == .orderedSame
}
static func <(lhs: String, rhs: String) -> Bool {
return lhs.compare(rhs, options: .numeric) == .orderedAscending
}
static func <=(lhs: String, rhs: String) -> Bool {
return lhs.compare(rhs, options: .numeric) == .orderedAscending || lhs.compare(rhs, options: .numeric) == .orderedSame
}
static func >(lhs: String, rhs: String) -> Bool {
return lhs.compare(rhs, options: .numeric) == .orderedDescending
}
static func >=(lhs: String, rhs: String) -> Bool {
return lhs.compare(rhs, options: .numeric) == .orderedDescending || lhs.compare(rhs, options: .numeric) == .orderedSame
}
}

View File

@ -0,0 +1,70 @@
//
// Created by Nico Verbruggen on 30/05/2024.
// Copyright © 2024 Nico Verbruggen. All rights reserved.
//
import Foundation
struct CaskFile {
var properties: [String: String]
var name: String { return self.properties["name"]! }
var url: String { return self.properties["url"]! }
var sha256: String { return self.properties["sha256"]! }
var version: String { return self.properties["version"]! }
static func from(url: URL) -> CaskFile? {
var string: String?
if url.scheme == "file" {
string = try? String(contentsOf: url)
} else {
string = system("curl -s --max-time 10 '\(url.absoluteString)'")
}
guard let string else {
Log.text("The content of the URL for the CaskFile could not be retrieved")
return nil
}
let lines = string.split(separator: "\n")
.map { line in
return line.trimmingCharacters(in: .whitespacesAndNewlines)
}
.filter { $0 != "" }
if lines.count < 4 {
Log.text("The CaskFile is <4 lines long, which is too short")
return nil
}
if !lines.first!.starts(with: "cask") || !lines.last!.starts(with: "end") {
Log.text("The CaskFile does not start with 'cask' or does not end with 'end'")
return nil
}
var props: [String: String] = [:]
let regex = try! NSRegularExpression(pattern: "(\\w+)\\s+'([^']+)'")
for line in lines {
if let match = regex.firstMatch(
in: String(line),
range: NSRange(location: 0, length: line.utf16.count)
) {
let keyRange = match.range(at: 1)
let valueRange = match.range(at: 2)
let key = (line as NSString).substring(with: keyRange)
let value = (line as NSString).substring(with: valueRange)
props[key] = value
}
}
for required in ["version", "sha256", "url", "name"] where !props.keys.contains(required) {
Log.text("Property '\(required)' expected on CaskFile, assuming CaskFile is invalid")
return nil
}
return CaskFile(properties: props)
}
}

View File

@ -0,0 +1,16 @@
//
// Created by Nico Verbruggen on 30/05/2024.
// Copyright © 2024 Nico Verbruggen. All rights reserved.
//
import Foundation
open class Log {
public static func text(_ text: String) {
self.handler(text)
}
public static var handler: (_ text: String) -> Void = { text in
print(text)
}
}