mirror of https://github.com/buresdv/Cork
~ Basic conformance to new style rules
This commit is contained in:
parent
64b5315661
commit
ec70520a42
|
|
@ -6,7 +6,6 @@ disabled_rules: # rule identifiers turned on by default to exclude from running
|
|||
- empty_count
|
||||
- function_body_length
|
||||
- shorthand_operator
|
||||
- cyclomatic_complexity # Turn this on once install function refactor is in main
|
||||
- opening_brace
|
||||
- line_length
|
||||
- statement_position
|
||||
|
|
@ -21,8 +20,8 @@ disabled_rules: # rule identifiers turned on by default to exclude from running
|
|||
opt_in_rules: # some rules are turned off by default, so you need to opt-in
|
||||
- empty_count # find all the available rules by running: `swiftlint rules`
|
||||
- explicit_init
|
||||
- contrasted_opening_brace
|
||||
# - explicit_type_interface # Force the specification of types
|
||||
# - contrasted_opening_brace
|
||||
- explicit_type_interface # Force the specification of types
|
||||
|
||||
# Alternatively, specify all rules explicitly by uncommenting this option:
|
||||
# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
|
||||
|
|
@ -93,4 +92,4 @@ custom_rules:
|
|||
# name: "Else Statement Position"
|
||||
# regex: '\}[ \t]*\n[ \t]*else'
|
||||
# message: "Else statements should be on their own line, right after the closing brace of the preceding block."
|
||||
# severity: error
|
||||
# severity: error
|
||||
|
|
|
|||
|
|
@ -6,46 +6,54 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
import UserNotifications
|
||||
|
||||
struct AppConstants
|
||||
enum AppConstants
|
||||
{
|
||||
// MARK: - Logging
|
||||
static let logger: Logger = Logger(subsystem: "com.davidbures.cork", category: "Cork")
|
||||
|
||||
|
||||
static let logger: Logger = .init(subsystem: "com.davidbures.cork", category: "Cork")
|
||||
|
||||
// MARK: - Notification stuff
|
||||
|
||||
static let notificationCenter = UNUserNotificationCenter.current()
|
||||
|
||||
|
||||
// MARK: - Proxy settings
|
||||
static let proxySettings: (host: String, port: Int)? =
|
||||
{
|
||||
|
||||
static let proxySettings: (host: String, port: Int)? = {
|
||||
let proxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() as? [String: Any]
|
||||
|
||||
guard let httpProxyHost = proxySettings?[kCFNetworkProxiesHTTPProxy as String] as? String else {
|
||||
|
||||
guard let httpProxyHost = proxySettings?[kCFNetworkProxiesHTTPProxy as String] as? String
|
||||
else
|
||||
{
|
||||
AppConstants.logger.error("Could not get proxy host")
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
guard let httpProxyPort = proxySettings?[kCFNetworkProxiesHTTPPort as String] as? Int else {
|
||||
guard let httpProxyPort = proxySettings?[kCFNetworkProxiesHTTPPort as String] as? Int
|
||||
else
|
||||
{
|
||||
AppConstants.logger.error("Could not get proxy port")
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return (host: httpProxyHost, port: httpProxyPort)
|
||||
}()
|
||||
|
||||
// MARK: - Basic executables and file locations
|
||||
|
||||
static let brewExecutablePath: URL =
|
||||
{
|
||||
static let brewExecutablePath: URL = {
|
||||
/// If a custom Homebrew path is defined, use it. Otherwise, use the default paths
|
||||
if let homebrewPath = UserDefaults.standard.string(forKey: "customHomebrewPath"), !homebrewPath.isEmpty {
|
||||
if let homebrewPath = UserDefaults.standard.string(forKey: "customHomebrewPath"), !homebrewPath.isEmpty
|
||||
{
|
||||
let customHomebrewPath = URL(string: homebrewPath)!
|
||||
|
||||
|
||||
return customHomebrewPath
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: "/opt/homebrew/bin/brew")
|
||||
{ // Apple Sillicon
|
||||
return URL(string: "/opt/homebrew/bin/brew")!
|
||||
|
|
@ -57,8 +65,7 @@ struct AppConstants
|
|||
}
|
||||
}()
|
||||
|
||||
static let brewCellarPath: URL =
|
||||
{
|
||||
static let brewCellarPath: URL = {
|
||||
if FileManager.default.fileExists(atPath: "/opt/homebrew/Cellar")
|
||||
{ // Apple Sillicon
|
||||
return URL(filePath: "/opt/homebrew/Cellar")
|
||||
|
|
@ -69,8 +76,7 @@ struct AppConstants
|
|||
}
|
||||
}()
|
||||
|
||||
static let brewCaskPath: URL =
|
||||
{
|
||||
static let brewCaskPath: URL = {
|
||||
if FileManager.default.fileExists(atPath: "/opt/homebrew/Caskroom")
|
||||
{ // Apple Sillicon
|
||||
return URL(filePath: "/opt/homebrew/Caskroom")
|
||||
|
|
@ -81,8 +87,7 @@ struct AppConstants
|
|||
}
|
||||
}()
|
||||
|
||||
static let tapPath: URL =
|
||||
{
|
||||
static let tapPath: URL = {
|
||||
if FileManager.default.fileExists(atPath: "/opt/homebrew/Library/Taps")
|
||||
{ // Apple Sillicon
|
||||
return URL(filePath: "/opt/homebrew/Library/Taps")
|
||||
|
|
@ -108,22 +113,24 @@ struct AppConstants
|
|||
|
||||
/// This one has all the downloaded files themselves
|
||||
static let brewCachedDownloadsPath: URL = brewCachePath.appendingPathComponent("downloads", conformingTo: .directory)
|
||||
|
||||
|
||||
// MARK: - Licensing
|
||||
static let demoLengthInSeconds: Double = 604800 // 7 days
|
||||
|
||||
static let authorizationEndpointURL: URL = URL(string: "https://automation.tomoserver.eu/webhook/38aacca6-5da8-453c-a001-804b15751319")!
|
||||
|
||||
static let demoLengthInSeconds: Double = 604_800 // 7 days
|
||||
|
||||
static let authorizationEndpointURL: URL = .init(string: "https://automation.tomoserver.eu/webhook/38aacca6-5da8-453c-a001-804b15751319")!
|
||||
static let licensingAuthorization: (username: String, passphrase: String) = ("cork-authorization", "choosy-defame-neon-resume-cahoots")
|
||||
|
||||
|
||||
// MARK: - Temporary OS version submission
|
||||
static let osSubmissionEndpointURL: URL = URL(string: "https://automation.tomoserver.eu/webhook/3a971576-fa96-479e-9dc4-e052fe33270b")!
|
||||
|
||||
|
||||
static let osSubmissionEndpointURL: URL = .init(string: "https://automation.tomoserver.eu/webhook/3a971576-fa96-479e-9dc4-e052fe33270b")!
|
||||
|
||||
// MARK: - Misc Stuff
|
||||
|
||||
static let backgroundUpdateInterval: TimeInterval = 10 * 60
|
||||
static let backgroundUpdateIntervalTolerance: TimeInterval = 1 * 60
|
||||
|
||||
static let osVersionString: (lookupName: String, fullName: String) =
|
||||
{
|
||||
static let osVersionString: (lookupName: String, fullName: String) = {
|
||||
let versionDictionary: [Int: (lookupName: String, fullName: String)] = [
|
||||
15: ("sequoia", "Sequoia"),
|
||||
14: ("sonoma", "Sonoma"),
|
||||
|
|
|
|||
|
|
@ -1,30 +1,34 @@
|
|||
//
|
||||
// AppState.swift
|
||||
// App State.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 05.02.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
@preconcurrency import UserNotifications
|
||||
|
||||
/// Class that holds the global state of the app, excluding services
|
||||
@MainActor
|
||||
class AppState: ObservableObject
|
||||
class AppState: ObservableObject
|
||||
{
|
||||
// MARK: - Licensing
|
||||
|
||||
@Published var licensingState: LicensingState = .notBoughtOrHasNotActivatedDemo
|
||||
@Published var isShowingLicensingSheet: Bool = false
|
||||
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
@Published var navigationSelection: UUID?
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@Published var notificationEnabledInSystemSettings: Bool?
|
||||
@Published var notificationAuthStatus: UNAuthorizationStatus = .notDetermined
|
||||
|
||||
|
||||
// MARK: - Stuff for controlling various sheets from the menu bar
|
||||
|
||||
@Published var isShowingInstallationSheet: Bool = false
|
||||
@Published var isShowingPackageReinstallationSheet: Bool = false
|
||||
@Published var isShowingUninstallationSheet: Bool = false
|
||||
|
|
@ -32,198 +36,207 @@ class AppState: ObservableObject
|
|||
@Published var isShowingFastCacheDeletionMaintenanceView: Bool = false
|
||||
@Published var isShowingAddTapSheet: Bool = false
|
||||
@Published var isShowingUpdateSheet: Bool = false
|
||||
|
||||
|
||||
// MARK: - Stuff for controlling the UI in general
|
||||
|
||||
@Published var isSearchFieldFocused: Bool = false
|
||||
|
||||
|
||||
// MARK: - Brewfile importing and exporting
|
||||
|
||||
@Published var isShowingBrewfileExportProgress: Bool = false
|
||||
@Published var isShowingBrewfileImportProgress: Bool = false
|
||||
@Published var brewfileImportingStage: BrewfileImportStage = .importing
|
||||
|
||||
|
||||
@Published var isCheckingForPackageUpdates: Bool = true
|
||||
|
||||
|
||||
@Published var isShowingUninstallationProgressView: Bool = false
|
||||
@Published var isShowingFatalError: Bool = false
|
||||
@Published var fatalAlertType: DisplayableAlert? = nil
|
||||
|
||||
|
||||
@Published var isShowingSudoRequiredForUninstallSheet: Bool = false
|
||||
@Published var packageTryingToBeUninstalledWithSudo: BrewPackage?
|
||||
|
||||
|
||||
@Published var isShowingRemoveTapFailedAlert: Bool = false
|
||||
|
||||
|
||||
@Published var isShowingIncrementalUpdateSheet: Bool = false
|
||||
|
||||
|
||||
@Published var isLoadingFormulae: Bool = true
|
||||
@Published var isLoadingCasks: Bool = true
|
||||
|
||||
|
||||
@Published var isLoadingTopPackages: Bool = false
|
||||
@Published var failedWhileLoadingTopPackages: Bool = false
|
||||
|
||||
|
||||
@Published var cachedDownloadsFolderSize: Int64 = AppConstants.brewCachedDownloadsPath.directorySize
|
||||
@Published var cachedDownloads: [CachedDownload] = .init()
|
||||
|
||||
|
||||
private var cachedDownloadsTemp: [CachedDownload] = .init()
|
||||
|
||||
|
||||
@Published var taggedPackageNames: Set<String> = .init()
|
||||
|
||||
|
||||
@Published var corruptedPackage: String = ""
|
||||
|
||||
|
||||
// MARK: - Showing errors
|
||||
|
||||
func showAlert(errorToShow: DisplayableAlert)
|
||||
{
|
||||
self.fatalAlertType = errorToShow
|
||||
|
||||
self.isShowingFatalError = true
|
||||
fatalAlertType = errorToShow
|
||||
|
||||
isShowingFatalError = true
|
||||
}
|
||||
|
||||
|
||||
func dismissAlert()
|
||||
{
|
||||
self.isShowingFatalError = false
|
||||
|
||||
self.fatalAlertType = nil
|
||||
isShowingFatalError = false
|
||||
|
||||
fatalAlertType = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notification setup
|
||||
|
||||
func setupNotifications() async
|
||||
{
|
||||
let notificationCenter = AppConstants.notificationCenter
|
||||
|
||||
|
||||
let authStatus = await notificationCenter.authorizationStatus()
|
||||
|
||||
switch authStatus
|
||||
{
|
||||
case .notDetermined:
|
||||
AppConstants.logger.debug("Notification authorization status not determined. Will request notifications again")
|
||||
|
||||
await self.requestNotificationAuthorization()
|
||||
case .denied:
|
||||
AppConstants.logger.debug("Notifications were refused")
|
||||
case .authorized:
|
||||
AppConstants.logger.debug("Notifications were authorized")
|
||||
|
||||
case .provisional:
|
||||
AppConstants.logger.debug("Notifications are provisional")
|
||||
|
||||
case .ephemeral:
|
||||
AppConstants.logger.debug("Notifications are ephemeral")
|
||||
|
||||
@unknown default:
|
||||
AppConstants.logger.error("Something got really fucked up about notifications setup")
|
||||
case .notDetermined:
|
||||
AppConstants.logger.debug("Notification authorization status not determined. Will request notifications again")
|
||||
|
||||
await requestNotificationAuthorization()
|
||||
|
||||
case .denied:
|
||||
AppConstants.logger.debug("Notifications were refused")
|
||||
|
||||
case .authorized:
|
||||
AppConstants.logger.debug("Notifications were authorized")
|
||||
|
||||
case .provisional:
|
||||
AppConstants.logger.debug("Notifications are provisional")
|
||||
|
||||
case .ephemeral:
|
||||
AppConstants.logger.debug("Notifications are ephemeral")
|
||||
|
||||
@unknown default:
|
||||
AppConstants.logger.error("Something got really fucked up about notifications setup")
|
||||
}
|
||||
|
||||
|
||||
notificationAuthStatus = authStatus
|
||||
}
|
||||
|
||||
|
||||
func requestNotificationAuthorization() async
|
||||
{
|
||||
let notificationCenter = AppConstants.notificationCenter
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
|
||||
|
||||
notificationEnabledInSystemSettings = true
|
||||
}
|
||||
catch let notificationPermissionsObtainingError as NSError
|
||||
{
|
||||
AppConstants.logger.error("Notification permissions obtaining error: \(notificationPermissionsObtainingError.localizedDescription, privacy: .public)\nError code: \(notificationPermissionsObtainingError.code, privacy: .public)")
|
||||
|
||||
|
||||
notificationEnabledInSystemSettings = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Initiating the update process from legacy contexts
|
||||
@objc func startUpdateProcessForLegacySelectors(_ sender: NSMenuItem!) -> Void
|
||||
|
||||
@objc func startUpdateProcessForLegacySelectors(_: NSMenuItem!)
|
||||
{
|
||||
self.isShowingUpdateSheet = true
|
||||
|
||||
isShowingUpdateSheet = true
|
||||
|
||||
sendNotification(title: String(localized: "notification.upgrade-process-started"))
|
||||
}
|
||||
|
||||
|
||||
func loadCachedDownloadedPackages() async
|
||||
{
|
||||
let smallestDispalyableSize: Int = Int(self.cachedDownloadsFolderSize / 50)
|
||||
|
||||
var packagesThatAreTooSmallToDisplaySize: Int = 0
|
||||
|
||||
guard let cachedDownloadsFolderContents: [URL] = try? FileManager.default.contentsOfDirectory(at: AppConstants.brewCachedDownloadsPath, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles]) else
|
||||
let smallestDispalyableSize = Int(cachedDownloadsFolderSize / 50)
|
||||
|
||||
var packagesThatAreTooSmallToDisplaySize = 0
|
||||
|
||||
guard let cachedDownloadsFolderContents: [URL] = try? FileManager.default.contentsOfDirectory(at: AppConstants.brewCachedDownloadsPath, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles])
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
let usableCachedDownloads: [URL] = cachedDownloadsFolderContents.filter({ $0.pathExtension != "json" })
|
||||
|
||||
|
||||
let usableCachedDownloads: [URL] = cachedDownloadsFolderContents.filter { $0.pathExtension != "json" }
|
||||
|
||||
for usableCachedDownload in usableCachedDownloads
|
||||
{
|
||||
guard var itemName: String = try? regexMatch(from: usableCachedDownload.lastPathComponent, regex: "(?<=--)(.*?)(?=\\.)") else
|
||||
guard var itemName: String = try? regexMatch(from: usableCachedDownload.lastPathComponent, regex: "(?<=--)(.*?)(?=\\.)")
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
AppConstants.logger.debug("Temp item name: \(itemName, privacy: .public)")
|
||||
|
||||
|
||||
if itemName.contains("--")
|
||||
{
|
||||
do
|
||||
{
|
||||
itemName = try regexMatch(from: itemName, regex: ".*?(?=--)")
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
guard let itemAttributes = try? FileManager.default.attributesOfItem(atPath: usableCachedDownload.path) else
|
||||
|
||||
guard let itemAttributes = try? FileManager.default.attributesOfItem(atPath: usableCachedDownload.path)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard let itemSize = itemAttributes[.size] as? Int else
|
||||
|
||||
guard let itemSize = itemAttributes[.size] as? Int
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if itemSize < smallestDispalyableSize
|
||||
{
|
||||
packagesThatAreTooSmallToDisplaySize = packagesThatAreTooSmallToDisplaySize + itemSize
|
||||
}
|
||||
else
|
||||
{
|
||||
self.cachedDownloads.append(CachedDownload(packageName: itemName, sizeInBytes: itemSize))
|
||||
cachedDownloads.append(CachedDownload(packageName: itemName, sizeInBytes: itemSize))
|
||||
}
|
||||
|
||||
|
||||
AppConstants.logger.debug("Others size: \(packagesThatAreTooSmallToDisplaySize, privacy: .public)")
|
||||
}
|
||||
|
||||
|
||||
AppConstants.logger.log("Cached downloads contents: \(self.cachedDownloads)")
|
||||
|
||||
self.cachedDownloads = self.cachedDownloads.sorted(by: { $0.sizeInBytes < $1.sizeInBytes })
|
||||
|
||||
self.cachedDownloads.append(.init(packageName: String(localized: "start-page.cached-downloads.graph.other-smaller-packages"), sizeInBytes: packagesThatAreTooSmallToDisplaySize, packageType: .other))
|
||||
|
||||
cachedDownloads = cachedDownloads.sorted(by: { $0.sizeInBytes < $1.sizeInBytes })
|
||||
|
||||
cachedDownloads.append(.init(packageName: String(localized: "start-page.cached-downloads.graph.other-smaller-packages"), sizeInBytes: packagesThatAreTooSmallToDisplaySize, packageType: .other))
|
||||
}
|
||||
}
|
||||
|
||||
private extension UNUserNotificationCenter {
|
||||
func authorizationStatus() async -> UNAuthorizationStatus {
|
||||
private extension UNUserNotificationCenter
|
||||
{
|
||||
func authorizationStatus() async -> UNAuthorizationStatus
|
||||
{
|
||||
await notificationSettings().authorizationStatus
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension AppState
|
||||
{
|
||||
func assignPackageTypeToCachedDownloads(brewData: BrewDataStorage) -> Void
|
||||
func assignPackageTypeToCachedDownloads(brewData: BrewDataStorage)
|
||||
{
|
||||
var cachedDownloadsTracker: [CachedDownload] = .init()
|
||||
|
||||
|
||||
AppConstants.logger.debug("Package tracker in cached download assignment function has \(brewData.installedFormulae.count + brewData.installedCasks.count) packages")
|
||||
|
||||
for cachedDownload in self.cachedDownloads
|
||||
|
||||
for cachedDownload in cachedDownloads
|
||||
{
|
||||
let normalizedCachedPackageName: String = cachedDownload.packageName.onlyLetters
|
||||
|
||||
|
||||
if brewData.installedFormulae.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
|
||||
{ /// The cached package is a formula
|
||||
AppConstants.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a formula")
|
||||
|
|
@ -240,7 +253,7 @@ extension AppState
|
|||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .unknown))
|
||||
}
|
||||
}
|
||||
|
||||
self.cachedDownloads = cachedDownloadsTracker
|
||||
|
||||
cachedDownloads = cachedDownloadsTracker
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
{
|
||||
@AppStorage("showInMenuBar") var showInMenuBar = false
|
||||
@AppStorage("startWithoutWindow") var startWithoutWindow: Bool = false
|
||||
|
||||
|
||||
@MainActor let appState = AppState()
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification)
|
||||
|
||||
func applicationWillFinishLaunching(_: Notification)
|
||||
{
|
||||
if startWithoutWindow
|
||||
{
|
||||
|
|
@ -28,26 +28,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
NSApp.setActivationPolicy(.regular)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification)
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification)
|
||||
{
|
||||
if startWithoutWindow
|
||||
{
|
||||
if let window = NSApplication.shared.windows.first {
|
||||
if let window = NSApplication.shared.windows.first
|
||||
{
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillBecomeActive(_ notification: Notification)
|
||||
|
||||
func applicationWillBecomeActive(_: Notification)
|
||||
{
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
}
|
||||
func applicationWillUnhide(_ notification: Notification)
|
||||
|
||||
func applicationWillUnhide(_: Notification)
|
||||
{
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
}
|
||||
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool
|
||||
{
|
||||
if showInMenuBar
|
||||
|
|
@ -62,7 +64,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
func applicationWillTerminate(_: Notification)
|
||||
{
|
||||
AppConstants.logger.debug("Will die...")
|
||||
do
|
||||
{
|
||||
|
|
@ -74,15 +77,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
}
|
||||
AppConstants.logger.debug("Died")
|
||||
}
|
||||
|
||||
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
|
||||
|
||||
func applicationDockMenu(_: NSApplication) -> NSMenu?
|
||||
{
|
||||
let menu = NSMenu()
|
||||
menu.autoenablesItems = false
|
||||
|
||||
|
||||
let updatePackagesMenuItem = NSMenuItem()
|
||||
updatePackagesMenuItem.action = #selector(appState.startUpdateProcessForLegacySelectors(_:))
|
||||
updatePackagesMenuItem.target = appState
|
||||
|
||||
|
||||
if appState.isCheckingForPackageUpdates
|
||||
{
|
||||
updatePackagesMenuItem.title = String(localized: "start-page.updates.loading")
|
||||
|
|
@ -98,9 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
updatePackagesMenuItem.title = String(localized: "navigation.menu.packages.update")
|
||||
updatePackagesMenuItem.isEnabled = true
|
||||
}
|
||||
|
||||
|
||||
menu.addItem(updatePackagesMenuItem)
|
||||
|
||||
|
||||
return menu
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by David Bureš on 03.07.2022.
|
||||
//
|
||||
|
||||
import DavidFoundation
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import DavidFoundation
|
||||
|
||||
@main
|
||||
struct CorkApp: App
|
||||
|
|
@ -134,14 +134,14 @@ struct CorkApp: App
|
|||
if let demoActivatedAt
|
||||
{
|
||||
let timeDemoWillRunOutAt: Date = demoActivatedAt + AppConstants.demoLengthInSeconds
|
||||
|
||||
|
||||
AppConstants.logger.debug("There is \(demoActivatedAt.timeIntervalSinceNow.formatted()) to go on the demo")
|
||||
|
||||
|
||||
AppConstants.logger.debug("Demo will time out at \(timeDemoWillRunOutAt.formatted(date: .complete, time: .complete))")
|
||||
|
||||
|
||||
if ((demoActivatedAt.timeIntervalSinceNow) + AppConstants.demoLengthInSeconds) > 0
|
||||
{ // Check if there is still time on the demo
|
||||
/// do stuff if there is
|
||||
/// do stuff if there is
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -226,9 +226,8 @@ struct CorkApp: App
|
|||
}
|
||||
.onChange(of: outdatedPackageTracker.displayableOutdatedPackages.count)
|
||||
{ outdatedPackageCount in
|
||||
|
||||
AppConstants.logger.debug("Number of displayable outdated packages changed (\(outdatedPackageCount))")
|
||||
|
||||
|
||||
// TODO: Remove this once I figure out why the updating spinner sometimes doesn't disappear
|
||||
withAnimation
|
||||
{
|
||||
|
|
@ -443,29 +442,27 @@ struct CorkApp: App
|
|||
ButtonThatOpensWebsites(
|
||||
websiteURL: URL(string: "https://forum.rikidar.eu/forumdisplay.php?fid=8")!, buttonText: "actiton.report-bugs.forum"
|
||||
)
|
||||
|
||||
|
||||
/*
|
||||
Button
|
||||
{
|
||||
let emailSubject = "Cork Error Report: v\(NSApplication.appVersion!)-\(NSApplication.buildVersion!)"
|
||||
let emailBody = "This is what went wrong:\n\nThis is what I expected to happen:\n\nDid Cork crash?"
|
||||
Button
|
||||
{
|
||||
let emailSubject = "Cork Error Report: v\(NSApplication.appVersion!)-\(NSApplication.buildVersion!)"
|
||||
let emailBody = "This is what went wrong:\n\nThis is what I expected to happen:\n\nDid Cork crash?"
|
||||
|
||||
let emailService = NSSharingService(named: NSSharingService.Name.composeEmail)
|
||||
emailService?.recipients = ["bug-reporting@corkmac.app"]
|
||||
emailService?.subject = emailSubject
|
||||
emailService?.perform(withItems: [emailBody])
|
||||
let emailService = NSSharingService(named: NSSharingService.Name.composeEmail)
|
||||
emailService?.recipients = ["bug-reporting@corkmac.app"]
|
||||
emailService?.subject = emailSubject
|
||||
emailService?.perform(withItems: [emailBody])
|
||||
|
||||
} label: {
|
||||
Text("action.report-bugs.email")
|
||||
}
|
||||
*/
|
||||
} label: {
|
||||
Text("action.report-bugs.email")
|
||||
}
|
||||
*/
|
||||
|
||||
} label: {
|
||||
Text("action.report-bugs.menu-category")
|
||||
}
|
||||
|
||||
|
||||
|
||||
Divider()
|
||||
#endif
|
||||
}
|
||||
|
|
@ -519,9 +516,8 @@ struct CorkApp: App
|
|||
}
|
||||
catch let brewfileExportError as BrewfileDumpingError
|
||||
{
|
||||
|
||||
AppConstants.logger.error("\(brewfileExportError)")
|
||||
|
||||
|
||||
switch brewfileExportError
|
||||
{
|
||||
case .couldNotDetermineWorkingDirectory:
|
||||
|
|
@ -567,7 +563,7 @@ struct CorkApp: App
|
|||
catch let brewfileImportingError
|
||||
{
|
||||
AppConstants.logger.error("\(brewfileImportingError.localizedDescription, privacy: .public)")
|
||||
|
||||
|
||||
appDelegate.appState.showAlert(errorToShow: .malformedBrewfile)
|
||||
|
||||
appDelegate.appState.isShowingBrewfileImportProgress = false
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ enum DisplayableAlert: LocalizedError
|
|||
case couldNotSynchronizePackages(error: String)
|
||||
|
||||
// MARK: - Brewfile exporting/importing
|
||||
|
||||
case couldNotGetWorkingDirectory, couldNotDumpBrewfile(error: String), couldNotReadBrewfile
|
||||
case couldNotGetBrewfileLocation, couldNotImportBrewfile, malformedBrewfile
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ extension DisplayableAlert
|
|||
return String(localized: "alert.could-not-find-package-in-parent-directory.title")
|
||||
case .fatalPackageInstallationError:
|
||||
return String(localized: "alert.fatal-installation.error")
|
||||
case .couldNotSynchronizePackages(_):
|
||||
case .couldNotSynchronizePackages:
|
||||
return String(localized: "alert.fatal.could-not-synchronize-packages.title")
|
||||
case .couldNotGetWorkingDirectory:
|
||||
return String(localized: "alert.could-not-get-brewfile-working-directory.title")
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ extension ServicesFatalError
|
|||
{
|
||||
switch self
|
||||
{
|
||||
case .couldNotLoadServices:
|
||||
return String(localized: "services.error.could-not-load-services")
|
||||
case .homebrewOutdated:
|
||||
return String(localized: "services.error.homebrew-outdated")
|
||||
case let .couldNotStartService(offendingService, _):
|
||||
return String(localized: "services.error.could-not-start-service.\(offendingService)")
|
||||
case let .couldNotStopService(offendingService, _):
|
||||
return String(localized: "services.error.could-not-stop-service.\(offendingService)")
|
||||
case .couldNotSynchronizeServices:
|
||||
return String(localized: "services.error.could-not-synchronize-services")
|
||||
case .couldNotLoadServices:
|
||||
return String(localized: "services.error.could-not-load-services")
|
||||
case .homebrewOutdated:
|
||||
return String(localized: "services.error.homebrew-outdated")
|
||||
case .couldNotStartService(let offendingService, _):
|
||||
return String(localized: "services.error.could-not-start-service.\(offendingService)")
|
||||
case .couldNotStopService(let offendingService, _):
|
||||
return String(localized: "services.error.could-not-stop-service.\(offendingService)")
|
||||
case .couldNotSynchronizeServices:
|
||||
return String(localized: "services.error.could-not-synchronize-services")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,21 +10,25 @@ import SwiftUI
|
|||
|
||||
enum DiscoverabilityDaySpans: Int, Hashable, Identifiable, CaseIterable
|
||||
{
|
||||
var id: Self { self }
|
||||
|
||||
var id: Self
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
case month = 30
|
||||
case quarterYear = 90
|
||||
case year = 365
|
||||
|
||||
|
||||
var key: LocalizedStringKey
|
||||
{
|
||||
switch self {
|
||||
case .month:
|
||||
return "settings.discoverability.time-span.month"
|
||||
case .quarterYear:
|
||||
return "settings.discoverability.time-span.quarter-year"
|
||||
case .year:
|
||||
return "settings.discoverability.time-span.year"
|
||||
switch self
|
||||
{
|
||||
case .month:
|
||||
return "settings.discoverability.time-span.month"
|
||||
case .quarterYear:
|
||||
return "settings.discoverability.time-span.quarter-year"
|
||||
case .year:
|
||||
return "settings.discoverability.time-span.year"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,23 @@ import SwiftUI
|
|||
|
||||
enum TopPackageSorting: Int, Hashable, Identifiable, CaseIterable
|
||||
{
|
||||
var id: Self { self }
|
||||
|
||||
var id: Self
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
case mostDownloads, fewestDownloads, random
|
||||
|
||||
|
||||
var key: LocalizedStringKey
|
||||
{
|
||||
switch self {
|
||||
case .mostDownloads:
|
||||
return "settings.discoverability.sorting.by-most-downloads"
|
||||
case .fewestDownloads:
|
||||
return "settings.discoverability.sorting.by-fewest-downloads"
|
||||
case .random:
|
||||
return "settings.discoverability.sorting.random"
|
||||
switch self
|
||||
{
|
||||
case .mostDownloads:
|
||||
return "settings.discoverability.sorting.by-most-downloads"
|
||||
case .fewestDownloads:
|
||||
return "settings.discoverability.sorting.by-fewest-downloads"
|
||||
case .random:
|
||||
return "settings.discoverability.sorting.random"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import Foundation
|
|||
enum LicensingState
|
||||
{
|
||||
case notBoughtOrHasNotActivatedDemo
|
||||
|
||||
|
||||
case demo
|
||||
case bought
|
||||
|
||||
|
||||
case selfCompiled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import Foundation
|
|||
|
||||
enum OutdatedPackageInfoAmount: String, Identifiable, Codable, CaseIterable
|
||||
{
|
||||
var id: Self { self }
|
||||
|
||||
var id: Self
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
case none, versionOnly, all
|
||||
|
||||
var localizedName: String
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by David Bureš on 17.05.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Charts
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum CachedDownloadType: String, CustomStringConvertible, Plottable
|
||||
|
|
@ -15,33 +15,34 @@ enum CachedDownloadType: String, CustomStringConvertible, Plottable
|
|||
case cask
|
||||
case other
|
||||
case unknown
|
||||
|
||||
|
||||
var description: String
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .formula:
|
||||
return String(localized: "package-details.type.formula")
|
||||
case .cask:
|
||||
return String(localized: "package-details.type.cask")
|
||||
case .other:
|
||||
return String(localized: "start-page.cached-downloads.graph.other-smaller-packages")
|
||||
default:
|
||||
return String(localized: "cached-downloads.type.unknown")
|
||||
case .formula:
|
||||
return String(localized: "package-details.type.formula")
|
||||
case .cask:
|
||||
return String(localized: "package-details.type.cask")
|
||||
case .other:
|
||||
return String(localized: "start-page.cached-downloads.graph.other-smaller-packages")
|
||||
default:
|
||||
return String(localized: "cached-downloads.type.unknown")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var color: Color
|
||||
{
|
||||
switch self {
|
||||
case .formula:
|
||||
return .purple
|
||||
case .cask:
|
||||
return .orange
|
||||
case .other:
|
||||
return .mint
|
||||
default:
|
||||
return .gray
|
||||
switch self
|
||||
{
|
||||
case .formula:
|
||||
return .purple
|
||||
case .cask:
|
||||
return .orange
|
||||
case .other:
|
||||
return .mint
|
||||
default:
|
||||
return .gray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,35 +5,36 @@
|
|||
// Created by David Bureš on 05.02.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Charts
|
||||
import AppIntents
|
||||
import Charts
|
||||
import Foundation
|
||||
|
||||
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity
|
||||
{
|
||||
case formula
|
||||
case cask
|
||||
|
||||
|
||||
var description: String
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .formula:
|
||||
return String(localized: "package-details.type.formula")
|
||||
case .cask:
|
||||
return String(localized: "package-details.type.cask")
|
||||
case .formula:
|
||||
return String(localized: "package-details.type.formula")
|
||||
case .cask:
|
||||
return String(localized: "package-details.type.cask")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "package-details.type")
|
||||
|
||||
|
||||
var displayRepresentation: DisplayRepresentation
|
||||
{
|
||||
switch self {
|
||||
case .formula:
|
||||
DisplayRepresentation(title: "package-details.type.formula")
|
||||
case .cask:
|
||||
DisplayRepresentation(title: "package-details.type.cask")
|
||||
switch self
|
||||
{
|
||||
case .formula:
|
||||
DisplayRepresentation(title: "package-details.type.formula")
|
||||
case .cask:
|
||||
DisplayRepresentation(title: "package-details.type.cask")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ enum ServiceStatus: Codable, Hashable, CustomStringConvertible
|
|||
|
||||
init(from decoder: Decoder) throws
|
||||
{
|
||||
let container = try decoder.singleValueContainer()
|
||||
let rawValue = try container.decode(String.self)
|
||||
let container: SingleValueDecodingContainer = try decoder.singleValueContainer()
|
||||
let rawValue: String = try container.decode(String.self)
|
||||
|
||||
switch rawValue
|
||||
{
|
||||
|
|
@ -66,7 +66,7 @@ enum ServiceStatus: Codable, Hashable, CustomStringConvertible
|
|||
|
||||
func encode(to encoder: Encoder) throws
|
||||
{
|
||||
var container = encoder.singleValueContainer()
|
||||
var container: SingleValueEncodingContainer = encoder.singleValueContainer()
|
||||
try container.encode(description)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,15 @@ import Foundation
|
|||
enum JSONParsingError: LocalizedError
|
||||
{
|
||||
case couldNotConvertStringToData(failureReason: String?), couldNotDecode(failureReason: String)
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .couldNotConvertStringToData(let failureReason):
|
||||
return String(localized: "error.json-parsing.could-not-convert-string-to-data.\(failureReason ?? "")")
|
||||
case .couldNotDecode(let failureReason):
|
||||
return String(localized: "error.json-parsing.could-not-decode.\(failureReason)")
|
||||
switch self
|
||||
{
|
||||
case .couldNotConvertStringToData(let failureReason):
|
||||
return String(localized: "error.json-parsing.could-not-convert-string-to-data.\(failureReason ?? "")")
|
||||
case .couldNotDecode(let failureReason):
|
||||
return String(localized: "error.json-parsing.could-not-decode.\(failureReason)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
|
||||
extension String
|
||||
{
|
||||
static let mainWindowID = "main"
|
||||
static let servicesWindowID = "services"
|
||||
static let aboutWindowID = "about"
|
||||
static let mainWindowID: String = "main"
|
||||
static let servicesWindowID: String = "services"
|
||||
static let aboutWindowID: String = "about"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@
|
|||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
extension UTType {
|
||||
static var homebrewBackup: UTType {
|
||||
extension UTType
|
||||
{
|
||||
static var homebrewBackup: UTType
|
||||
{
|
||||
UTType(exportedAs: "com.davidbures.homebrew-backup")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,30 +12,31 @@ struct GetInstalledCasksIntent: AppIntent
|
|||
{
|
||||
static var title: LocalizedStringResource = "intent.get-installed-casks.title"
|
||||
static var description: LocalizedStringResource = "intent.get-installed-casks.description"
|
||||
|
||||
|
||||
static var isDiscoverable: Bool = true
|
||||
static var openAppWhenRun: Bool = false
|
||||
|
||||
|
||||
func perform() async throws -> some ReturnsValue<[MinimalHomebrewPackage]>
|
||||
{
|
||||
let allowAccessToFile = AppConstants.brewCaskPath.startAccessingSecurityScopedResource()
|
||||
|
||||
let allowAccessToFile: Bool = AppConstants.brewCaskPath.startAccessingSecurityScopedResource()
|
||||
|
||||
if allowAccessToFile
|
||||
{
|
||||
let installedFormulae = await loadUpPackages(whatToLoad: .cask, appState: AppState())
|
||||
|
||||
let installedFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .cask, appState: AppState())
|
||||
|
||||
AppConstants.brewCaskPath.stopAccessingSecurityScopedResource()
|
||||
|
||||
let minimalPackages: [MinimalHomebrewPackage] = installedFormulae.map { package in
|
||||
return .init(name: package.name, type: .cask, installDate: package.installedOn, installedIntentionally: true)
|
||||
|
||||
let minimalPackages: [MinimalHomebrewPackage] = installedFormulae.map
|
||||
{ package in
|
||||
.init(name: package.name, type: .cask, installDate: package.installedOn, installedIntentionally: true)
|
||||
}
|
||||
|
||||
|
||||
return .result(value: minimalPackages)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Could not obtain access to folder")
|
||||
|
||||
|
||||
throw FolderAccessingError.couldNotObtainPermissionToAccessFolder(formattedPath: AppConstants.brewCaskPath.absoluteString)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ struct GetInstalledFormulaeIntent: AppIntent
|
|||
|
||||
if getOnlyManuallyInstalledPackages
|
||||
{
|
||||
minimalPackages = minimalPackages.filter { $0.installedIntentionally }
|
||||
minimalPackages = minimalPackages.filter({ $0.installedIntentionally })
|
||||
}
|
||||
|
||||
return .result(value: minimalPackages)
|
||||
|
|
|
|||
|
|
@ -12,20 +12,19 @@ struct GetInstalledPackagesIntent: AppIntent
|
|||
{
|
||||
@Parameter(title: "intent.get-installed-packages.limit-to-manually-installed-packages")
|
||||
var getOnlyManuallyInstalledPackages: Bool
|
||||
|
||||
|
||||
static var title: LocalizedStringResource = "intent.get-installed-packages.title"
|
||||
static var description: LocalizedStringResource = "intent.get-installed-packages.description"
|
||||
|
||||
|
||||
static var isDiscoverable: Bool = true
|
||||
static var openAppWhenRun: Bool = false
|
||||
|
||||
|
||||
func perform() async throws -> some ReturnsValue<[MinimalHomebrewPackage]>
|
||||
{
|
||||
let installedMinimalFormulae: [MinimalHomebrewPackage] = try await GetInstalledFormulaeIntent(getOnlyManuallyInstalledPackages: $getOnlyManuallyInstalledPackages).perform().value ?? .init()
|
||||
|
||||
|
||||
let installedMinimalCasks: [MinimalHomebrewPackage] = try await GetInstalledCasksIntent().perform().value ?? .init()
|
||||
|
||||
|
||||
return .result(value: installedMinimalFormulae + installedMinimalCasks)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ enum RefreshIntentResult: String, AppEnum
|
|||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "intent.refresh.result.display-representation")
|
||||
|
||||
static var caseDisplayRepresentations: [RefreshIntentResult : DisplayRepresentation] = [
|
||||
static var caseDisplayRepresentations: [RefreshIntentResult: DisplayRepresentation] = [
|
||||
.refreshed: DisplayRepresentation(title: "intent.refresh.result.refreshed"),
|
||||
.refreshedWithErrors: DisplayRepresentation(title: "intent.refresh.result.refreshed-with-errors"),
|
||||
.failed: DisplayRepresentation(title: "intent.refresh.result.failed")
|
||||
|
|
@ -36,8 +36,8 @@ struct RefreshPackagesIntent: AppIntent
|
|||
let refreshCommandResult: TerminalOutput = await shell(AppConstants.brewExecutablePath, ["update"])
|
||||
|
||||
var refreshErrorWithoutBuggedHomebrewMessages: [String] = refreshCommandResult.standardError.components(separatedBy: "\n")
|
||||
refreshErrorWithoutBuggedHomebrewMessages = refreshErrorWithoutBuggedHomebrewMessages.filter({ !$0.contains("Updating Homebrew") })
|
||||
|
||||
refreshErrorWithoutBuggedHomebrewMessages = refreshErrorWithoutBuggedHomebrewMessages.filter { !$0.contains("Updating Homebrew") }
|
||||
|
||||
if !refreshErrorWithoutBuggedHomebrewMessages.isEmpty
|
||||
{
|
||||
return .result(value: .refreshedWithErrors)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by David Bureš on 26.05.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppIntents
|
||||
import Foundation
|
||||
|
||||
struct CorkShortcuts: AppShortcutsProvider
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,10 +30,9 @@ extension TopPackagesTracker
|
|||
func loadTopPackages(numberOfDays: Int = 30, appState: AppState) async
|
||||
{
|
||||
/// The magic number here is the result of 1000/30, a base limit for 30 days: If the user selects the number of days to be 30, only show packages with more than 1000 downloads
|
||||
let packageDownloadsCutoff: Int = 33 * numberOfDays
|
||||
let packageDownloadsCutoff = 33 * numberOfDays
|
||||
|
||||
let decoder: JSONDecoder =
|
||||
{
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ private extension URL
|
|||
/// e.g. only actual package URLs
|
||||
var validPackageURLs: [String]?
|
||||
{
|
||||
let items: [String]? = try? FileManager.default.contentsOfDirectory(atPath: self.path).filter { !$0.hasPrefix(".") }.filter
|
||||
let items: [String]? = try? FileManager.default.contentsOfDirectory(atPath: path).filter { !$0.hasPrefix(".") }.filter
|
||||
{ item in
|
||||
/// Filter out all symlinks from the folder
|
||||
let completeURLtoItem: URL = self.appendingPathComponent(item, conformingTo: .folder)
|
||||
|
|
@ -147,24 +147,26 @@ private extension URL
|
|||
guard let localPackagePath = versionURLs.first
|
||||
else
|
||||
{
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-load-version-to-check-from-available-versions"))
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-load-version-to-check-from-available-versions"))
|
||||
}
|
||||
|
||||
guard localPackagePath.lastPathComponent != "Cellar" else
|
||||
|
||||
guard localPackagePath.lastPathComponent != "Cellar"
|
||||
else
|
||||
{
|
||||
AppConstants.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(localPackagePath)")
|
||||
|
||||
|
||||
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.formulae"))
|
||||
}
|
||||
|
||||
guard localPackagePath.lastPathComponent != "Caskroom" else
|
||||
|
||||
guard localPackagePath.lastPathComponent != "Caskroom"
|
||||
else
|
||||
{
|
||||
AppConstants.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(localPackagePath)")
|
||||
|
||||
|
||||
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.casks"))
|
||||
}
|
||||
|
||||
if self.path.contains("Cellar")
|
||||
if path.contains("Cellar")
|
||||
{
|
||||
let localPackageInfoJSONPath = localPackagePath.appendingPathComponent("INSTALL_RECEIPT.json", conformingTo: .json)
|
||||
if FileManager.default.fileExists(atPath: localPackageInfoJSONPath.path)
|
||||
|
|
@ -174,8 +176,7 @@ private extension URL
|
|||
let installedOnRequest: Bool
|
||||
}
|
||||
|
||||
let decoder: JSONDecoder =
|
||||
{
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
|
@ -194,7 +195,7 @@ private extension URL
|
|||
{
|
||||
AppConstants.logger.error("Failed to decode install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptParsingError.localizedDescription, privacy: .public)")
|
||||
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized:"error.package-loading.could-not-decode-installa-receipt-\(installReceiptParsingError.localizedDescription)"))
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-decode-installa-receipt-\(installReceiptParsingError.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
catch let installReceiptLoadingError
|
||||
|
|
@ -213,7 +214,7 @@ private extension URL
|
|||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.missing-install-receipt"))
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.missing-install-receipt"))
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -221,20 +222,20 @@ private extension URL
|
|||
}
|
||||
}
|
||||
}
|
||||
else if self.path.contains("Caskroom")
|
||||
else if path.contains("Caskroom")
|
||||
{
|
||||
return true
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.unexpected-folder-name"))
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.unexpected-folder-name"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine a package's type type from its URL
|
||||
var packageType: PackageType
|
||||
{
|
||||
if self.path.contains("Cellar")
|
||||
if path.contains("Cellar")
|
||||
{
|
||||
return .formula
|
||||
}
|
||||
|
|
@ -265,7 +266,7 @@ private extension URL
|
|||
}
|
||||
catch
|
||||
{
|
||||
AppConstants.logger.error("Failed while loading version for package \(self.lastPathComponent, privacy: .public) at URL \(self, privacy: .public)")
|
||||
AppConstants.logger.error("Failed while loading version for package \(lastPathComponent, privacy: .public) at URL \(self, privacy: .public)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -277,7 +278,7 @@ extension [URL]
|
|||
/// Returns an array of versions from an array of URLs to available versions
|
||||
var versions: [String]
|
||||
{
|
||||
return self.map
|
||||
return map
|
||||
{ versionURL in
|
||||
versionURL.lastPathComponent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ extension URL
|
|||
{
|
||||
do
|
||||
{
|
||||
let fileAttributes = try self.resourceValues(forKeys: [.isSymbolicLinkKey])
|
||||
|
||||
let fileAttributes = try resourceValues(forKeys: [.isSymbolicLinkKey])
|
||||
|
||||
return fileAttributes.isSymbolicLink
|
||||
}
|
||||
catch let symlinkCheckingError
|
||||
{
|
||||
AppConstants.logger.error("Error checking if \(self) is symlink: \(symlinkCheckingError)")
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ extension URL
|
|||
{
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
var size: Int64 = 0
|
||||
|
||||
|
||||
for url in contents
|
||||
{
|
||||
let isDirectoryResourceValue: URLResourceValues
|
||||
|
|
@ -34,7 +34,7 @@ extension URL
|
|||
{
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if isDirectoryResourceValue.isDirectory == true
|
||||
{
|
||||
size += url.directorySize
|
||||
|
|
@ -50,11 +50,11 @@ extension URL
|
|||
{
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
size += Int64(fileSizeResourceValue.fileSize ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by David Bureš on 01.10.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
extension String
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,19 +10,23 @@ import Foundation
|
|||
enum RegexError: LocalizedError
|
||||
{
|
||||
case regexFunctionCouldNotMatchAnything
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .regexFunctionCouldNotMatchAnything:
|
||||
return String(localized: "error.regex.nothing-matched")
|
||||
switch self
|
||||
{
|
||||
case .regexFunctionCouldNotMatchAnything:
|
||||
return String(localized: "error.regex.nothing-matched")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func regexMatch(from string: String, regex: String) throws -> String
|
||||
{
|
||||
guard let matchedRange = string.range(of: regex, options: .regularExpression) else { throw RegexError.regexFunctionCouldNotMatchAnything }
|
||||
|
||||
guard let matchedRange = string.range(of: regex, options: .regularExpression) else
|
||||
{
|
||||
throw RegexError.regexFunctionCouldNotMatchAnything
|
||||
}
|
||||
|
||||
return String(string[matchedRange])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by David Bureš on 30.03.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
func switchCorkToForeground()
|
||||
{
|
||||
|
|
@ -17,7 +17,7 @@ func switchCorkToForeground()
|
|||
else
|
||||
{
|
||||
let runningApps: [NSRunningApplication] = NSWorkspace.shared.runningApplications
|
||||
|
||||
|
||||
for app in runningApps
|
||||
{
|
||||
if app.localizedName == "Cork"
|
||||
|
|
@ -29,4 +29,4 @@ func switchCorkToForeground()
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// What this function does:
|
||||
/// **Background**
|
||||
/// When a package is installed or uninstalled, its dependencies don't show up in the package list, because there's no system for getting them. I needed a way for all new packages to show up/disappear even when they were not installed manually
|
||||
|
|
@ -32,10 +31,9 @@ import SwiftUI
|
|||
/// 4. In the second for loop, check each package in the old array against each package in the final *mutable* array. If the names of packages match, remove them; if they match, it means that this package has been removed
|
||||
|
||||
@MainActor
|
||||
func synchronizeInstalledPackages(brewData: BrewDataStorage) async -> Void
|
||||
func synchronizeInstalledPackages(brewData: BrewDataStorage) async
|
||||
{
|
||||
|
||||
let dummyAppState: AppState = AppState()
|
||||
let dummyAppState = AppState()
|
||||
dummyAppState.isLoadingFormulae = false
|
||||
dummyAppState.isLoadingCasks = false
|
||||
|
||||
|
|
@ -45,7 +43,7 @@ func synchronizeInstalledPackages(brewData: BrewDataStorage) async -> Void
|
|||
|
||||
if newFormulae.count != brewData.installedFormulae.count
|
||||
{
|
||||
withAnimation
|
||||
withAnimation
|
||||
{
|
||||
brewData.installedFormulae = newFormulae
|
||||
}
|
||||
|
|
@ -53,7 +51,7 @@ func synchronizeInstalledPackages(brewData: BrewDataStorage) async -> Void
|
|||
|
||||
if newCasks.count != brewData.installedCasks.count
|
||||
{
|
||||
withAnimation
|
||||
withAnimation
|
||||
{
|
||||
brewData.installedCasks = newCasks
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func loadUpTappedTaps() async -> [BrewTap]
|
|||
return finalAvailableTaps
|
||||
}
|
||||
|
||||
private func checkIfTapIsAdded(tapToCheck: String) async -> Bool
|
||||
private func checkIfTapIsAdded(tapToCheck _: String) async -> Bool
|
||||
{
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
func deleteCachedDownloads() -> Void
|
||||
func deleteCachedDownloads()
|
||||
{
|
||||
/// This folder has the symlinks, so we have do **delete ONLY THE SYMLINKS**
|
||||
for url in getContentsOfFolder(targetFolder: AppConstants.brewCachedFormulaeDownloadsPath)
|
||||
|
|
@ -28,7 +28,7 @@ func deleteCachedDownloads() -> Void
|
|||
AppConstants.logger.warning("Could not check symlink status of \(url)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// This folder has the symlinks, so we have to **delete ONLY THE SYMLINKS**
|
||||
for url in getContentsOfFolder(targetFolder: AppConstants.brewCachedCasksDownloadsPath)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/*enum CachePurgeError: Error
|
||||
{
|
||||
case standardErrorNotEmpty
|
||||
}*/
|
||||
/* enum CachePurgeError: Error
|
||||
{
|
||||
case standardErrorNotEmpty
|
||||
} */
|
||||
|
||||
func purgeBrewCache() async throws -> TerminalOutput
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,14 +10,15 @@ import Foundation
|
|||
enum HomebrewCachePurgeError: LocalizedError
|
||||
{
|
||||
case purgingCommandFailed, regexMatchingCouldNotMatchAnything
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .purgingCommandFailed:
|
||||
return String(localized: "error.maintenance.cache-purging.command-failed")
|
||||
case .regexMatchingCouldNotMatchAnything:
|
||||
return String(localized: "error.regex.nothing-matched")
|
||||
switch self
|
||||
{
|
||||
case .purgingCommandFailed:
|
||||
return String(localized: "error.maintenance.cache-purging.command-failed")
|
||||
case .regexMatchingCouldNotMatchAnything:
|
||||
return String(localized: "error.regex.nothing-matched")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +47,8 @@ func purgeHomebrewCacheUtility() async throws -> [String]
|
|||
|
||||
let packageHoldingBackCachePurgeNameRegex = "(?<=Skipping ).*?(?=:)"
|
||||
|
||||
guard let packageHoldingBackCachePurgeName = try? regexMatch(from: blockingPackageRaw, regex: packageHoldingBackCachePurgeNameRegex) else
|
||||
guard let packageHoldingBackCachePurgeName = try? regexMatch(from: blockingPackageRaw, regex: packageHoldingBackCachePurgeNameRegex)
|
||||
else
|
||||
{
|
||||
throw HomebrewCachePurgeError.regexMatchingCouldNotMatchAnything
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ enum OrphanRemovalError: LocalizedError
|
|||
switch self
|
||||
{
|
||||
case .couldNotUninstallOrphans(let output):
|
||||
return String(localized: "error.maintenance.orphan-removal.could-not-uninstall-orphans.\(output)")
|
||||
return String(localized: "error.maintenance.orphan-removal.could-not-uninstall-orphans.\(output)")
|
||||
case .couldNotGetNumberOfUninstalledOrphans:
|
||||
return String(localized: "error.maintenance.orphan-removal.could-not-get-number-of-uninstalled-orphans")
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ func uninstallOrphansUtility() async throws -> Int
|
|||
}
|
||||
else
|
||||
{
|
||||
let numberOfUninstalledOrphansRegex: String = "(?<=Autoremoving ).*?(?= unneeded)"
|
||||
let numberOfUninstalledOrphansRegex = "(?<=Autoremoving ).*?(?= unneeded)"
|
||||
|
||||
guard let numberOfRemovedOrphans = try Int(regexMatch(from: orphanUninstallationOutput.standardOutput, regex: numberOfUninstalledOrphansRegex))
|
||||
else
|
||||
|
|
|
|||
|
|
@ -9,32 +9,32 @@ import Foundation
|
|||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
func sendNotification(title: String, body: String? = nil, subtitle: String? = nil, sensitivity: UNNotificationInterruptionLevel = .timeSensitive) -> Void
|
||||
func sendNotification(title: String, body: String? = nil, subtitle: String? = nil, sensitivity: UNNotificationInterruptionLevel = .timeSensitive)
|
||||
{
|
||||
// Get whether we can send notifications
|
||||
let notificationsAreEnabled = UserDefaults.standard.bool(forKey: "areNotificationsEnabled")
|
||||
|
||||
|
||||
if notificationsAreEnabled
|
||||
{
|
||||
let notification = UNMutableNotificationContent()
|
||||
|
||||
|
||||
notification.title = title
|
||||
|
||||
|
||||
if let body
|
||||
{
|
||||
notification.body = body
|
||||
}
|
||||
|
||||
|
||||
if let subtitle
|
||||
{
|
||||
notification.subtitle = subtitle
|
||||
}
|
||||
|
||||
|
||||
notification.sound = .default
|
||||
notification.interruptionLevel = sensitivity
|
||||
|
||||
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notification, trigger: nil)
|
||||
|
||||
|
||||
AppConstants.notificationCenter.add(request)
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
// Created by David Bureš on 19.11.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
func openTerminal()
|
||||
{
|
||||
guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Terminal") else { return }
|
||||
|
||||
|
||||
let path = "/bin"
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
configuration.arguments = [path]
|
||||
|
|
|
|||
|
|
@ -62,10 +62,10 @@ extension OutdatedPackageTracker
|
|||
{
|
||||
// First, we have to pull the newest updates
|
||||
await shell(AppConstants.brewExecutablePath, ["update"])
|
||||
|
||||
|
||||
// Then we can get the updating under way
|
||||
let rawOutput: TerminalOutput = await shell(AppConstants.brewExecutablePath, ["outdated", "--json=v2"])
|
||||
|
||||
|
||||
print("Outdated package function oputput: \(rawOutput)")
|
||||
|
||||
// MARK: - Error checking
|
||||
|
|
@ -84,8 +84,7 @@ extension OutdatedPackageTracker
|
|||
|
||||
// MARK: - Decoding
|
||||
|
||||
let outdatedPackagesOutputDecoder: JSONDecoder =
|
||||
{
|
||||
let outdatedPackagesOutputDecoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
|
@ -108,7 +107,7 @@ extension OutdatedPackageTracker
|
|||
async let finalOutdatedFormulae: Set<OutdatedPackage> = await getOutdatedFormulae(from: rawDecodedOutdatedPackages.formulae, brewData: brewData)
|
||||
async let finalOutdatedCasks: Set<OutdatedPackage> = await getOutdatedCasks(from: rawDecodedOutdatedPackages.casks, brewData: brewData)
|
||||
|
||||
self.outdatedPackages = await finalOutdatedFormulae.union(finalOutdatedCasks)
|
||||
outdatedPackages = await finalOutdatedFormulae.union(finalOutdatedCasks)
|
||||
}
|
||||
catch let decodingError
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ extension BrewPackage
|
|||
private struct PackageCommandOutput: Codable
|
||||
{
|
||||
// MARK: - Formulae
|
||||
|
||||
struct Formulae: Codable
|
||||
{
|
||||
/// Name of the formula
|
||||
|
|
@ -79,7 +80,7 @@ extension BrewPackage
|
|||
|
||||
/// Various info about the installation
|
||||
let installed: [Installed]
|
||||
|
||||
|
||||
struct Bottle: Codable
|
||||
{
|
||||
struct Stable: Codable
|
||||
|
|
@ -90,15 +91,15 @@ extension BrewPackage
|
|||
let url: URL
|
||||
let sha256: String
|
||||
}
|
||||
|
||||
|
||||
/// The individual files
|
||||
let files: [String: FileInfo]
|
||||
}
|
||||
|
||||
|
||||
/// The stable files
|
||||
let stable: Stable
|
||||
}
|
||||
|
||||
|
||||
/// Info about the relevant files
|
||||
let bottle: Bottle
|
||||
|
||||
|
|
@ -109,9 +110,10 @@ extension BrewPackage
|
|||
let pinned: Bool
|
||||
|
||||
// MARK: - Formuale functions
|
||||
|
||||
func extractDependencies() -> [BrewPackageDependency]?
|
||||
{
|
||||
let allDependencies = self.installed.flatMap
|
||||
let allDependencies = installed.flatMap
|
||||
{ installed in
|
||||
installed.runtimeDependencies ?? []
|
||||
}
|
||||
|
|
@ -126,24 +128,25 @@ extension BrewPackage
|
|||
.init(name: dependency.fullName, version: dependency.version, directlyDeclared: dependency.declaredDirectly)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getCompatibility() -> Bool
|
||||
{
|
||||
for compatibleSystem in self.bottle.stable.files.keys
|
||||
for compatibleSystem in bottle.stable.files.keys
|
||||
{
|
||||
if compatibleSystem.contains(AppConstants.osVersionString.lookupName)
|
||||
{
|
||||
AppConstants.logger.debug("Package \(self.name) is compatible")
|
||||
AppConstants.logger.debug("Package \(name) is compatible")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
AppConstants.logger.debug("Package \(self.name) is NOT compatible")
|
||||
|
||||
AppConstants.logger.debug("Package \(name) is NOT compatible")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Casks
|
||||
|
||||
struct Casks: Codable
|
||||
{
|
||||
/// Name of the cask
|
||||
|
|
@ -175,7 +178,7 @@ extension BrewPackage
|
|||
/// Extract dependencies from the struct so they can be used
|
||||
func extractDependencies() -> [BrewPackageDependency]?
|
||||
{
|
||||
guard let formulae = self.formulae
|
||||
guard let formulae = formulae
|
||||
else
|
||||
{
|
||||
return nil
|
||||
|
|
@ -205,8 +208,7 @@ extension BrewPackage
|
|||
@MainActor
|
||||
func loadDetails() async throws -> BrewPackageDetails
|
||||
{
|
||||
let decoder: JSONDecoder =
|
||||
{
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
|
@ -215,13 +217,13 @@ extension BrewPackage
|
|||
|
||||
var rawOutput: TerminalOutput?
|
||||
|
||||
switch self.type
|
||||
switch type
|
||||
{
|
||||
case .formula:
|
||||
rawOutput = await shell(AppConstants.brewExecutablePath, ["info", "--json=v2", self.name])
|
||||
rawOutput = await shell(AppConstants.brewExecutablePath, ["info", "--json=v2", name])
|
||||
|
||||
case .cask:
|
||||
rawOutput = await shell(AppConstants.brewExecutablePath, ["info", "--json=v2", "--cask", self.name])
|
||||
rawOutput = await shell(AppConstants.brewExecutablePath, ["info", "--json=v2", "--cask", name])
|
||||
}
|
||||
|
||||
// MARK: - Error checking
|
||||
|
|
@ -251,7 +253,7 @@ extension BrewPackage
|
|||
}
|
||||
|
||||
AppConstants.logger.debug("JSON output: \(rawOutput.standardOutput)")
|
||||
|
||||
|
||||
guard let decodableData: Data = rawOutput.standardOutput.data(using: .utf8, allowLossyConversion: false)
|
||||
else
|
||||
{
|
||||
|
|
@ -266,7 +268,7 @@ extension BrewPackage
|
|||
{
|
||||
let rawDecodedPackageInfo: PackageCommandOutput = try decoder.decode(PackageCommandOutput.self, from: decodableData)
|
||||
|
||||
switch self.type
|
||||
switch type
|
||||
{
|
||||
case .formula:
|
||||
guard let formulaInfo: PackageCommandOutput.Formulae = rawDecodedPackageInfo.formulae?.first
|
||||
|
|
@ -286,7 +288,7 @@ extension BrewPackage
|
|||
dependencies: formulaInfo.extractDependencies(),
|
||||
outdated: formulaInfo.outdated,
|
||||
caveats: formulaInfo.caveats,
|
||||
pinned: formulaInfo.pinned,
|
||||
pinned: formulaInfo.pinned,
|
||||
isCompatible: formulaInfo.getCompatibility()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
func pinAndUnpinPackage(package: BrewPackage, pinned: Bool) async -> Void
|
||||
func pinAndUnpinPackage(package: BrewPackage, pinned: Bool) async
|
||||
{
|
||||
if pinned
|
||||
{
|
||||
let pinResult = await shell(AppConstants.brewExecutablePath, ["pin", package.name])
|
||||
|
||||
|
||||
if !pinResult.standardError.isEmpty
|
||||
{
|
||||
AppConstants.logger.error("Error pinning: \(pinResult.standardError, privacy: .public)")
|
||||
|
|
|
|||
|
|
@ -7,39 +7,37 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
|
||||
private enum PackageRetrievalByUUIDError: LocalizedError
|
||||
{
|
||||
case couldNotfindAnypackagesInTracker
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .couldNotfindAnypackagesInTracker:
|
||||
return String(localized: "error.package-retrieval.uuid.could-not-find-any-packages-in-tracker")
|
||||
switch self
|
||||
{
|
||||
case .couldNotfindAnypackagesInTracker:
|
||||
return String(localized: "error.package-retrieval.uuid.could-not-find-any-packages-in-tracker")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPackageFromUUID(requestedPackageUUID: UUID, tracker: SearchResultTracker) throws -> BrewPackage
|
||||
{
|
||||
|
||||
var filteredPackage: BrewPackage?
|
||||
|
||||
|
||||
AppConstants.logger.log("Formula tracker: \(tracker.foundFormulae.count)")
|
||||
AppConstants.logger.log("Cask tracker: \(tracker.foundCasks.count)")
|
||||
|
||||
if tracker.foundFormulae.count != 0
|
||||
{
|
||||
filteredPackage = tracker.foundFormulae.filter({ $0.id == requestedPackageUUID }).first
|
||||
filteredPackage = tracker.foundFormulae.filter { $0.id == requestedPackageUUID }.first
|
||||
}
|
||||
|
||||
if filteredPackage == nil
|
||||
{
|
||||
filteredPackage = tracker.foundCasks.filter({ $0.id == requestedPackageUUID }).first
|
||||
filteredPackage = tracker.foundCasks.filter { $0.id == requestedPackageUUID }.first
|
||||
}
|
||||
|
||||
|
||||
if let filteredPackage
|
||||
{
|
||||
return filteredPackage
|
||||
|
|
@ -53,12 +51,13 @@ func getPackageFromUUID(requestedPackageUUID: UUID, tracker: SearchResultTracker
|
|||
enum TopPackageRetrievalError: LocalizedError
|
||||
{
|
||||
case resultingArrayWasEmptyEvenThoughPackagesWereInIt
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .resultingArrayWasEmptyEvenThoughPackagesWereInIt:
|
||||
return String(localized: "error.top-packages.impossible-error")
|
||||
switch self
|
||||
{
|
||||
case .resultingArrayWasEmptyEvenThoughPackagesWereInIt:
|
||||
return String(localized: "error.top-packages.impossible-error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,53 +67,55 @@ func getTopPackageFromUUID(requestedPackageUUID: UUID, packageType: PackageType,
|
|||
{
|
||||
if packageType == .formula
|
||||
{
|
||||
guard let foundTopFormula: TopPackage = topPackageTracker.topFormulae.filter({ $0.id == requestedPackageUUID }).first else
|
||||
guard let foundTopFormula: TopPackage = topPackageTracker.topFormulae.filter({ $0.id == requestedPackageUUID }).first
|
||||
else
|
||||
{
|
||||
throw TopPackageRetrievalError.resultingArrayWasEmptyEvenThoughPackagesWereInIt
|
||||
}
|
||||
|
||||
|
||||
return .init(name: foundTopFormula.packageName, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let foundTopCask: TopPackage = topPackageTracker.topCasks.filter({ $0.id == requestedPackageUUID }).first else
|
||||
guard let foundTopCask: TopPackage = topPackageTracker.topCasks.filter({ $0.id == requestedPackageUUID }).first
|
||||
else
|
||||
{
|
||||
throw TopPackageRetrievalError.resultingArrayWasEmptyEvenThoughPackagesWereInIt
|
||||
}
|
||||
|
||||
|
||||
return .init(name: foundTopCask.packageName, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func getPackageNamesFromUUID(selectionBinding: Set<UUID>, tracker: SearchResultTracker) -> [String]
|
||||
{
|
||||
let foundFormulae: [SearchResult] = tracker.foundFormulae
|
||||
let foundCasks: [SearchResult] = tracker.foundCasks
|
||||
func getPackageNamesFromUUID(selectionBinding: Set<UUID>, tracker: SearchResultTracker) -> [String]
|
||||
{
|
||||
let foundFormulae: [SearchResult] = tracker.foundFormulae
|
||||
let foundCasks: [SearchResult] = tracker.foundCasks
|
||||
|
||||
var resultArray = [String]()
|
||||
var resultArray = [String]()
|
||||
|
||||
for selection in selectionBinding
|
||||
{
|
||||
/// Step 1: Look through formulae
|
||||
for item in foundFormulae
|
||||
{
|
||||
if selection == item.id
|
||||
{
|
||||
resultArray.append(item.packageName)
|
||||
}
|
||||
}
|
||||
for selection in selectionBinding
|
||||
{
|
||||
/// Step 1: Look through formulae
|
||||
for item in foundFormulae
|
||||
{
|
||||
if selection == item.id
|
||||
{
|
||||
resultArray.append(item.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
/// Step 2: Look through casks
|
||||
for item in foundCasks
|
||||
{
|
||||
if selection == item.id
|
||||
{
|
||||
resultArray.append(item.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Step 2: Look through casks
|
||||
for item in foundCasks
|
||||
{
|
||||
if selection == item.id
|
||||
{
|
||||
resultArray.append(item.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultArray
|
||||
}
|
||||
*/
|
||||
return resultArray
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ func searchForPackage(packageName: String, packageType: PackageType) async throw
|
|||
}
|
||||
|
||||
finalPackageArray.removeLast()
|
||||
|
||||
|
||||
AppConstants.logger.info("Search found these packages: \(finalPackageArray, privacy: .auto)")
|
||||
|
||||
|
||||
return finalPackageArray
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,8 +54,7 @@ extension ServicesTracker
|
|||
/// Load services into the service tracker
|
||||
func loadServices() async throws
|
||||
{
|
||||
let decoder: JSONDecoder =
|
||||
{
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
|
@ -103,7 +102,7 @@ extension ServicesTracker
|
|||
))
|
||||
}
|
||||
|
||||
self.services = finalServices
|
||||
services = finalServices
|
||||
}
|
||||
catch let servicesParsingError
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,25 +13,25 @@ extension ServicesTracker
|
|||
{
|
||||
for await output in shell(AppConstants.brewExecutablePath, ["services", "kill", serviceToKill.name])
|
||||
{
|
||||
switch output
|
||||
switch output
|
||||
{
|
||||
case .standardOutput(let outputLine):
|
||||
AppConstants.logger.debug("Service killing output: \(outputLine)")
|
||||
case .standardError(let errorLine):
|
||||
AppConstants.logger.error("Service killing error: \(errorLine)")
|
||||
case .standardOutput(let outputLine):
|
||||
AppConstants.logger.debug("Service killing output: \(outputLine)")
|
||||
case .standardError(let errorLine):
|
||||
AppConstants.logger.error("Service killing error: \(errorLine)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
serviceModificationProgress.progress = 5.0
|
||||
|
||||
|
||||
try await synchronizeServices(preserveIDs: true)
|
||||
}
|
||||
catch let servicesSynchronizationError
|
||||
{
|
||||
AppConstants.logger.error("Could not synchronize services: \(servicesSynchronizationError.localizedDescription)")
|
||||
|
||||
|
||||
servicesState.showError(.couldNotSynchronizeServices(errorThrown: servicesSynchronizationError.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ extension ServicesTracker
|
|||
{
|
||||
switch output
|
||||
{
|
||||
case let .standardOutput(outputLine):
|
||||
case .standardOutput(let outputLine):
|
||||
AppConstants.logger.debug("Services startup output line: \(outputLine)")
|
||||
switch outputLine
|
||||
{
|
||||
|
|
@ -27,7 +27,7 @@ extension ServicesTracker
|
|||
|
||||
serviceModificationProgress.progress += 1
|
||||
|
||||
case let .standardError(errorLine):
|
||||
case .standardError(let errorLine):
|
||||
switch errorLine
|
||||
{
|
||||
case _ where errorLine.contains("must be run as root"):
|
||||
|
|
@ -40,14 +40,13 @@ extension ServicesTracker
|
|||
|
||||
servicesState.showError(.couldNotStartService(offendingService: serviceToStart.name, errorThrown: errorLine))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
serviceModificationProgress.progress = 5.0
|
||||
|
||||
|
||||
try await synchronizeServices(preserveIDs: true)
|
||||
}
|
||||
catch let servicesSynchronizationError
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ extension ServicesTracker
|
|||
func synchronizeServices(preserveIDs: Bool) async throws
|
||||
{
|
||||
do
|
||||
{
|
||||
{
|
||||
let dummyServicesTracker: ServicesTracker = .init()
|
||||
|
||||
|
||||
try await dummyServicesTracker.loadServices()
|
||||
|
||||
|
||||
let updatedServices: Set<HomebrewService> = dummyServicesTracker.services
|
||||
|
||||
if !preserveIDs
|
||||
|
|
@ -32,9 +32,8 @@ extension ServicesTracker
|
|||
|
||||
let updatedServicesWithOldIDs: Set<HomebrewService> = Set(updatedServices.map
|
||||
{ updatedService in
|
||||
|
||||
var copyUpdatedService = updatedService
|
||||
|
||||
|
||||
for originalServiceWithItsOldUUID in originalServicesWithTheirUUIDs
|
||||
{
|
||||
if originalServiceWithItsOldUUID.key == copyUpdatedService.name
|
||||
|
|
@ -42,10 +41,10 @@ extension ServicesTracker
|
|||
copyUpdatedService.id = originalServiceWithItsOldUUID.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return copyUpdatedService
|
||||
})
|
||||
|
||||
|
||||
services = updatedServicesWithOldIDs
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,47 +8,49 @@
|
|||
import Foundation
|
||||
|
||||
extension HomebrewService
|
||||
{
|
||||
{
|
||||
@MainActor
|
||||
func loadDetails() async throws -> ServiceDetails?
|
||||
{
|
||||
AppConstants.logger.debug("Will try to load up service details for service \(self.name)")
|
||||
|
||||
let rawOutput: TerminalOutput = await shell(AppConstants.brewExecutablePath, ["services", "info", self.name, "--json"])
|
||||
|
||||
let decoder: JSONDecoder =
|
||||
{
|
||||
AppConstants.logger.debug("Will try to load up service details for service \(name)")
|
||||
|
||||
let rawOutput: TerminalOutput = await shell(AppConstants.brewExecutablePath, ["services", "info", name, "--json"])
|
||||
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
||||
return decoder
|
||||
}()
|
||||
|
||||
|
||||
// MARK: - Error checking
|
||||
|
||||
if !rawOutput.standardError.isEmpty
|
||||
{
|
||||
AppConstants.logger.error("Failed while loading up service details: Standard Error not empty")
|
||||
throw HomebrewServiceLoadingError.standardErrorNotEmpty(standardError: rawOutput.standardError)
|
||||
}
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
guard let decodableOutput: Data = rawOutput.standardOutput.data(using: .utf8, allowLossyConversion: false) else
|
||||
guard let decodableOutput: Data = rawOutput.standardOutput.data(using: .utf8, allowLossyConversion: false)
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let decodedOutput: ServiceDetails = try decoder.decode([ServiceDetails].self, from: decodableOutput).first else
|
||||
|
||||
guard let decodedOutput: ServiceDetails = try decoder.decode([ServiceDetails].self, from: decodableOutput).first
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return decodedOutput
|
||||
}
|
||||
catch let parsingError
|
||||
{
|
||||
AppConstants.logger.error("Parsing of service details of service \(self.name) failed: \(parsingError)")
|
||||
|
||||
|
||||
throw HomebrewServiceLoadingError.servicesParsingFailed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct SearchResults
|
||||
{
|
||||
let foundFormulae: [String]
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ func shell(
|
|||
{
|
||||
switch streamedOutput
|
||||
{
|
||||
case let .standardOutput(output):
|
||||
case .standardOutput(let output):
|
||||
allOutput.append(output)
|
||||
case let .standardError(error):
|
||||
case .standardError(let error):
|
||||
allErrors.append(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,6 @@ func shell(
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
/// # Usage:
|
||||
/// for await output in shell(AppConstants.brewExecutablePath, ["install", package.name])
|
||||
/// {
|
||||
|
|
@ -45,18 +44,20 @@ func shell(
|
|||
/// case let .error(errorLine):
|
||||
/// // Do something with `errorLine`
|
||||
/// }
|
||||
///}
|
||||
/// }
|
||||
func shell(
|
||||
_ launchPath: URL,
|
||||
_ arguments: [String],
|
||||
environment: [String: String]? = nil,
|
||||
workingDirectory: URL? = nil
|
||||
) -> AsyncStream<StreamedTerminalOutput> {
|
||||
) -> AsyncStream<StreamedTerminalOutput>
|
||||
{
|
||||
let task = Process()
|
||||
|
||||
|
||||
var finalEnvironment: [String: String] = .init()
|
||||
|
||||
|
||||
// MARK: - Set up the $HOME environment variable so brew commands work on versions 4.1 and up
|
||||
|
||||
if var environment
|
||||
{
|
||||
environment["HOME"] = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
|
|
@ -66,43 +67,46 @@ func shell(
|
|||
{
|
||||
finalEnvironment = ["HOME": FileManager.default.homeDirectoryForCurrentUser.path]
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Set up proxy if it's enabled
|
||||
|
||||
if let proxySettings = AppConstants.proxySettings
|
||||
{
|
||||
AppConstants.logger.info("Proxy is enabled")
|
||||
finalEnvironment["ALL_PROXY"] = "\(proxySettings.host):\(proxySettings.port)"
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Block automatic cleanup is configured
|
||||
|
||||
if !UserDefaults.standard.bool(forKey: "isAutomaticCleanupEnabled")
|
||||
{
|
||||
finalEnvironment["HOMEBREW_NO_INSTALL_CLEANUP"] = "TRUE"
|
||||
}
|
||||
|
||||
|
||||
AppConstants.logger.debug("Final environment: \(finalEnvironment)")
|
||||
|
||||
|
||||
// MARK: - Set working directory if provided
|
||||
|
||||
if let workingDirectory
|
||||
{
|
||||
AppConstants.logger.info("Working directory configured: \(workingDirectory)")
|
||||
task.currentDirectoryURL = workingDirectory
|
||||
}
|
||||
|
||||
|
||||
let sudoHelperURL: URL = Bundle.main.resourceURL!.appendingPathComponent("Sudo Helper", conformingTo: .executable)
|
||||
|
||||
|
||||
finalEnvironment["SUDO_ASKPASS"] = sudoHelperURL.path
|
||||
|
||||
|
||||
task.environment = finalEnvironment
|
||||
task.launchPath = launchPath.absoluteString
|
||||
task.arguments = arguments
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
|
||||
|
||||
let errorPipe = Pipe()
|
||||
task.standardError = errorPipe
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
try task.run()
|
||||
|
|
@ -112,20 +116,26 @@ func shell(
|
|||
AppConstants.logger.error("\(String(describing: error))")
|
||||
}
|
||||
|
||||
return AsyncStream { continuation in
|
||||
return AsyncStream
|
||||
{ continuation in
|
||||
pipe.fileHandleForReading.readabilityHandler = { handler in
|
||||
guard let standardOutput = String(data: handler.availableData, encoding: .utf8) else
|
||||
guard let standardOutput = String(data: handler.availableData, encoding: .utf8)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard !standardOutput.isEmpty else { return }
|
||||
guard !standardOutput.isEmpty else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
continuation.yield(.standardOutput(standardOutput))
|
||||
}
|
||||
|
||||
errorPipe.fileHandleForReading.readabilityHandler = { handler in
|
||||
guard let errorOutput = String(data: handler.availableData, encoding: .utf8) else
|
||||
guard let errorOutput = String(data: handler.availableData, encoding: .utf8)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
// Created by David Bureš on 31.03.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
func submitSystemVersion() async throws -> Void
|
||||
func submitSystemVersion() async throws
|
||||
{
|
||||
let corkVersion: String = await String(NSApplication.appVersion!)
|
||||
|
||||
|
||||
let sessionConfiguration = URLSessionConfiguration.default
|
||||
if AppConstants.proxySettings != nil
|
||||
{
|
||||
|
|
@ -21,47 +21,49 @@ func submitSystemVersion() async throws -> Void
|
|||
kCFNetworkProxiesHTTPProxy: AppConstants.proxySettings!.host
|
||||
] as [AnyHashable: Any]
|
||||
}
|
||||
|
||||
let session: URLSession = URLSession(configuration: sessionConfiguration)
|
||||
|
||||
var isSelfCompiled: Bool = false
|
||||
|
||||
let session = URLSession(configuration: sessionConfiguration)
|
||||
|
||||
var isSelfCompiled = false
|
||||
if ProcessInfo.processInfo.environment["SELF_COMPILED"] == "true"
|
||||
{
|
||||
isSelfCompiled = true
|
||||
}
|
||||
|
||||
|
||||
var urlComponents = URLComponents(url: AppConstants.osSubmissionEndpointURL, resolvingAgainstBaseURL: false)
|
||||
urlComponents?.queryItems = [
|
||||
URLQueryItem(name: "systemVersion", value: String(ProcessInfo.processInfo.operatingSystemVersion.majorVersion)),
|
||||
URLQueryItem(name: "corkVersion", value: corkVersion),
|
||||
URLQueryItem(name: "isSelfCompiled", value: String(isSelfCompiled))
|
||||
URLQueryItem(name: "isSelfCompiled", value: String(isSelfCompiled)),
|
||||
]
|
||||
guard let modifiedURL = urlComponents?.url else {
|
||||
guard let modifiedURL = urlComponents?.url
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
var request: URLRequest = URLRequest(url: modifiedURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
|
||||
|
||||
|
||||
var request = URLRequest(url: modifiedURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
|
||||
|
||||
request.httpMethod = "GET"
|
||||
|
||||
|
||||
let authorizationComplex = "\(AppConstants.licensingAuthorization.username):\(AppConstants.licensingAuthorization.passphrase)"
|
||||
|
||||
|
||||
guard let authorizationComplexAsData: Data = authorizationComplex.data(using: .utf8, allowLossyConversion: false)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
request.addValue("Basic \(authorizationComplexAsData.base64EncodedString())", forHTTPHeaderField: "Authorization")
|
||||
|
||||
|
||||
let (_, response) = try await session.data(for: request)
|
||||
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200
|
||||
{
|
||||
#if DEBUG
|
||||
AppConstants.logger.debug("Sucessfully submitted system version")
|
||||
AppConstants.logger.debug("Sucessfully submitted system version")
|
||||
#endif
|
||||
|
||||
|
||||
UserDefaults.standard.setValue(corkVersion, forKey: "lastSubmittedCorkVersion")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,28 +8,29 @@
|
|||
import Foundation
|
||||
|
||||
@MainActor
|
||||
func applyTagsToPackageTrackingArray(appState: AppState, brewData: BrewDataStorage) async throws -> Void
|
||||
func applyTagsToPackageTrackingArray(appState: AppState, brewData: BrewDataStorage) async throws
|
||||
{
|
||||
for taggedName in appState.taggedPackageNames
|
||||
{
|
||||
AppConstants.logger.log("Will attempt to place package name \(taggedName, privacy: .public)")
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map({ formula in
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula = formula
|
||||
if copyFormula.name == taggedName
|
||||
{
|
||||
copyFormula.changeTaggedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
}))
|
||||
})
|
||||
|
||||
brewData.installedCasks = Set(brewData.installedCasks.map({ cask in
|
||||
brewData.installedCasks = Set(brewData.installedCasks.map
|
||||
{ cask in
|
||||
var copyCask = cask
|
||||
if copyCask.name == taggedName
|
||||
{
|
||||
copyCask.changeTaggedStatus()
|
||||
}
|
||||
return copyCask
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,29 +8,31 @@
|
|||
import Foundation
|
||||
|
||||
@MainActor
|
||||
func changePackageTagStatus(package: BrewPackage, brewData: BrewDataStorage, appState: AppState) async -> Void
|
||||
func changePackageTagStatus(package: BrewPackage, brewData: BrewDataStorage, appState: AppState) async
|
||||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map({ formula in
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula = formula
|
||||
if copyFormula.name == package.name
|
||||
{
|
||||
copyFormula.changeTaggedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
}))
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
brewData.installedFormulae = Set(brewData.installedCasks.map({ cask in
|
||||
brewData.installedFormulae = Set(brewData.installedCasks.map
|
||||
{ cask in
|
||||
var copyCask = cask
|
||||
if copyCask.name == package.name
|
||||
{
|
||||
copyCask.changeTaggedStatus()
|
||||
}
|
||||
return copyCask
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
if appState.taggedPackageNames.contains(package.name)
|
||||
|
|
@ -43,6 +45,6 @@ func changePackageTagStatus(package: BrewPackage, brewData: BrewDataStorage, app
|
|||
}
|
||||
|
||||
AppConstants.logger.debug("Tagged package with ID \(package.id, privacy: .public): \(package.name, privacy: .public)")
|
||||
|
||||
|
||||
AppConstants.logger.debug("Tagged packages: \(appState.taggedPackageNames, privacy: .public)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,24 +10,23 @@ import Foundation
|
|||
func loadTaggedIDsFromDisk() throws -> Set<String>
|
||||
{
|
||||
var nameSet: Set<String> = .init()
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
let rawPackageNamesFromFile: String = try String(contentsOf: AppConstants.metadataFilePath, encoding: .utf8)
|
||||
let packageNamesAsArray: [String] = rawPackageNamesFromFile.components(separatedBy: ":")
|
||||
|
||||
|
||||
for packageNameAsString in packageNamesAsArray
|
||||
{
|
||||
nameSet.insert(packageNameAsString)
|
||||
}
|
||||
|
||||
}
|
||||
catch let dataReadingError as NSError
|
||||
{
|
||||
AppConstants.logger.error("Failed while reading data from disk: \(dataReadingError, privacy: .public)")
|
||||
}
|
||||
|
||||
|
||||
AppConstants.logger.debug("Loaded name set: \(nameSet, privacy: .public)")
|
||||
|
||||
|
||||
return nameSet
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
func addTap(name: String, forcedRepoAddress: String? = nil) async -> String
|
||||
{
|
||||
var tapResult: String
|
||||
|
||||
|
||||
if let forcedRepoAddress
|
||||
{
|
||||
tapResult = await shell(AppConstants.brewExecutablePath, ["tap", name, forcedRepoAddress]).standardError
|
||||
|
|
|
|||
|
|
@ -9,27 +9,28 @@ import Foundation
|
|||
|
||||
func parseTapInfo(from rawJSON: String) async throws -> TapInfo?
|
||||
{
|
||||
let decoder: JSONDecoder =
|
||||
{
|
||||
let decoder: JSONDecoder = JSONDecoder()
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
|
||||
return decoder
|
||||
}()
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
guard let jsonAsData: Data = rawJSON.data(using: .utf8, allowLossyConversion: false) else
|
||||
guard let jsonAsData: Data = rawJSON.data(using: .utf8, allowLossyConversion: false)
|
||||
else
|
||||
{
|
||||
AppConstants.logger.error("Could not convert tap JSON string into data")
|
||||
throw JSONParsingError.couldNotConvertStringToData(failureReason: nil)
|
||||
}
|
||||
|
||||
|
||||
return try decoder.decode([TapInfo].self, from: jsonAsData).first
|
||||
|
||||
} catch let decodingError {
|
||||
}
|
||||
catch let decodingError
|
||||
{
|
||||
AppConstants.logger.error("Failed while decoding tap info: \(decodingError.localizedDescription, privacy: .public)\n-\(decodingError, privacy: .public)")
|
||||
|
||||
|
||||
throw JSONParsingError.couldNotDecode(failureReason: decodingError.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,21 +11,21 @@ import SwiftUI
|
|||
enum UntapError: LocalizedError
|
||||
{
|
||||
case couldNotUntap(tapName: String, failureReason: String)
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .couldNotUntap(let tapName, let failureReason):
|
||||
return String(localized: "error.tap.untap.could-not-untap.tap-\(tapName).failure-reason-\(failureReason)")
|
||||
switch self
|
||||
{
|
||||
case .couldNotUntap(let tapName, let failureReason):
|
||||
return String(localized: "error.tap.untap.could-not-untap.tap-\(tapName).failure-reason-\(failureReason)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func removeTap(name: String, availableTaps: AvailableTaps, appState: AppState, shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false) async throws -> Void
|
||||
func removeTap(name: String, availableTaps: AvailableTaps, appState: AppState, shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false) async throws
|
||||
{
|
||||
|
||||
var indexToReplaceGlobal: Int? = nil
|
||||
var indexToReplaceGlobal: Int?
|
||||
|
||||
/// Store the old navigation selection to see if it got updated in the middle of switching
|
||||
let oldNavigationSelectionID: UUID? = appState.navigationSelection
|
||||
|
|
@ -55,8 +55,10 @@ func removeTap(name: String, availableTaps: AvailableTaps, appState: AppState, s
|
|||
if untapResult.contains("Untapped")
|
||||
{
|
||||
AppConstants.logger.info("Untapping was successful")
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
DispatchQueue.main.async
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
availableTaps.addedTaps.removeAll(where: { $0.name == name })
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +77,7 @@ func removeTap(name: String, availableTaps: AvailableTaps, appState: AppState, s
|
|||
AppConstants.logger.warning("Untapping failed")
|
||||
|
||||
if untapResult.contains("because it contains the following installed formulae or casks")
|
||||
{
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(offendingTapProhibitingRemovalOfTap: name))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ func refreshPackages(_ updateProgressTracker: UpdateProgressTracker, outdatedPac
|
|||
{
|
||||
switch output
|
||||
{
|
||||
case let .standardOutput(outputLine):
|
||||
AppConstants.logger.log("Update function output: \(outputLine, privacy: .public)")
|
||||
case .standardOutput(let outputLine):
|
||||
AppConstants.logger.log("Update function output: \(outputLine, privacy: .public)")
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
{
|
||||
|
|
@ -36,7 +36,7 @@ func refreshPackages(_ updateProgressTracker: UpdateProgressTracker, outdatedPac
|
|||
}
|
||||
}
|
||||
|
||||
case let .standardError(errorLine):
|
||||
case .standardError(let errorLine):
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
func updatePackages(updateProgressTracker: UpdateProgressTracker, appState _: AppState, outdatedPackageTracker _: OutdatedPackageTracker, detailStage: UpdatingProcessDetails) async
|
||||
func updatePackages(updateProgressTracker: UpdateProgressTracker, detailStage: UpdatingProcessDetails) async
|
||||
{
|
||||
let showRealTimeTerminalOutputs = UserDefaults.standard.bool(forKey: "showRealTimeTerminalOutputOfOperations")
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ func updatePackages(updateProgressTracker: UpdateProgressTracker, appState _: Ap
|
|||
{
|
||||
switch output
|
||||
{
|
||||
case let .standardOutput(outputLine):
|
||||
case .standardOutput(let outputLine):
|
||||
AppConstants.logger.log("Upgrade function output: \(outputLine, privacy: .public)")
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
|
|
@ -54,7 +54,7 @@ func updatePackages(updateProgressTracker: UpdateProgressTracker, appState _: Ap
|
|||
|
||||
updateProgressTracker.updateProgress = updateProgressTracker.updateProgress + 0.1
|
||||
|
||||
case let .standardError(errorLine):
|
||||
case .standardError(let errorLine):
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class BrewDataStorage: ObservableObject
|
|||
{
|
||||
removePackageFromTracker(withName: name, tracker: .formula)
|
||||
}
|
||||
|
||||
func removeCaskFromTracker(withName name: String)
|
||||
{
|
||||
removePackageFromTracker(withName: name, tracker: .cask)
|
||||
|
|
@ -37,17 +38,18 @@ class BrewDataStorage: ObservableObject
|
|||
|
||||
private func removePackageFromTracker(withName name: String, tracker: PackageType)
|
||||
{
|
||||
switch tracker {
|
||||
case .formula:
|
||||
if let index = installedFormulae.firstIndex(where: { $0.name == name })
|
||||
{
|
||||
installedFormulae.remove(at: index)
|
||||
}
|
||||
case .cask:
|
||||
if let index = installedCasks.firstIndex(where: { $0.name == name })
|
||||
{
|
||||
installedCasks.remove(at: index)
|
||||
}
|
||||
switch tracker
|
||||
{
|
||||
case .formula:
|
||||
if let index = installedFormulae.firstIndex(where: { $0.name == name })
|
||||
{
|
||||
installedFormulae.remove(at: index)
|
||||
}
|
||||
case .cask:
|
||||
if let index = installedCasks.firstIndex(where: { $0.name == name })
|
||||
{
|
||||
installedCasks.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ struct BrewTap: Identifiable, Hashable
|
|||
{
|
||||
let id = UUID()
|
||||
let name: String
|
||||
|
||||
|
||||
var isBeingModified: Bool = false
|
||||
|
||||
mutating func changeBeingModifiedStatus() -> Void
|
||||
|
||||
mutating func changeBeingModifiedStatus()
|
||||
{
|
||||
self.isBeingModified.toggle()
|
||||
isBeingModified.toggle()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@
|
|||
// Created by David Bureš on 04.11.2023.
|
||||
//
|
||||
|
||||
import Charts
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Charts
|
||||
|
||||
struct CachedDownload: Identifiable, Hashable
|
||||
{
|
||||
var id: UUID = UUID()
|
||||
var id: UUID = .init()
|
||||
|
||||
let packageName: String
|
||||
let sizeInBytes: Int
|
||||
|
||||
|
||||
var packageType: CachedDownloadType?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// DataFile.swift
|
||||
// Data File.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 10.11.2023.
|
||||
|
|
@ -11,8 +11,15 @@ import UniformTypeIdentifiers
|
|||
|
||||
struct StringFile: FileDocument
|
||||
{
|
||||
static var readableContentTypes: [UTType] { [.homebrewBackup, .plainText] }
|
||||
static var writableContentTypes: [UTType] { [.homebrewBackup] }
|
||||
static var readableContentTypes: [UTType]
|
||||
{
|
||||
[.homebrewBackup, .plainText]
|
||||
}
|
||||
|
||||
static var writableContentTypes: [UTType]
|
||||
{
|
||||
[.homebrewBackup]
|
||||
}
|
||||
|
||||
var text: String
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import Foundation
|
|||
|
||||
struct TopPackage: Hashable, Identifiable
|
||||
{
|
||||
var id: UUID = UUID()
|
||||
|
||||
var id: UUID = .init()
|
||||
|
||||
let packageName: String
|
||||
let packageDownloads: Int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ class TopPackagesTracker: ObservableObject, Sendable
|
|||
switch sortTopPackagesBy
|
||||
{
|
||||
case .mostDownloads:
|
||||
return self.topFormulae.sorted(by: { $0.packageDownloads > $1.packageDownloads })
|
||||
return topFormulae.sorted(by: { $0.packageDownloads > $1.packageDownloads })
|
||||
case .fewestDownloads:
|
||||
return self.topFormulae.sorted(by: { $0.packageDownloads < $1.packageDownloads })
|
||||
return topFormulae.sorted(by: { $0.packageDownloads < $1.packageDownloads })
|
||||
case .random:
|
||||
return self.topFormulae.shuffled()
|
||||
return topFormulae.shuffled()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,11 +34,11 @@ class TopPackagesTracker: ObservableObject, Sendable
|
|||
switch sortTopPackagesBy
|
||||
{
|
||||
case .mostDownloads:
|
||||
return self.topCasks.sorted(by: { $0.packageDownloads > $1.packageDownloads })
|
||||
return topCasks.sorted(by: { $0.packageDownloads > $1.packageDownloads })
|
||||
case .fewestDownloads:
|
||||
return self.topCasks.sorted(by: { $0.packageDownloads < $1.packageDownloads })
|
||||
return topCasks.sorted(by: { $0.packageDownloads < $1.packageDownloads })
|
||||
case .random:
|
||||
return self.topCasks.shuffled()
|
||||
return topCasks.shuffled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ import Foundation
|
|||
|
||||
struct MinimalHomebrewPackage: Identifiable, Hashable, AppEntity
|
||||
{
|
||||
var id: UUID = UUID()
|
||||
var id: UUID = .init()
|
||||
|
||||
var name: String
|
||||
|
||||
var type: PackageType
|
||||
|
||||
|
||||
var installDate: Date?
|
||||
|
||||
|
||||
var installedIntentionally: Bool
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "intents.type.minimal-homebrew-package")
|
||||
|
|
@ -25,7 +25,7 @@ struct MinimalHomebrewPackage: Identifiable, Hashable, AppEntity
|
|||
var displayRepresentation: DisplayRepresentation
|
||||
{
|
||||
DisplayRepresentation(
|
||||
title: "\(self.name)",
|
||||
title: "\(name)",
|
||||
subtitle: "intents.type.minimal-homebrew-package.representation.subtitle"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,41 +12,42 @@ struct TapInfo: Codable
|
|||
{
|
||||
/// The name of the tap
|
||||
let name: String
|
||||
|
||||
|
||||
/// The user responsible for the tap
|
||||
let user: String
|
||||
|
||||
|
||||
/// Name of the upstream repo
|
||||
let repo: String
|
||||
|
||||
|
||||
/// Path to the tap
|
||||
let path: URL
|
||||
|
||||
|
||||
/// Whether the tap is currently added
|
||||
let installed: Bool
|
||||
|
||||
|
||||
/// Whether the tap is from the Homebrew developers
|
||||
let official: Bool
|
||||
|
||||
|
||||
// MARK: - The contents of the tap
|
||||
|
||||
/// The formulae included in the tap
|
||||
let formulaNames: [String]
|
||||
|
||||
|
||||
/// The casks included in the tap
|
||||
let caskTokens: [String]
|
||||
|
||||
|
||||
/// The paths to the formula files
|
||||
let formulaFiles: [URL]
|
||||
|
||||
|
||||
/// The paths to the cask files
|
||||
let caskFiles: [URL]
|
||||
|
||||
|
||||
/// No idea, honestly
|
||||
let commandFiles: [String]
|
||||
|
||||
|
||||
/// Link to the actual repo
|
||||
let remote: URL
|
||||
|
||||
|
||||
/// IDK
|
||||
let customRemote: Bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,3 @@ struct UsedPackage: Identifiable
|
|||
let whyIsItUsed: LocalizedStringKey
|
||||
let packageURL: URL
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import SwiftUI
|
|||
class OutdatedPackageTracker: ObservableObject, Sendable
|
||||
{
|
||||
@AppStorage("displayOnlyIntentionallyInstalledPackagesByDefault") var displayOnlyIntentionallyInstalledPackagesByDefault: Bool = true
|
||||
|
||||
|
||||
@Published var outdatedPackages: Set<OutdatedPackage> = .init()
|
||||
|
||||
|
||||
var displayableOutdatedPackages: Set<OutdatedPackage>
|
||||
{
|
||||
if displayOnlyIntentionallyInstalledPackagesByDefault
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import Foundation
|
|||
|
||||
struct OutdatedPackage: Identifiable, Equatable, Hashable
|
||||
{
|
||||
let id: UUID = UUID()
|
||||
|
||||
let id: UUID = .init()
|
||||
|
||||
let package: BrewPackage
|
||||
|
||||
|
||||
let installedVersions: [String]
|
||||
let newerVersion: String
|
||||
|
||||
|
||||
var isMarkedForUpdating: Bool = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class InstallationProgressTracker: ObservableObject
|
|||
{
|
||||
switch output
|
||||
{
|
||||
case let .standardOutput(outputLine):
|
||||
case .standardOutput(let outputLine):
|
||||
|
||||
AppConstants.logger.debug("Package instrall line out: \(outputLine, privacy: .public)")
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ class InstallationProgressTracker: ObservableObject
|
|||
|
||||
AppConstants.logger.debug("Current installation stage: \(self.packageBeingInstalled.installationStage.description, privacy: .public)")
|
||||
|
||||
case let .standardError(errorLine):
|
||||
case .standardError(let errorLine):
|
||||
AppConstants.logger.error("Errored out: \(errorLine, privacy: .public)")
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
|
|
@ -183,7 +183,7 @@ class InstallationProgressTracker: ObservableObject
|
|||
{
|
||||
switch output
|
||||
{
|
||||
case let .standardOutput(outputLine):
|
||||
case .standardOutput(let outputLine):
|
||||
AppConstants.logger.info("Output line: \(outputLine, privacy: .public)")
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
|
|
@ -226,9 +226,9 @@ class InstallationProgressTracker: ObservableObject
|
|||
else if outputLine.contains("Purging files")
|
||||
{
|
||||
AppConstants.logger.info("Purging old version of cask \(package.name)")
|
||||
|
||||
|
||||
packageBeingInstalled.installationStage = .installingCask
|
||||
|
||||
|
||||
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + 1
|
||||
}
|
||||
else if outputLine.contains("was successfully installed")
|
||||
|
|
@ -240,7 +240,7 @@ class InstallationProgressTracker: ObservableObject
|
|||
packageBeingInstalled.packageInstallationProgress = 10
|
||||
}
|
||||
|
||||
case let .standardError(errorLine):
|
||||
case .standardError(let errorLine):
|
||||
AppConstants.logger.error("Line had error: \(errorLine, privacy: .public)")
|
||||
|
||||
if showRealTimeTerminalOutputs
|
||||
|
|
@ -257,7 +257,7 @@ class InstallationProgressTracker: ObservableObject
|
|||
else if errorLine.contains("there is already an App at")
|
||||
{
|
||||
AppConstants.logger.warning("The app already exists")
|
||||
|
||||
|
||||
packageBeingInstalled.installationStage = .binaryAlreadyExists
|
||||
}
|
||||
else if errorLine.contains(/depends on hardware architecture being.+but you are running/)
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ struct RealTimeTerminalLine: Identifiable, Hashable, Equatable
|
|||
struct PackageInProgressOfBeingInstalled: Identifiable
|
||||
{
|
||||
let id = UUID()
|
||||
|
||||
|
||||
let package: BrewPackage
|
||||
var installationStage: PackageInstallationStage
|
||||
var packageInstallationProgress: Double
|
||||
|
||||
|
||||
var realTimeTerminalOutput: [RealTimeTerminalLine] = .init()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import SwiftUI
|
|||
|
||||
@MainActor
|
||||
class UninstallationConfirmationTracker: ObservableObject
|
||||
{
|
||||
{
|
||||
@Published var isShowingUninstallOrPurgeConfirmation: Bool = false
|
||||
|
||||
|
||||
@Published private(set) var packageThatNeedsConfirmation: BrewPackage = .init(name: "", type: .formula, installedOn: Date(), versions: [], sizeInBytes: 0)
|
||||
@Published private(set) var shouldPurge: Bool = false
|
||||
@Published private(set) var isCalledFromSidebar: Bool = false
|
||||
|
|
@ -22,17 +22,17 @@ class UninstallationConfirmationTracker: ObservableObject
|
|||
self.packageThatNeedsConfirmation = packageThatNeedsConfirmation
|
||||
self.shouldPurge = shouldPurge
|
||||
self.isCalledFromSidebar = isCalledFromSidebar
|
||||
|
||||
self.isShowingUninstallOrPurgeConfirmation = true
|
||||
|
||||
isShowingUninstallOrPurgeConfirmation = true
|
||||
}
|
||||
|
||||
|
||||
func dismissConfirmationDialog()
|
||||
{
|
||||
if self.isShowingUninstallOrPurgeConfirmation
|
||||
if isShowingUninstallOrPurgeConfirmation
|
||||
{
|
||||
self.isShowingUninstallOrPurgeConfirmation = false
|
||||
isShowingUninstallOrPurgeConfirmation = false
|
||||
}
|
||||
|
||||
self.packageThatNeedsConfirmation = .init(name: "", type: .formula, installedOn: Date(), versions: [], sizeInBytes: 0)
|
||||
|
||||
packageThatNeedsConfirmation = .init(name: "", type: .formula, installedOn: Date(), versions: [], sizeInBytes: 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ import Foundation
|
|||
enum PinningUnpinningError: LocalizedError
|
||||
{
|
||||
case failedWhileChangingPinnedStatus
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .failedWhileChangingPinnedStatus:
|
||||
return String(localized: "error.package-details.couldnt-pin-unpin")
|
||||
switch self
|
||||
{
|
||||
case .failedWhileChangingPinnedStatus:
|
||||
return String(localized: "error.package-details.couldnt-pin-unpin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +25,7 @@ enum PinningUnpinningError: LocalizedError
|
|||
class BrewPackageDetails: ObservableObject
|
||||
{
|
||||
// MARK: - Immutable properties
|
||||
|
||||
/// Name of the package
|
||||
let name: String
|
||||
|
||||
|
|
@ -35,14 +37,16 @@ class BrewPackageDetails: ObservableObject
|
|||
let dependencies: [BrewPackageDependency]?
|
||||
let outdated: Bool
|
||||
let caveats: String?
|
||||
|
||||
|
||||
let isCompatible: Bool
|
||||
|
||||
|
||||
// MARK: - Mutable properties
|
||||
|
||||
@Published var dependents: [String]?
|
||||
@Published var pinned: Bool
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(name: String, description: String?, homepage: URL, tap: BrewTap, installedAsDependency: Bool, dependents: [String]? = nil, dependencies: [BrewPackageDependency]? = nil, outdated: Bool, caveats: String? = nil, pinned: Bool, isCompatible: Bool)
|
||||
{
|
||||
self.name = name
|
||||
|
|
@ -59,23 +63,24 @@ class BrewPackageDetails: ObservableObject
|
|||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
func loadDependents() async
|
||||
{
|
||||
AppConstants.logger.debug("Will load dependents for \(self.name)")
|
||||
let packageDependentsRaw: String = await shell(AppConstants.brewExecutablePath, ["uses", "--installed", self.name]).standardOutput
|
||||
let packageDependentsRaw: String = await shell(AppConstants.brewExecutablePath, ["uses", "--installed", name]).standardOutput
|
||||
|
||||
let finalDependents: [String] = packageDependentsRaw.components(separatedBy: "\n").dropLast()
|
||||
|
||||
|
||||
AppConstants.logger.debug("Dependents loaded: \(finalDependents)")
|
||||
|
||||
self.dependents = finalDependents
|
||||
|
||||
dependents = finalDependents
|
||||
}
|
||||
|
||||
func changePinnedStatus() async throws
|
||||
{
|
||||
if self.pinned
|
||||
if pinned
|
||||
{
|
||||
let pinResult = await shell(AppConstants.brewExecutablePath, ["unpin", self.name])
|
||||
let pinResult = await shell(AppConstants.brewExecutablePath, ["unpin", name])
|
||||
|
||||
if !pinResult.standardError.isEmpty
|
||||
{
|
||||
|
|
@ -85,14 +90,14 @@ class BrewPackageDetails: ObservableObject
|
|||
}
|
||||
else
|
||||
{
|
||||
let unpinResult = await shell(AppConstants.brewExecutablePath, ["pin", self.name])
|
||||
let unpinResult = await shell(AppConstants.brewExecutablePath, ["pin", name])
|
||||
if !unpinResult.standardError.isEmpty
|
||||
{
|
||||
AppConstants.logger.error("Error unpinning: \(unpinResult.standardError, privacy: .public)")
|
||||
throw PinningUnpinningError.failedWhileChangingPinnedStatus
|
||||
}
|
||||
}
|
||||
|
||||
self.pinned.toggle()
|
||||
|
||||
pinned.toggle()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,17 @@
|
|||
// Created by David Bureš on 03.07.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import DavidFoundation
|
||||
import Foundation
|
||||
|
||||
/// A representation of a Homebrew package
|
||||
struct BrewPackage: Identifiable, Equatable, Hashable
|
||||
{
|
||||
var id = UUID()
|
||||
let name: String
|
||||
|
||||
lazy var sanitizedName: String? =
|
||||
{
|
||||
|
||||
lazy var sanitizedName: String? = {
|
||||
var packageNameWithoutTap: String
|
||||
{ /// First, remove the tap name from the package name if it has it
|
||||
if self.name.contains("/")
|
||||
|
|
@ -35,7 +34,7 @@ struct BrewPackage: Identifiable, Equatable, Hashable
|
|||
return self.name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if packageNameWithoutTap.contains("@")
|
||||
{ /// Only do the matching if the name contains @
|
||||
if let sanitizedName = try? regexMatch(from: packageNameWithoutTap, regex: ".+?(?=@)")
|
||||
|
|
@ -55,54 +54,53 @@ struct BrewPackage: Identifiable, Equatable, Hashable
|
|||
|
||||
let type: PackageType
|
||||
var isTagged: Bool = false
|
||||
|
||||
|
||||
let installedOn: Date?
|
||||
let versions: [String]
|
||||
|
||||
|
||||
var installedIntentionally: Bool = true
|
||||
|
||||
|
||||
let sizeInBytes: Int64?
|
||||
|
||||
|
||||
var isBeingModified: Bool = false
|
||||
|
||||
|
||||
func getFormattedVersions() -> String
|
||||
{
|
||||
return self.versions.formatted(.list(type: .and))
|
||||
return versions.formatted(.list(type: .and))
|
||||
}
|
||||
|
||||
mutating func changeTaggedStatus() -> Void
|
||||
|
||||
mutating func changeTaggedStatus()
|
||||
{
|
||||
self.isTagged.toggle()
|
||||
isTagged.toggle()
|
||||
}
|
||||
|
||||
mutating func changeBeingModifiedStatus() -> Void
|
||||
|
||||
mutating func changeBeingModifiedStatus()
|
||||
{
|
||||
self.isBeingModified.toggle()
|
||||
isBeingModified.toggle()
|
||||
}
|
||||
|
||||
mutating func purgeSanitizedName() -> Void
|
||||
|
||||
mutating func purgeSanitizedName()
|
||||
{
|
||||
self.sanitizedName = nil
|
||||
sanitizedName = nil
|
||||
}
|
||||
|
||||
|
||||
/// Open the location of this package in Finder
|
||||
func revealInFinder() throws
|
||||
{
|
||||
|
||||
enum FinderRevealError: LocalizedError
|
||||
{
|
||||
case couldNotFindPackageInParent
|
||||
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
return String(localized: "error.finder-reveal.could-not-find-package-in-parent")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var packageURL: URL?
|
||||
var packageLocationParent: URL
|
||||
{
|
||||
if self.type == .formula
|
||||
if type == .formula
|
||||
{
|
||||
return AppConstants.brewCellarPath
|
||||
}
|
||||
|
|
@ -111,19 +109,23 @@ struct BrewPackage: Identifiable, Equatable, Hashable
|
|||
return AppConstants.brewCaskPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let contentsOfParentFolder = try! FileManager.default.contentsOfDirectory(at: packageLocationParent, includingPropertiesForKeys: [.isDirectoryKey])
|
||||
|
||||
packageURL = contentsOfParentFolder.filter({ $0.lastPathComponent.contains(name) }).first
|
||||
|
||||
guard let packageURL else
|
||||
|
||||
packageURL = contentsOfParentFolder.filter
|
||||
{
|
||||
$0.lastPathComponent.contains(name)
|
||||
}.first
|
||||
|
||||
guard let packageURL
|
||||
else
|
||||
{
|
||||
throw FinderRevealError.couldNotFindPackageInParent
|
||||
}
|
||||
|
||||
|
||||
packageURL.revealInFinder(.openParentDirectoryAndHighlightTarget)
|
||||
|
||||
//NSWorkspace.shared.selectFile(packageURL.path, inFileViewerRootedAtPath: packageURL.deletingLastPathComponent().path)
|
||||
|
||||
// NSWorkspace.shared.selectFile(packageURL.path, inFileViewerRootedAtPath: packageURL.deletingLastPathComponent().path)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +133,6 @@ extension FormatStyle where Self == Date.FormatStyle
|
|||
{
|
||||
static var packageInstallationStyle: Self
|
||||
{
|
||||
Self.dateTime.day().month(.wide).year().weekday(.wide).hour().minute()
|
||||
dateTime.day().month(.wide).year().weekday(.wide).hour().minute()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
|
||||
struct BrewPackageDependency: Identifiable, Hashable
|
||||
{
|
||||
let id: UUID = UUID()
|
||||
let id: UUID = .init()
|
||||
let name: String
|
||||
let version: String
|
||||
let directlyDeclared: Bool
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
|
||||
class SearchResultTracker: ObservableObject
|
||||
{
|
||||
/// These two have to be arrays because the order matters
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ extension PackageSearchToken: Identifiable
|
|||
|
||||
extension PackageSearchToken
|
||||
{
|
||||
var name: LocalizedStringKey {
|
||||
switch self {
|
||||
var name: LocalizedStringKey
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .formula:
|
||||
return "search.token.filter-formulae"
|
||||
case .cask:
|
||||
|
|
@ -38,8 +40,10 @@ extension PackageSearchToken
|
|||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
var icon: String
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .formula:
|
||||
return "terminal"
|
||||
case .cask:
|
||||
|
|
|
|||
|
|
@ -11,38 +11,39 @@ struct ServiceDetails: Hashable, Codable
|
|||
{
|
||||
let name: String
|
||||
let serviceName: String
|
||||
|
||||
|
||||
let running: Bool
|
||||
let loaded: Bool
|
||||
let schedulable: Bool
|
||||
|
||||
|
||||
let pid: Int?
|
||||
|
||||
|
||||
let exitCode: Int?
|
||||
|
||||
|
||||
let user: String?
|
||||
let status: ServiceStatus
|
||||
|
||||
|
||||
let file: URL?
|
||||
|
||||
|
||||
let command: URL?
|
||||
|
||||
|
||||
let workingDir: URL?
|
||||
let rootDir: URL?
|
||||
let logPath: URL?
|
||||
let errorLogPath: URL?
|
||||
|
||||
|
||||
let interval: String?
|
||||
let cron: String?
|
||||
|
||||
|
||||
// MARK: - Legacy stuff
|
||||
|
||||
/*
|
||||
let loaded: Bool
|
||||
let schedulable: Bool
|
||||
let pid: Int?
|
||||
|
||||
let rootDir: URL?
|
||||
let logPath: URL?
|
||||
let errorLogPath: URL?
|
||||
*/
|
||||
let loaded: Bool
|
||||
let schedulable: Bool
|
||||
let pid: Int?
|
||||
|
||||
let rootDir: URL?
|
||||
let logPath: URL?
|
||||
let errorLogPath: URL?
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,22 @@
|
|||
// Created by David Bureš on 20.03.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
struct HomebrewService: Identifiable, Hashable, Codable
|
||||
{
|
||||
var id: UUID = UUID()
|
||||
|
||||
var id: UUID = .init()
|
||||
|
||||
let name: String
|
||||
var status: ServiceStatus
|
||||
|
||||
|
||||
let user: String?
|
||||
|
||||
|
||||
let location: URL
|
||||
|
||||
|
||||
let exitCode: Int?
|
||||
|
||||
|
||||
func revealInFinder()
|
||||
{
|
||||
location.revealInFinder(.openParentDirectoryAndHighlightTarget)
|
||||
|
|
|
|||
|
|
@ -38,14 +38,16 @@ class ServicesTracker: ObservableObject
|
|||
|
||||
func changeServiceStatus(_ serviceToChange: HomebrewService, newStatus: ServiceStatus)
|
||||
{
|
||||
self.replaceServiceInTracker(
|
||||
replaceServiceInTracker(
|
||||
serviceToChange,
|
||||
with: .init(
|
||||
name: serviceToChange.name,
|
||||
status: newStatus,
|
||||
user: serviceToChange.user,
|
||||
location: serviceToChange.location,
|
||||
exitCode: serviceToChange.exitCode),
|
||||
performInPlaceReplacement: true)
|
||||
exitCode: serviceToChange.exitCode
|
||||
),
|
||||
performInPlaceReplacement: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,20 +15,21 @@ enum UpdateProcessStages: LocalizedStringKey, CustomStringConvertible
|
|||
case cleanup = "update-packages.detail-stage.cleanup"
|
||||
case backingUp = "update-packages.detail-stage.backing-up"
|
||||
case linking = "update-packages.detail-stage.linking"
|
||||
|
||||
|
||||
var description: String
|
||||
{
|
||||
switch self {
|
||||
case .downloading:
|
||||
return "Downloading"
|
||||
case .pouring:
|
||||
return "Pouring"
|
||||
case .cleanup:
|
||||
return "Cleanup"
|
||||
case .backingUp:
|
||||
return "Backing Up"
|
||||
case .linking:
|
||||
return "Linkling"
|
||||
switch self
|
||||
{
|
||||
case .downloading:
|
||||
return "Downloading"
|
||||
case .pouring:
|
||||
return "Pouring"
|
||||
case .cleanup:
|
||||
return "Cleanup"
|
||||
case .backingUp:
|
||||
return "Backing Up"
|
||||
case .linking:
|
||||
return "Linkling"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct LargeButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
struct LargeButtonStyle: ButtonStyle
|
||||
{
|
||||
func makeBody(configuration: Configuration) -> some View
|
||||
{
|
||||
configuration.label
|
||||
.padding(.horizontal, 27)
|
||||
.padding(.vertical, 7)
|
||||
|
|
@ -18,4 +20,3 @@ struct LargeButtonStyle: ButtonStyle {
|
|||
.shadow(radius: 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ struct NoPadding: DisclosureGroupStyle
|
|||
{
|
||||
configuration.isExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
}
|
||||
label:
|
||||
{
|
||||
HStack(alignment: .center, spacing: 4)
|
||||
{
|
||||
Image(systemName: configuration.isExpanded ? "chevron.down" : "chevron.right")
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ import SwiftUI
|
|||
|
||||
struct MiniGaugeStyle: GaugeStyle
|
||||
{
|
||||
|
||||
let tint: Color
|
||||
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View
|
||||
{
|
||||
ZStack
|
||||
|
|
|
|||
|
|
@ -79,9 +79,9 @@ struct AboutView: View
|
|||
reasonForAcknowledgement: "about.contributors.4.purpose",
|
||||
profileService: .mastodon,
|
||||
profileURL: URL(string: "https://hachyderm.io/@oscb")!
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
private let translators: [AcknowledgedContributor] = [
|
||||
AcknowledgedContributor(
|
||||
name: "about.translator.1.name",
|
||||
|
|
@ -93,27 +93,32 @@ struct AboutView: View
|
|||
name: "about.translator.2.name",
|
||||
reasonForAcknowledgement: "about.translator.2.purpose",
|
||||
profileService: .github,
|
||||
profileURL: URL(string: "https://github.com/sh95014")!),
|
||||
profileURL: URL(string: "https://github.com/sh95014")!
|
||||
),
|
||||
AcknowledgedContributor(
|
||||
name: "about.translator.3.name",
|
||||
reasonForAcknowledgement: "about.translator.3.purpose",
|
||||
profileService: .github,
|
||||
profileURL: URL(string: "https://github.com/hecaex")!),
|
||||
AcknowledgedContributor(
|
||||
name: "about.translator.4.name",
|
||||
reasonForAcknowledgement: "about.translator.4.purpose",
|
||||
profileService: .github,
|
||||
profileURL: URL(string: "https://github.com/louchebem06")!),
|
||||
profileURL: URL(string: "https://github.com/hecaex")!
|
||||
),
|
||||
AcknowledgedContributor(
|
||||
name: "about.translator.4.name",
|
||||
reasonForAcknowledgement: "about.translator.4.purpose",
|
||||
profileService: .github,
|
||||
profileURL: URL(string: "https://github.com/louchebem06")!
|
||||
),
|
||||
AcknowledgedContributor(
|
||||
name: "about.translator.5.name",
|
||||
reasonForAcknowledgement: "about.translator.5.purpose",
|
||||
profileService: .github,
|
||||
profileURL: URL(string: "https://github.com/utkinn")!),
|
||||
profileURL: URL(string: "https://github.com/utkinn")!
|
||||
),
|
||||
AcknowledgedContributor(
|
||||
name: "about.translator.6.name",
|
||||
reasonForAcknowledgement: "about.translator.6.purpose",
|
||||
profileService: .github,
|
||||
profileURL: URL(string: "https://github.com/smitt14ua")!),
|
||||
profileURL: URL(string: "https://github.com/smitt14ua")!
|
||||
)
|
||||
]
|
||||
|
||||
@State private var isPackageGroupExpanded: Bool = false
|
||||
|
|
@ -231,7 +236,7 @@ struct AboutView: View
|
|||
Text("about.contributors")
|
||||
}
|
||||
.animation(.none, value: isContributorGroupExpanded)
|
||||
|
||||
|
||||
DisclosureGroup
|
||||
{
|
||||
List
|
||||
|
|
@ -264,7 +269,7 @@ struct AboutView: View
|
|||
Text("about.translators")
|
||||
}
|
||||
.animation(.none, value: isTranslatorGroupExpanded)
|
||||
|
||||
|
||||
Text("about.privacy-policy")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
@ -293,6 +298,6 @@ struct AboutView: View
|
|||
.transaction { $0.animation = nil }
|
||||
}
|
||||
.padding()
|
||||
//.fixedSize()
|
||||
// .fixedSize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ struct SearchResultRow: View, Sendable
|
|||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
let searchedForPackage: BrewPackage
|
||||
|
||||
|
||||
@State private var description: String?
|
||||
@State private var isCompatible: Bool?
|
||||
|
||||
|
|
@ -51,7 +51,8 @@ struct SearchResultRow: View, Sendable
|
|||
{
|
||||
if showCompatibilityWarning
|
||||
{
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
HStack(alignment: .center, spacing: 4)
|
||||
{
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
Text("add-package.result.not-optimized-for-\(AppConstants.osVersionString.fullName)")
|
||||
}
|
||||
|
|
@ -61,7 +62,7 @@ struct SearchResultRow: View, Sendable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if showDescriptionsInSearchResults
|
||||
{
|
||||
if !descriptionParsingFailed
|
||||
|
|
@ -92,7 +93,6 @@ struct SearchResultRow: View, Sendable
|
|||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.task
|
||||
{
|
||||
|
|
@ -106,28 +106,27 @@ struct SearchResultRow: View, Sendable
|
|||
{
|
||||
isLoadingDescription = false
|
||||
}
|
||||
|
||||
|
||||
AppConstants.logger.info("\(searchedForPackage.name, privacy: .auto) does not have its description loaded")
|
||||
|
||||
do
|
||||
{
|
||||
let searchedForPackage: BrewPackage = .init(name: searchedForPackage.name, type: searchedForPackage.type, installedOn: Date(), versions: [], sizeInBytes: nil)
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
let parsedPackageInfo: BrewPackageDetails = try await searchedForPackage.loadDetails()
|
||||
|
||||
|
||||
description = parsedPackageInfo.description
|
||||
|
||||
|
||||
isCompatible = parsedPackageInfo.isCompatible
|
||||
}
|
||||
catch let descriptionParsingError
|
||||
{ // This happens when a package doesn' have any description at all, hence why we don't display an error
|
||||
AppConstants.logger.error("Failed while parsing searched-for package info: \(descriptionParsingError.localizedDescription, privacy: .public)")
|
||||
|
||||
|
||||
descriptionParsingFailed = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@ import SwiftUI
|
|||
struct InstallationInitialView: View
|
||||
{
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
@AppStorage("enableDiscoverability") var enableDiscoverability: Bool = false
|
||||
@AppStorage("discoverabilityDaySpan") var discoverabilityDaySpan: DiscoverabilityDaySpans = .month
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@EnvironmentObject var topPackagesTracker: TopPackagesTracker
|
||||
|
||||
|
||||
@ObservedObject var searchResultTracker: SearchResultTracker
|
||||
|
||||
@State private var isTopFormulaeSectionCollapsed: Bool = false
|
||||
|
|
@ -30,7 +30,7 @@ struct InstallationInitialView: View
|
|||
@Binding var foundPackageSelection: UUID?
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
|
||||
@Binding var packageInstallationProcessStep: PackageInstallationProcessSteps
|
||||
|
||||
@State var isSearchFieldFocused: Bool = true
|
||||
|
|
@ -65,8 +65,9 @@ struct InstallationInitialView: View
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
InstallProcessCustomSearchField(search: $packageRequested, isFocused: $isSearchFieldFocused, customPromptText: String(localized: "add-package.search.prompt")) {
|
||||
|
||||
InstallProcessCustomSearchField(search: $packageRequested, isFocused: $isSearchFieldFocused, customPromptText: String(localized: "add-package.search.prompt"))
|
||||
{
|
||||
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
|
||||
}
|
||||
|
||||
|
|
@ -83,9 +84,9 @@ struct InstallationInitialView: View
|
|||
if let foundPackageSelection
|
||||
{
|
||||
AppConstants.logger.debug("Would install package \(foundPackageSelection)")
|
||||
|
||||
|
||||
let topCasksSet = Set(topPackagesTracker.topCasks)
|
||||
|
||||
|
||||
var selectedTopPackageType: PackageType
|
||||
{
|
||||
// If this UUID is in the top casks tracker, it means it's a cask. Otherwise, it's a formula. So we test if the result of looking for the selected package in the cask tracker returns nothing; if it does return nothing, it's a formula (since the package is not in the cask tracker)
|
||||
|
|
@ -98,31 +99,30 @@ struct InstallationInitialView: View
|
|||
return .cask
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
do
|
||||
{
|
||||
let packageToInstall: BrewPackage = try getTopPackageFromUUID(requestedPackageUUID: foundPackageSelection, packageType: selectedTopPackageType, topPackageTracker: topPackagesTracker)
|
||||
|
||||
|
||||
installationProgressTracker.packageBeingInstalled = PackageInProgressOfBeingInstalled(package: packageToInstall, installationStage: .ready, packageInstallationProgress: 0)
|
||||
|
||||
|
||||
AppConstants.logger.debug("Packages to install: \(installationProgressTracker.packageBeingInstalled.package.name, privacy: .public)")
|
||||
|
||||
|
||||
packageInstallationProcessStep = .installing
|
||||
}
|
||||
catch let topPackageInstallationError
|
||||
{
|
||||
AppConstants.logger.error("Failed while trying to get top package to install: \(topPackageInstallationError, privacy: .public)")
|
||||
|
||||
|
||||
dismiss()
|
||||
|
||||
|
||||
appState.showAlert(errorToShow: .topPackageArrayFilterCouldNotRetrieveAnyPackages)
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.logger.warning("Could not find the UUID in the package list")
|
||||
//appState.showAlert(errorToShow: .couldNotFindPackageUUIDInList)
|
||||
// appState.showAlert(errorToShow: .couldNotFindPackageUUIDInList)
|
||||
}
|
||||
} label: {
|
||||
Text("add-package.install.action")
|
||||
|
|
|
|||
|
|
@ -9,17 +9,16 @@ import SwiftUI
|
|||
|
||||
struct TopPackageListItem: View
|
||||
{
|
||||
|
||||
let topPackage: TopPackage
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
HStack(alignment: .center)
|
||||
{
|
||||
SanitizedPackageName(packageName: topPackage.packageName, shouldShowVersion: true)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text("add-package.top-packages.list-item-\(topPackage.packageDownloads)")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ struct InstallingPackageView: View
|
|||
if [.installingCask, .installingPackage, .ready].contains(installationStage)
|
||||
{
|
||||
AppConstants.logger.warning("The installation process quit before it was supposed to")
|
||||
|
||||
|
||||
installationProgressTracker.packageBeingInstalled.installationStage = .terminatedUnexpectedly
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import SwiftUI
|
|||
struct BinaryAlreadyExistsView: View, Sendable
|
||||
{
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
ComplexWithImage(image: Image(localURL: URL(filePath: "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns"))!)
|
||||
|
|
@ -25,10 +25,11 @@ struct BinaryAlreadyExistsView: View, Sendable
|
|||
HeadlineWithSubheadline(
|
||||
headline: "add-package.install.binary-already-exists-\(installationProgressTracker.packageBeingInstalled.package.name)",
|
||||
subheadline: "add-package.install.binary-already-exists.subheadline",
|
||||
alignment: .leading)
|
||||
|
||||
alignment: .leading
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
HStack
|
||||
{
|
||||
Button
|
||||
|
|
@ -37,13 +38,13 @@ struct BinaryAlreadyExistsView: View, Sendable
|
|||
} label: {
|
||||
Text("action.reveal-applications-folder-in-finder")
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
|
||||
|
||||
Task.detached
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import SwiftUI
|
|||
struct InstallationTerminatedUnexpectedlyView: View
|
||||
{
|
||||
let terminalOutputOfTheInstallation: [RealTimeTerminalLine]
|
||||
|
||||
|
||||
@State private var usableLiveTerminalOutput: [RealTimeTerminalLine] = .init()
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
ComplexWithIcon(systemName: "xmark.seal")
|
||||
|
|
@ -24,10 +24,11 @@ struct InstallationTerminatedUnexpectedlyView: View
|
|||
subheadline: "add-package.install.installation-terminated.subheadline",
|
||||
alignment: .leading
|
||||
)
|
||||
|
||||
|
||||
DisclosureGroup
|
||||
{
|
||||
List {
|
||||
List
|
||||
{
|
||||
ForEach(usableLiveTerminalOutput)
|
||||
{ outputLine in
|
||||
Text(outputLine.line)
|
||||
|
|
@ -41,7 +42,7 @@ struct InstallationTerminatedUnexpectedlyView: View
|
|||
HStack
|
||||
{
|
||||
Spacer()
|
||||
|
||||
|
||||
DismissSheetButton()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import SwiftUI
|
|||
struct SudoRequiredView: View, Sendable
|
||||
{
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack(alignment: .leading)
|
||||
|
|
@ -68,26 +68,26 @@ struct SudoRequiredView: View, Sendable
|
|||
private struct ManualInstallInstructions: View
|
||||
{
|
||||
let installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
|
||||
var manualInstallCommand: String
|
||||
{
|
||||
return "brew install \(installationProgressTracker.packageBeingInstalled.package.type == .cask ? "--cask" : "") \(installationProgressTracker.packageBeingInstalled.package.name)"
|
||||
}
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack
|
||||
{
|
||||
Text("add-package.install.requires-sudo-password.description")
|
||||
|
||||
|
||||
GroupBox
|
||||
{
|
||||
HStack(alignment: .center, spacing: 5)
|
||||
{
|
||||
Text(manualInstallCommand)
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
manualInstallCommand.copyToClipboard()
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct WrongArchitectureView: View, Sendable {
|
||||
|
||||
struct WrongArchitectureView: View, Sendable
|
||||
{
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
var body: some View {
|
||||
|
||||
var body: some View
|
||||
{
|
||||
ComplexWithIcon(systemName: "cpu")
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
|
|
@ -23,16 +24,17 @@ struct WrongArchitectureView: View, Sendable {
|
|||
HeadlineWithSubheadline(
|
||||
headline: "add-package.install.wrong-architecture.title",
|
||||
subheadline: "add-package.install.wrong-architecture-\(installationProgressTracker.packageBeingInstalled.package.name).user-architecture-is-\(ProcessInfo().CPUArchitecture == .arm ? "Apple Silicon" : "Intel")",
|
||||
alignment: .leading)
|
||||
|
||||
alignment: .leading
|
||||
)
|
||||
|
||||
HStack
|
||||
{
|
||||
Spacer()
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
|
||||
|
||||
Task.detached
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
|
|
|
|||
|
|
@ -11,22 +11,23 @@ struct LicensingView: View
|
|||
{
|
||||
@AppStorage("demoActivatedAt") var demoActivatedAt: Date?
|
||||
@AppStorage("hasValidatedEmail") var hasValidatedEmail: Bool = false
|
||||
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack
|
||||
{
|
||||
switch appState.licensingState {
|
||||
case .notBoughtOrHasNotActivatedDemo:
|
||||
Licensing_NotBoughtOrActivatedView()
|
||||
case .demo:
|
||||
Licensing_DemoView()
|
||||
case .bought:
|
||||
Licensing_BoughtView()
|
||||
case .selfCompiled:
|
||||
Licensing_SelfCompiledView()
|
||||
switch appState.licensingState
|
||||
{
|
||||
case .notBoughtOrHasNotActivatedDemo:
|
||||
Licensing_NotBoughtOrActivatedView()
|
||||
case .demo:
|
||||
Licensing_DemoView()
|
||||
case .bought:
|
||||
Licensing_BoughtView()
|
||||
case .selfCompiled:
|
||||
Licensing_SelfCompiledView()
|
||||
}
|
||||
}
|
||||
.onAppear
|
||||
|
|
@ -38,7 +39,7 @@ struct LicensingView: View
|
|||
else
|
||||
{
|
||||
AppConstants.logger.debug("Has validated email? \(hasValidatedEmail ? "YES" : "NO")")
|
||||
|
||||
|
||||
if hasValidatedEmail
|
||||
{
|
||||
appState.licensingState = .bought
|
||||
|
|
|
|||
|
|
@ -9,29 +9,28 @@ import SwiftUI
|
|||
|
||||
struct Licensing_BoughtView: View
|
||||
{
|
||||
|
||||
@AppStorage("demoActivatedAt") var demoActivatedAt: Date?
|
||||
@AppStorage("hasFinishedLicensingWorkflow") var hasFinishedLicensingWorkflow: Bool = false
|
||||
@AppStorage("hasValidatedEmail") var hasValidatedEmail: Bool = false
|
||||
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack(alignment: .center, spacing: 15)
|
||||
{
|
||||
{
|
||||
Image(systemName: "checkmark.seal")
|
||||
.resizable()
|
||||
.foregroundColor(.green)
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
|
||||
Text("licensing.bought.title")
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("licensing.bought.body")
|
||||
|
||||
|
||||
HStack(alignment: .center, spacing: 20)
|
||||
{
|
||||
Button
|
||||
|
|
@ -53,9 +52,8 @@ struct Licensing_BoughtView: View
|
|||
.onAppear
|
||||
{
|
||||
demoActivatedAt = nil // Reset the demo, since it won't be needed anymore
|
||||
|
||||
|
||||
hasValidatedEmail = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import SwiftUI
|
|||
struct Licensing_DemoView: View
|
||||
{
|
||||
@AppStorage("demoActivatedAt") var demoActivatedAt: Date?
|
||||
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack(alignment: .center, spacing: 15)
|
||||
|
|
@ -23,9 +23,9 @@ struct Licensing_DemoView: View
|
|||
{
|
||||
Text("licensing.demo-activated.title")
|
||||
.font(.title)
|
||||
|
||||
|
||||
Text("licensing.demo.time-until-\((demoActivatedAt + AppConstants.demoLengthInSeconds).formatted(date: .complete, time: .complete))")
|
||||
|
||||
|
||||
HStack
|
||||
{
|
||||
Button
|
||||
|
|
@ -35,9 +35,9 @@ struct Licensing_DemoView: View
|
|||
Text("action.close")
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
appState.licensingState = .notBoughtOrHasNotActivatedDemo
|
||||
|
|
@ -52,4 +52,3 @@ struct Licensing_DemoView: View
|
|||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ struct Licensing_NotBoughtOrActivatedView: View
|
|||
catch let licenseCheckingError as CorkLicenseRetrievalError
|
||||
{
|
||||
AppConstants.logger.error("\(licenseCheckingError.localizedDescription, privacy: .public)")
|
||||
|
||||
|
||||
switch licenseCheckingError
|
||||
{
|
||||
case .authorizationComplexNotEncodedProperly:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Self-Compiled.swift
|
||||
// Self-Compiled View.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 13.05.2024.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ struct MaintenanceView: View
|
|||
shouldDeleteDownloads: shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: shouldPerformHealthCheck,
|
||||
numberOfOrphansRemoved: $numberOfOrphansRemoved,
|
||||
packagesHoldingBackCachePurge: $packagesHoldingBackCachePurge,
|
||||
packagesHoldingBackCachePurge: $packagesHoldingBackCachePurge,
|
||||
reclaimedSpaceAfterCachePurge: $reclaimedSpaceAfterCachePurge,
|
||||
brewHealthCheckFoundNoProblems: $brewHealthCheckFoundNoProblems,
|
||||
maintenanceSteps: $maintenanceSteps
|
||||
|
|
@ -72,7 +72,7 @@ struct MaintenanceView: View
|
|||
shouldPurgeCache: shouldPurgeCache,
|
||||
shouldDeleteDownloads: shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: shouldPerformHealthCheck,
|
||||
packagesHoldingBackCachePurge: packagesHoldingBackCachePurge,
|
||||
packagesHoldingBackCachePurge: packagesHoldingBackCachePurge,
|
||||
numberOfOrphansRemoved: numberOfOrphansRemoved,
|
||||
reclaimedSpaceAfterCachePurge: reclaimedSpaceAfterCachePurge,
|
||||
brewHealthCheckFoundNoProblems: brewHealthCheckFoundNoProblems,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue