Merge branch 'outdated-package-display-fixes.model-refactoring' into adoptable-discoverability.outdated-package-display-fixes

This commit is contained in:
David Bureš 2025-12-06 20:10:11 +01:00
commit 12570b1441
No known key found for this signature in database
216 changed files with 2820 additions and 1587 deletions

View File

@ -12,6 +12,7 @@ import SwiftUI
import CorkShared
import Defaults
import DefaultsMacros
import CorkModels
@Observable
class AppDelegate: NSObject, NSApplicationDelegate

View File

@ -11,6 +11,8 @@ import ButtonKit
import CorkShared
import Defaults
import SwiftUI
import CorkModels
import CorkTerminalFunctions
struct ContentView: View, Sendable
{

View File

@ -15,6 +15,8 @@ import Defaults
import SwiftData
import SwiftUI
import UserNotifications
import CorkModels
import CorkTerminalFunctions
@main
struct CorkApp: App
@ -90,7 +92,8 @@ struct CorkApp: App
.environment(outdatedPackagesTracker)
.environment(topPackagesTracker)
.modelContainer(for: [
SavedTaggedPackage.self
SavedTaggedPackage.self,
ExcludedAdoptableApp.self
])
.task
{
@ -131,146 +134,19 @@ struct CorkApp: App
}
.onAppear
{
print("Licensing state: \(appDelegate.appState.licensingState)")
#if SELF_COMPILED
AppConstants.shared.logger.debug("Will set licensing state to Self Compiled")
appDelegate.appState.licensingState = .selfCompiled
#else
if !hasValidatedEmail
{
if appDelegate.appState.licensingState != .selfCompiled
{
if let demoActivatedAt
{
let timeDemoWillRunOutAt: Date = demoActivatedAt + AppConstants.shared.demoLengthInSeconds
AppConstants.shared.logger.debug("There is \(demoActivatedAt.timeIntervalSinceNow.formatted()) to go on the demo")
AppConstants.shared.logger.debug("Demo will time out at \(timeDemoWillRunOutAt.formatted(date: .complete, time: .complete))")
if ((demoActivatedAt.timeIntervalSinceNow) + AppConstants.shared.demoLengthInSeconds) > 0
{ // Check if there is still time on the demo
/// do stuff if there is
}
else
{
hasFinishedLicensingWorkflow = false
}
}
}
}
#endif
handleLicensing()
}
.onAppear
{
// Start the background update scheduler when the app starts
backgroundUpdateTimer.schedule
{ (completion: NSBackgroundActivityScheduler.CompletionHandler) in
AppConstants.shared.logger.log("Scheduled event fired at \(Date(), privacy: .auto)")
Task
{
var updateResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["update"])
AppConstants.shared.logger.debug("Update result:\nStandard output: \(updateResult.standardOutput, privacy: .public)\nStandard error: \(updateResult.standardError, privacy: .public)")
do
{
let temporaryOutdatedPackageTracker: OutdatedPackagesTracker = await .init()
try await temporaryOutdatedPackageTracker.getOutdatedPackages(brewPackagesTracker: brewPackagesTracker)
var newOutdatedPackages: Set<OutdatedPackage> = await temporaryOutdatedPackageTracker.outdatedPackages
AppConstants.shared.logger.debug("Outdated packages checker output: \(newOutdatedPackages, privacy: .public)")
defer
{
AppConstants.shared.logger.log("Will purge temporary update trackers")
updateResult = .init(standardOutput: "", standardError: "")
newOutdatedPackages = .init()
}
if await newOutdatedPackages.count > outdatedPackagesTracker.outdatedPackages.count
{
AppConstants.shared.logger.log("New updates found")
/// Set this to `true` so the normal notification doesn't get sent
await setWhetherToSendStandardUpdatesAvailableNotification(to: false)
let differentPackages: Set<OutdatedPackage> = await newOutdatedPackages.subtracting(outdatedPackagesTracker.displayableOutdatedPackages)
AppConstants.shared.logger.debug("Changed packages: \(differentPackages, privacy: .auto)")
sendNotification(title: String(localized: "notification.new-outdated-packages-found.title"), subtitle: differentPackages.map(\.package.name).formatted(.list(type: .and)))
await outdatedPackagesTracker.setOutdatedPackages(to: newOutdatedPackages)
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
sendStandardUpdatesAvailableNotification = true
}
}
else
{
AppConstants.shared.logger.log("No new updates found")
}
}
catch
{
AppConstants.shared.logger.error("Something got fucked up about checking for outdated packages")
}
}
completion(NSBackgroundActivityScheduler.Result.finished)
}
handleBackgroundUpdating()
}
.onChange(of: demoActivatedAt) // React to when the user activates the demo
{ _, newValue in
if let newValue
{ // If the demo has not been activated, `demoActivatedAt` is nil. So, when it's not nil anymore, it means the user activated it
AppConstants.shared.logger.debug("The user activated the demo at \(newValue.formatted(date: .complete, time: .complete), privacy: .public)")
hasFinishedLicensingWorkflow = true
}
handleDemoTiming(newValue: newValue)
}
.onChange(of: outdatedPackagesTracker.displayableOutdatedPackages.count)
{ _, outdatedPackageCount in
AppConstants.shared.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
{
outdatedPackagesTracker.isCheckingForPackageUpdates = false
}
if outdatedPackageCount == 0
{
NSApp.dockTile.badgeLabel = ""
}
else
{
if areNotificationsEnabled
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
{
NSApp.dockTile.badgeLabel = String(outdatedPackageCount)
}
// TODO: Changing the package display type sends a notificaiton, which is not visible since the app is in the foreground. Once macOS 15 comes out, move `sendStandardUpdatesAvailableNotification` into the AppState and suppress it
if outdatedPackageNotificationType == .notification || outdatedPackageNotificationType == .both
{
AppConstants.shared.logger.log("Will try to send notification")
/// This needs to be checked because when the background update system finds an update, we don't want to send this normal notification.
/// Instead, we want to send a more succinct notification that includes only the new package
if sendStandardUpdatesAvailableNotification
{
sendNotification(title: String(localized: "notification.outdated-packages-found.title"), subtitle: String(localized: "notification.outdated-packages-found.body-\(outdatedPackageCount)"))
}
}
}
}
handleOutdatedPackageChangeAppBadge(outdatedPackageCount: outdatedPackageCount)
}
.onChange(of: outdatedPackageNotificationType) // Set the correct app badge number when the user changes their notification settings
{ _, newValue in
@ -415,7 +291,7 @@ struct CorkApp: App
WindowGroup(id: .previewWindowID, for: MinimalHomebrewPackage.self)
{ $packageToPreview in
let convertedMinimalPackage: BrewPackage? = .init(from: packageToPreview)
let convertedMinimalPackage: BrewPackage? = BrewPackage(using: packageToPreview)
PackagePreview(packageToPreview: convertedMinimalPackage)
.navigationTitle(packageToPreview?.name ?? "")
@ -716,7 +592,7 @@ struct CorkApp: App
{
Button
{
openWindow(id: .errorInspectorWindowID, value: PackageLoadingError.packageIsNotAFolder("Hello I am an error", packageURL: .applicationDirectory).localizedDescription)
openWindow(id: .errorInspectorWindowID, value: BrewPackage.PackageLoadingError.packageIsNotAFolder("Hello I am an error", packageURL: .applicationDirectory).localizedDescription)
} label: {
Text("debug.action.show-error-inspector")
}
@ -724,9 +600,10 @@ struct CorkApp: App
Text("debug.action.ui")
}
}
// MARK: - Functions
// MARK: - App badge
func setAppBadge(outdatedPackageNotificationType: OutdatedPackageNotificationType)
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
@ -741,9 +618,160 @@ struct CorkApp: App
NSApp.dockTile.badgeLabel = ""
}
}
private func setWhetherToSendStandardUpdatesAvailableNotification(to newValue: Bool)
{
self.sendStandardUpdatesAvailableNotification = newValue
}
func handleOutdatedPackageChangeAppBadge(outdatedPackageCount: Int)
{
AppConstants.shared.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
{
outdatedPackagesTracker.isCheckingForPackageUpdates = false
}
if outdatedPackageCount == 0
{
NSApp.dockTile.badgeLabel = ""
}
else
{
if areNotificationsEnabled
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
{
NSApp.dockTile.badgeLabel = String(outdatedPackageCount)
}
// TODO: Changing the package display type sends a notificaiton, which is not visible since the app is in the foreground. Once macOS 15 comes out, move `sendStandardUpdatesAvailableNotification` into the AppState and suppress it
if outdatedPackageNotificationType == .notification || outdatedPackageNotificationType == .both
{
AppConstants.shared.logger.log("Will try to send notification")
/// This needs to be checked because when the background update system finds an update, we don't want to send this normal notification.
/// Instead, we want to send a more succinct notification that includes only the new package
if sendStandardUpdatesAvailableNotification
{
sendNotification(title: String(localized: "notification.outdated-packages-found.title"), subtitle: String(localized: "notification.outdated-packages-found.body-\(outdatedPackageCount)"))
}
}
}
}
}
// MARK: - Background updating
func handleBackgroundUpdating()
{
// Start the background update scheduler when the app starts
backgroundUpdateTimer.schedule
{ (completion: NSBackgroundActivityScheduler.CompletionHandler) in
AppConstants.shared.logger.log("Scheduled event fired at \(Date(), privacy: .auto)")
Task
{
var updateResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["update"])
AppConstants.shared.logger.debug("Update result:\nStandard output: \(updateResult.standardOutput, privacy: .public)\nStandard error: \(updateResult.standardError, privacy: .public)")
do
{
let temporaryOutdatedPackageTracker: OutdatedPackagesTracker = await .init()
try await temporaryOutdatedPackageTracker.getOutdatedPackages(brewPackagesTracker: brewPackagesTracker)
var newOutdatedPackages: Set<OutdatedPackage> = await temporaryOutdatedPackageTracker.outdatedPackages
AppConstants.shared.logger.debug("Outdated packages checker output: \(newOutdatedPackages, privacy: .public)")
defer
{
AppConstants.shared.logger.log("Will purge temporary update trackers")
updateResult = .init(standardOutput: "", standardError: "")
newOutdatedPackages = .init()
}
if await newOutdatedPackages.count > outdatedPackagesTracker.outdatedPackages.count
{
AppConstants.shared.logger.log("New updates found")
/// Set this to `true` so the normal notification doesn't get sent
await setWhetherToSendStandardUpdatesAvailableNotification(to: false)
let differentPackages: Set<OutdatedPackage> = await newOutdatedPackages.subtracting(outdatedPackagesTracker.displayableOutdatedPackages)
AppConstants.shared.logger.debug("Changed packages: \(differentPackages, privacy: .auto)")
sendNotification(title: String(localized: "notification.new-outdated-packages-found.title"), subtitle: differentPackages.map(\.package.name).formatted(.list(type: .and)))
await outdatedPackagesTracker.setOutdatedPackages(to: newOutdatedPackages)
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
sendStandardUpdatesAvailableNotification = true
}
}
else
{
AppConstants.shared.logger.log("No new updates found")
}
}
catch
{
AppConstants.shared.logger.error("Something got fucked up about checking for outdated packages")
}
}
completion(NSBackgroundActivityScheduler.Result.finished)
}
}
// MARK: - Licensing
func handleLicensing()
{
print("Licensing state: \(appDelegate.appState.licensingState)")
#if SELF_COMPILED
AppConstants.shared.logger.debug("Will set licensing state to Self Compiled")
appDelegate.appState.licensingState = .selfCompiled
#else
if !hasValidatedEmail
{
if appDelegate.appState.licensingState != .selfCompiled
{
if let demoActivatedAt
{
let timeDemoWillRunOutAt: Date = demoActivatedAt + AppConstants.shared.demoLengthInSeconds
AppConstants.shared.logger.debug("There is \(demoActivatedAt.timeIntervalSinceNow.formatted()) to go on the demo")
AppConstants.shared.logger.debug("Demo will time out at \(timeDemoWillRunOutAt.formatted(date: .complete, time: .complete))")
if ((demoActivatedAt.timeIntervalSinceNow) + AppConstants.shared.demoLengthInSeconds) > 0
{ // Check if there is still time on the demo
/// do stuff if there is
}
else
{
hasFinishedLicensingWorkflow = false
}
}
}
}
#endif
}
func handleDemoTiming(newValue: Date?)
{
if let newValue
{ // If the demo has not been activated, `demoActivatedAt` is nil. So, when it's not nil anymore, it means the user activated it
AppConstants.shared.logger.debug("The user activated the demo at \(newValue.formatted(date: .complete, time: .complete), privacy: .public)")
hasFinishedLicensingWorkflow = true
}
}
}

View File

@ -1,13 +0,0 @@
//
// Brewfile Import Stage.swift
// Cork
//
// Created by David Bureš on 11.11.2023.
//
import Foundation
enum BrewfileImportStage
{
case importing, finished
}

View File

@ -1,18 +0,0 @@
//
// Licensing State.swift
// Cork
//
// Created by David Bureš on 18.03.2024.
//
import Foundation
enum LicensingState
{
case notBoughtOrHasNotActivatedDemo
case demo
case bought
case selfCompiled
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import CorkModels
enum NavigationTargetMainWindow: Hashable
{

View File

@ -1,48 +0,0 @@
//
// Outdated Package Type.swift
// Cork
//
// Created by David Bureš on 17.05.2024.
//
import Charts
import Foundation
import SwiftUI
enum CachedDownloadType: String, CustomStringConvertible, Plottable
{
case formula
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")
}
}
var color: Color
{
switch self
{
case .formula:
return .purple
case .cask:
return .orange
case .other:
return .mint
default:
return .gray
}
}
}

View File

@ -1,79 +0,0 @@
//
// Package Types.swift
// Cork
//
// Created by David Bureš on 05.02.2023.
//
import AppIntents
import Charts
import CorkShared
import Foundation
import SwiftUI
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable
{
case formula
case cask
/// User-readable description of the package type
var description: String
{
switch self
{
case .formula:
return String(localized: "package-details.type.formula")
case .cask:
return String(localized: "package-details.type.cask")
}
}
/// Localization keys for description of the package type
var localizableDescription: LocalizedStringKey
{
switch self
{
case .formula:
return "package-details.type.formula"
case .cask:
return "package-details.type.cask"
}
}
/// Parent folder for this package type
var parentFolder: URL
{
switch self
{
case .formula:
return AppConstants.shared.brewCellarPath
case .cask:
return AppConstants.shared.brewCaskPath
}
}
/// Accessibility representation
var accessibilityLabel: LocalizedStringKey
{
switch self
{
case .formula:
return "accessibility.label.package-type.formula"
case .cask:
return "accessibility.label.package-type.cask"
}
}
static let 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")
}
}
}

View File

@ -1,24 +0,0 @@
//
// JSON Parsing Error.swift
// Cork
//
// Created by David Bureš on 21.06.2024.
//
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)")
}
}
}

View File

@ -1,70 +0,0 @@
//
// Package Loading Error.swift
// Cork
//
// Created by David Bureš on 10.11.2024.
//
import Foundation
/// Error representing failures while loading
enum PackageLoadingError: LocalizedError, Hashable, Identifiable
{
/// Tried to treat the folder `Cellar` or `Caskroom` itself as a package - means Homebrew itself is broken
case triedToThreatFolderContainingPackagesAsPackage(packageType: PackageType)
/// The `Cellar` and `Caskroom` folder itself couldn't be loaded
case couldNotReadContentsOfParentFolder(failureReason: String, folderURL: URL)
/// Failed while trying to read contents of package folder
case failedWhileReadingContentsOfPackageFolder(folderURL: URL, reportedError: String)
case failedWhileTryingToDetermineIntentionalInstallation(folderURL: URL, associatedIntentionalDiscoveryError: IntentionalInstallationDiscoveryError)
/// The package root folder exists, but the package itself doesn't have any versions
case packageDoesNotHaveAnyVersionsInstalled(packageURL: URL)
/// A folder that should have contained the package is not actually a folder
case packageIsNotAFolder(String, packageURL: URL)
/// The number of loaded packages does not match the number of package parent folders
case numberOLoadedPackagesDosNotMatchNumberOfPackageFolders
var errorDescription: String?
{
switch self
{
case .couldNotReadContentsOfParentFolder(let failureReason, _):
return String(localized: "error.package-loading.could-not-read-contents-of-parent-folder.\(failureReason)")
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
switch packageType
{
case .formula:
return "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.formulae"
case .cask:
return "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.casks"
}
case .failedWhileReadingContentsOfPackageFolder(let folderURL, let reportedError):
return String(localized: "error.package-loading.could-not-load-\(folderURL.packageNameFromURL())-at-\(folderURL.absoluteString)-because-\(reportedError)", comment: "Couldn't load package (package name) at (package URL) because (failure reason)")
case .failedWhileTryingToDetermineIntentionalInstallation(_, let associatedIntentionalDiscoveryError):
return associatedIntentionalDiscoveryError.localizedDescription
case .packageDoesNotHaveAnyVersionsInstalled(let packageURL):
return String(localized: "error.package-loading.\(packageURL.packageNameFromURL())-does-not-have-any-versions-installed")
case .packageIsNotAFolder(let string, _):
return String(localized: "error.package-loading.\(string)-not-a-folder", comment: "Package folder in this context means a folder that encloses package versions. Every package has its own folder, and this error occurs when the provided URL does not point to a folder that encloses package versions")
case .numberOLoadedPackagesDosNotMatchNumberOfPackageFolders:
return String(localized: "error.package-loading.number-of-loaded-poackages-does-not-match-number-of-package-folders", comment: "This error occurs when there's a mismatch between the number of loaded packages, and the number of package folders in the package folders")
}
}
var id: UUID
{
return UUID()
}
}

View File

@ -6625,6 +6625,38 @@
}
}
},
"action.hide-adoptable-packages-section-if-only-excluded-apps-available" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hide section…"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Masquer la section…"
}
}
}
},
"action.hide-adoptable-packages-section-if-only-excluded-apps-available.confirm" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hide section"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Masquer la section"
}
}
}
},
"action.inspect-error" : {
"localizations" : {
"cs" : {
@ -6883,6 +6915,24 @@
}
}
},
"action.package-adoption.include.%@" : {
"comment" : "A menu item that allows the user to include a package in the app store. The argument is the name of the package, and the second argument is the system image for the plus sign.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Stop ignoring %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ne plus ignorer %@"
}
}
}
},
"action.preview-package-app-would-be-adopted-as.%@" : {
"comment" : "A context menu item that previews the app that would be adopted. The argument is a localized string describing the app that would be adopted.",
"isCommentAutoGenerated" : true,
@ -15589,6 +15639,27 @@
}
}
},
"adoptable-packages.excluded-label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignored adoptable apps"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Applications adoptables ignorées"
}
}
}
},
"adoptable-packages.excluded.label" : {
"comment" : "A label for the section that lists apps that are excluded from being adopted.",
"isCommentAutoGenerated" : true,
"shouldTranslate" : false
},
"adoptable-packages.label" : {
"comment" : "The label for the disclosure group that lists adoptable packages.",
"isCommentAutoGenerated" : true,
@ -21576,6 +21647,11 @@
}
}
},
"DEBUG: Log packages to be adopted" : {
"comment" : "A button that logs to the debug log all the packages that would be adopted.",
"isCommentAutoGenerated" : true,
"shouldTranslate" : false
},
"debug.action.activate-demo" : {
"localizations" : {
"cs" : {
@ -22607,7 +22683,8 @@
"value" : "error.data-downloading.couldnt-execute-request.%@"
}
}
}
},
"shouldTranslate" : false
},
"error.data-downloading.invalid-response.%lld" : {
"localizations" : {
@ -27008,6 +27085,38 @@
}
}
},
"hide-adoptable-packages-section-if-only-excluded-apps-available.confirmation.message" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "When a new adoptable app becomes available, this section will automatically re-appear.\n\nYou can show the section again at any time in the “Discoverability“ tab of Corks Settings"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Lorsquune nouvelle application devient disponible, cette section re-apparaitra automatiquement.\n\nVous pouvez afficher la section à tout moment dans longlet « Découvrabilité » des paramètres de Cork"
}
}
}
},
"hide-adoptable-packages-section-if-only-excluded-apps-available.confirmation.title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Are you sure you want to hide the Adoptable Packages section?"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Êtes vous sûr de vouloir masquer la section des Paquets Adoptable ?"
}
}
}
},
"homebrew/core" : {
"localizations" : {
"cs" : {
@ -48779,6 +48888,40 @@
}
}
},
"settings.discoverability.mass-adoption.hide-adoptable-packages-section-if-there-are-only-excluded-apps-available.label" : {
"comment" : "A label describing a toggle that controls whether the \"Adoptable Packages\" section is hidden when there are only excluded apps available.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hide when all adoptable apps are excluded"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Masquer lorsque toutes les applications adoptables sont exclues"
}
}
}
},
"settings.discoverability.mass-adoption.label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "App adoption:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Adoption dapplication :"
}
}
}
},
"settings.discoverability.mass-adoption.toggle" : {
"comment" : "A toggle that allows or disallows mass adoption of packages.",
"isCommentAutoGenerated" : true,
@ -58104,6 +58247,106 @@
}
}
},
"start-page.adoptable-packages.available.%lld-excluded.%lld" : {
"comment" : "A headline that describes how many adoptable packages are available, including any that have been excluded. The first argument is the count of adoptable packages that are not excluded. The second argument is the count of adoptable packages that have been excluded.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "There are %1$lld installed apps -excluded.%2$lld"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il y a %1$lld applications installées (exclues : %2$lld)."
}
}
}
},
"start-page.adoptable-packages.excluded.%lld" : {
"comment" : "A subheading displaying the number of apps that will not be adopted. The argument is the count of apps that will not be adopted.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "There is %lld additional app that is ignored"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "There are %lld additional apps that are ignored"
}
}
}
}
},
"fr" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il y a %lld application additionnelle qui est ignorée"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il y a %lld applications additionnelles qui sont ignorées"
}
}
}
}
}
}
},
"start-page.adoptable-packages.only-%lld-excluded-available" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "There is %lld ignored adoptable apps"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "There are %lld ignored adoptable apps"
}
}
}
}
},
"fr" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il y a %lld application adoptable qui est ignorée"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "Il y a %lld applications adoptables qui sont ignorées"
}
}
}
}
}
}
},
"start-page.analytics.disabled" : {
"localizations" : {
"cs" : {

View File

@ -7,6 +7,8 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
extension MassAppAdoptionView.MassAppAdoptionTacker
{

View File

@ -8,16 +8,20 @@
import AppIntents
import Foundation
import CorkShared
import CorkModels
import CorkIntents
struct GetInstalledCasksIntent: AppIntent
public struct GetInstalledCasksIntent: AppIntent
{
static let title: LocalizedStringResource = "intent.get-installed-casks.title"
static let description: LocalizedStringResource = "intent.get-installed-casks.description"
public init() {}
public static let title: LocalizedStringResource = "intent.get-installed-casks.title"
public static let description: LocalizedStringResource = "intent.get-installed-casks.description"
static let isDiscoverable: Bool = true
static let openAppWhenRun: Bool = false
public static let isDiscoverable: Bool = true
public static let openAppWhenRun: Bool = false
func perform() async throws -> some ReturnsValue<[MinimalHomebrewPackage]>
public func perform() async throws -> some ReturnsValue<[MinimalHomebrewPackage]>
{
let allowAccessToFile: Bool = AppConstants.shared.brewCaskPath.startAccessingSecurityScopedResource()

View File

@ -8,6 +8,7 @@
import AppIntents
import Foundation
import CorkShared
import CorkModels
enum FolderAccessingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import AppIntents
import Foundation
import CorkModels
struct GetInstalledPackagesIntent: AppIntent
{

View File

@ -8,6 +8,7 @@
import AppIntents
import Foundation
import CorkShared
import CorkTerminalFunctions
enum RefreshIntentResult: String, AppEnum
{

View File

@ -7,6 +7,8 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
enum BrewfileDumpingError: LocalizedError
{

View File

@ -7,6 +7,8 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
enum BrewfileReadingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkModels
enum TopPackageLoadingError: LocalizedError
{

View File

@ -1,177 +0,0 @@
//
// Get Contents of Folder.swift
// Cork
//
// Created by David Bureš on 03.07.2022.
//
import Foundation
import SwiftUI
import CorkShared
/*
func getContentsOfFolder(targetFolder: URL) async throws -> Set<BrewPackage>
{
do
{
guard let items = targetFolder.validPackageURLs
else
{
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "alert.fatal.could-not-filter-invalid-packages"))
}
let loadedPackages: Set<BrewPackage> = try await withThrowingTaskGroup(of: BrewPackage.self, returning: Set<BrewPackage>.self)
{ taskGroup in
for item in items
{
let fullURLToPackageFolderCurrentlyBeingProcessed: URL = targetFolder.appendingPathComponent(item, conformingTo: .folder)
taskGroup.addTask(priority: .high)
{
guard let versionURLs: [URL] = fullURLToPackageFolderCurrentlyBeingProcessed.packageVersionURLs
else
{
if targetFolder.appendingPathComponent(item, conformingTo: .fileURL).isDirectory
{
AppConstants.shared.logger.error("Failed while getting package version for package \(fullURLToPackageFolderCurrentlyBeingProcessed.lastPathComponent). Package does not have any version installed.")
throw PackageLoadingError.packageDoesNotHaveAnyVersionsInstalled(item)
}
else
{
AppConstants.shared.logger.error("Failed while getting package version for package \(fullURLToPackageFolderCurrentlyBeingProcessed.lastPathComponent). Package is not a folder")
throw PackageLoadingError.packageIsNotAFolder(item, targetFolder.appendingPathComponent(item, conformingTo: .fileURL))
}
}
do
{
if versionURLs.isEmpty
{
throw PackageLoadingError.packageDoesNotHaveAnyVersionsInstalled(item)
}
let wasPackageInstalledIntentionally: Bool = try await targetFolder.checkIfPackageWasInstalledIntentionally(versionURLs)
let foundPackage: BrewPackage = .init(
name: item,
type: targetFolder.packageType,
installedOn: fullURLToPackageFolderCurrentlyBeingProcessed.creationDate,
versions: versionURLs.versions,
installedIntentionally: wasPackageInstalledIntentionally,
sizeInBytes: fullURLToPackageFolderCurrentlyBeingProcessed.directorySize
)
return foundPackage
}
catch
{
throw error
}
}
}
var loadedPackages: Set<BrewPackage> = .init()
for try await package in taskGroup
{
loadedPackages.insert(package)
}
return loadedPackages
}
return loadedPackages
}
catch
{
AppConstants.shared.logger.error("Failed while accessing folder: \(error)")
throw error
}
}
*/
// MARK: - Sub-functions
private extension URL
{
/// ``[URL]`` to packages without hidden files or symlinks.
/// e.g. only actual package URLs
var validPackageURLs: [String]?
{
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)
guard let isSymlink = completeURLtoItem.isSymlink()
else
{
return false
}
return !isSymlink
}
return items
}
/// Get URLs to a package's versions
var packageVersionURLs: [URL]?
{
AppConstants.shared.logger.debug("Will check URL \(self)")
do
{
let versions: [URL] = try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: [.isHiddenKey], options: .skipsHiddenFiles)
if versions.isEmpty
{
AppConstants.shared.logger.warning("Package URL \(self, privacy: .public) has no versions installed")
return nil
}
AppConstants.shared.logger.debug("URL \(self) has these versions: \(versions))")
return versions
}
catch
{
AppConstants.shared.logger.error("Failed while loading version for package \(lastPathComponent, privacy: .public) at URL \(self, privacy: .public)")
return nil
}
}
}
extension [URL]
{
/// Returns an array of versions from an array of URLs to available versions
var versions: [String]
{
return map
{ versionURL in
versionURL.lastPathComponent
}
}
}
// MARK: - Getting list of URLs in folder
func getContentsOfFolder(targetFolder: URL, options: FileManager.DirectoryEnumerationOptions? = nil) throws -> [URL]
{
do
{
if let options
{
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil, options: options)
}
else
{
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil)
}
}
catch let folderReadingError
{
AppConstants.shared.logger.error("\(folderReadingError.localizedDescription)")
throw folderReadingError
}
}

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkModels
func deleteCachedDownloads() throws(CachedDownloadDeletionError)
{
@ -15,7 +16,7 @@ func deleteCachedDownloads() throws(CachedDownloadDeletionError)
/// This folder has the symlinks, so we have do **delete ONLY THE SYMLINKS**
do
{
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedFormulaeDownloadsPath)
for url in try AppConstants.shared.brewCachedFormulaeDownloadsPath.getContents()
{
if let isSymlink = url.isSymlink()
{
@ -47,7 +48,7 @@ func deleteCachedDownloads() throws(CachedDownloadDeletionError)
/// This folder has the symlinks, so we have to **delete ONLY THE SYMLINKS**
do
{
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedCasksDownloadsPath)
for url in try AppConstants.shared.brewCachedCasksDownloadsPath.getContents()
{
if let isSymlink = url.isSymlink()
{
@ -79,7 +80,7 @@ func deleteCachedDownloads() throws(CachedDownloadDeletionError)
/// This folder has the downloads themselves, so we have do **DELETE EVERYTHING THAT IS NOT A SYMLINK**
do
{
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedDownloadsPath)
for url in try AppConstants.shared.brewCachedDownloadsPath.getContents()
{
if let isSymlink = url.isSymlink()
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum HealthCheckError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
/* enum CachePurgeError: Error
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum OrphanUninstallationError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum HomebrewCachePurgeError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum OrphanRemovalError: LocalizedError
{

View File

@ -1,54 +0,0 @@
//
// Load up Installed Packages.swift
// Cork
//
// Created by David Bureš on 11.02.2023.
//
import CorkShared
import Foundation
/*
@MainActor
func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<BrewPackage>
{
AppConstants.shared.logger.info("Started \(whatToLoad == .formula ? "Formula" : "Cask", privacy: .public) loading task at \(Date(), privacy: .public)")
var contentsOfFolder: Set<BrewPackage> = .init()
do
{
switch whatToLoad
{
case .formula:
contentsOfFolder = try await getContentsOfFolder(targetFolder: AppConstants.shared.brewCellarPath)
case .cask:
contentsOfFolder = try await getContentsOfFolder(targetFolder: AppConstants.shared.brewCaskPath)
}
}
catch let packageLoadingError as PackageLoadingError
{
switch packageLoadingError
{
case .couldNotReadContentsOfParentFolder(let failureReason):
appState.showAlert(errorToShow: .couldNotGetContentsOfPackageFolder(failureReason))
case .failedWhileLoadingPackages:
appState.showAlert(errorToShow: .couldNotLoadAnyPackages(packageLoadingError))
case .failedWhileLoadingCertainPackage(let offendingPackage, let offendingPackageURL, let failureReason):
appState.showAlert(errorToShow: .couldNotLoadCertainPackage(offendingPackage, offendingPackageURL, failureReason: failureReason))
case .packageDoesNotHaveAnyVersionsInstalled(let offendingPackage):
appState.showAlert(errorToShow: .installedPackageHasNoVersions(corruptedPackageName: offendingPackage))
case .packageIsNotAFolder(let offendingFile, let offendingFileURL):
appState.showAlert(errorToShow: .installedPackageIsNotAFolder(itemName: offendingFile, itemURL: offendingFileURL))
}
}
catch
{
print("Something got completely fucked up while loading packages")
}
AppConstants.shared.logger.info("Finished \(whatToLoad == .formula ? "Formula" : "Cask", privacy: .public) loading task at \(Date(), privacy: .auto)")
return contentsOfFolder
}
*/

View File

@ -7,8 +7,10 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
import CorkModels
func searchForPackage(packageName: String, packageType: PackageType) async -> [String]
func searchForPackage(packageName: String, packageType: BrewPackage.PackageType) async -> [String]
{
var finalPackageArray: [String]

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum HomebrewServiceLoadingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
extension ServicesTracker
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
extension ServicesTracker
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
enum ServiceStoppingError: LocalizedError
{

View File

@ -7,6 +7,7 @@
import Foundation
import CorkShared
import CorkTerminalFunctions
extension HomebrewService
{

View File

@ -1,37 +0,0 @@
//
// Parse Tap Info.swift
// Cork
//
// Created by David Bureš on 21.06.2024.
//
import Foundation
import CorkShared
func parseTapInfo(from rawJSON: String) async throws -> TapInfo?
{
let decoder: JSONDecoder = {
let decoder: JSONDecoder = .init()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
do
{
guard let jsonAsData: Data = rawJSON.data(using: .utf8, allowLossyConversion: false)
else
{
AppConstants.shared.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
{
AppConstants.shared.logger.error("Failed while decoding tap info: \(decodingError.localizedDescription, privacy: .public)\n-\(decodingError, privacy: .public)")
throw JSONParsingError.couldNotDecode(failureReason: decodingError.localizedDescription)
}
}

View File

@ -8,6 +8,8 @@
import Foundation
import SwiftUI
import CorkShared
import CorkTerminalFunctions
import CorkModels
@MainActor
func refreshPackages(_ updateProgressTracker: UpdateProgressTracker, outdatedPackagesTracker: OutdatedPackagesTracker) async -> PackageUpdateAvailability

View File

@ -8,6 +8,7 @@
import Foundation
import SwiftUI
import CorkShared
import CorkTerminalFunctions
@MainActor
func updatePackages(updateProgressTracker: UpdateProgressTracker, detailStage: UpdatingProcessDetails) async

View File

@ -1,141 +0,0 @@
//
// Application.swift
// Cork
//
// Created by David Bureš - P on 07.10.2025.
//
import Foundation
import SwiftUI
import CorkShared
// TODO: Move this over to the `ApplicationInspector` external library once we figure out how to use Tuist projects as external dependencies
public struct Application: Identifiable, Hashable, Sendable
{
public let id: UUID = .init()
public let name: String
public let url: URL
public let iconPath: URL?
public let iconImage: Image?
public static func == (lhs: Application, rhs: Application) -> Bool
{
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher)
{
hasher.combine(name)
hasher.combine(id)
}
public enum ApplicationInitializationError: LocalizedError
{
public enum MandatoryAppInformation: String, Sendable
{
case name
public var description: String
{
switch self {
case .name:
return "Name"
}
}
}
case applicationExecutableNotReadable(checkedPath: String)
case couldNotAccessApplicationExecutable(error: Error)
case couldNotReadBundle(applicationPath: String)
case couldNotGetInfoDictionary
case couldNotGetMandatoryAppInformation(_ mandatoryInformation: MandatoryAppInformation)
public var errorDescription: String?
{
switch self {
case .applicationExecutableNotReadable(let checkedPath):
return "Couldn't read application executable at \(checkedPath)"
case .couldNotAccessApplicationExecutable(let error):
return "Couldn't read application executable: \(error)"
case .couldNotReadBundle(let applicationPath):
return "Couldn't read application bundle at \(applicationPath)"
case .couldNotGetInfoDictionary:
return "Couldn't read application info.plist"
case .couldNotGetMandatoryAppInformation(let mandatoryInformation):
return "Couldn't read mandatory app information: \(mandatoryInformation.description)"
}
}
}
public init(from appURL: URL) throws(ApplicationInitializationError)
{
do
{
guard FileManager.default.isReadableFile(atPath: appURL.path) == true else
{
throw ApplicationInitializationError.applicationExecutableNotReadable(checkedPath: appURL.path)
}
guard let appBundle: Bundle = .init(url: appURL)
else
{
throw ApplicationInitializationError.couldNotReadBundle(applicationPath: appURL.absoluteString)
}
AppConstants.shared.logger.debug("Will try to initialize and App object form bundle \(appBundle)")
guard let appBundleInfoDictionary: [String: Any] = appBundle.infoDictionary
else
{
throw ApplicationInitializationError.couldNotGetInfoDictionary
}
guard let appName: String = Application.getAppName(fromInfoDictionary: appBundleInfoDictionary) else
{
throw ApplicationInitializationError.couldNotGetMandatoryAppInformation(.name)
}
self.name = appName
self.url = appURL
self.iconPath = Application.getAppIconPath(fromInfoDictionary: appBundleInfoDictionary, appBundle: appBundle)
if let iconPath = self.iconPath
{
self.iconImage = .init(
nsImage: .init(byReferencing: iconPath)
)
}
else
{
self.iconImage = nil
}
}
catch let applicationDirectoryAccessError
{
throw .couldNotAccessApplicationExecutable(error: applicationDirectoryAccessError)
}
}
private static func getAppName(fromInfoDictionary infoDictionary: [String: Any]) -> String?
{
return infoDictionary["CFBundleName"] as? String
}
private static func getAppIconPath(fromInfoDictionary infoDictionary: [String: Any], appBundle: Bundle) -> URL?
{
guard let iconFileName: String = infoDictionary["CFBundleIconFile"] as? String
else
{
return nil
}
return appBundle.resourceURL?.appendingPathComponent(iconFileName, conformingTo: .icns)
}
}

View File

@ -1,21 +0,0 @@
//
// Brew Tap.swift
// Cork
//
// Created by David Bureš on 10.02.2023.
//
import Foundation
struct BrewTap: Identifiable, Hashable
{
let id: UUID = .init()
let name: String
var isBeingModified: Bool = false
mutating func changeBeingModifiedStatus()
{
isBeingModified.toggle()
}
}

View File

@ -1,20 +0,0 @@
//
// Cached Download.swift
// Cork
//
// Created by David Bureš on 04.11.2023.
//
import Charts
import Foundation
import SwiftUI
struct CachedDownload: Identifiable, Hashable
{
var id: UUID = .init()
let packageName: String
let sizeInBytes: Int
var packageType: CachedDownloadType?
}

View File

@ -1,14 +0,0 @@
//
// Corrupted Package.swift
// Cork
//
// Created by David Bureš on 28.03.2024.
//
import Foundation
struct CorruptedPackage: Identifiable, Equatable
{
let id: UUID = .init()
let name: String
}

View File

@ -10,6 +10,7 @@ import SwiftUI
import CorkShared
import Defaults
import DefaultsMacros
import CorkModels
@Observable @MainActor
class TopPackagesTracker

View File

@ -1,60 +0,0 @@
//
// Minimal Homebrew Package.swift
// Cork
//
// Created by David Bureš on 25.05.2024.
//
import AppIntents
import Foundation
struct MinimalHomebrewPackage: Identifiable, Hashable, AppEntity, Codable
{
var id: UUID = .init()
var name: String
var type: PackageType
var installDate: Date?
var installedIntentionally: Bool
static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "intents.type.minimal-homebrew-package")
var displayRepresentation: DisplayRepresentation
{
DisplayRepresentation(
title: "\(name)",
subtitle: "intents.type.minimal-homebrew-package.representation.subtitle"
)
}
static let defaultQuery: MinimalHomebrewPackageIntentQuery = .init()
}
extension MinimalHomebrewPackage
{
init?(from homebrewPackage: BrewPackage?)
{
guard let homebrewPackage = homebrewPackage
else
{
return nil
}
self.init(
name: homebrewPackage.name,
type: homebrewPackage.type,
installedIntentionally: homebrewPackage.installedIntentionally
)
}
}
struct MinimalHomebrewPackageIntentQuery: EntityQuery
{
func entities(for _: [UUID]) async throws -> [MinimalHomebrewPackage]
{
return .init()
}
}

View File

@ -1,75 +0,0 @@
//
// Tap Codable Model.swift
// Cork
//
// Created by David Bureš on 21.06.2024.
//
import Foundation
/// Decodable tap info
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?
var numberOfPackages: Int
{
return self.formulaNames.count + self.caskTokens.count
}
/// Formulae that include the package type. Useful for rpeviewing packages.
var includedFormulaeWithAdditionalMetadata: [MinimalHomebrewPackage]
{
return formulaNames.map
{ formulaName in
.init(name: formulaName, type: .formula, installedIntentionally: false)
}
}
var includedCasksWithAdditionalMetadata: [MinimalHomebrewPackage]
{
return caskTokens.map
{ caskName in
.init(name: caskName, type: .cask, installedIntentionally: false)
}
}
}

View File

@ -1,51 +0,0 @@
//
// Outdated Package.swift
// Cork
//
// Created by David Bureš on 05.04.2023.
//
import Foundation
struct OutdatedPackage: Identifiable, Equatable, Hashable
{
enum PackageUpdatingType
{
/// The package is updating through Homebrew
case homebrew
/// The package updates itself
case selfUpdating
var argument: String
{
switch self
{
case .homebrew:
return .init()
case .selfUpdating:
return "--greedy"
}
}
}
let id: UUID = .init()
let package: BrewPackage
let installedVersions: [String]
let newerVersion: String
var isMarkedForUpdating: Bool = true
var updatingManagedBy: PackageUpdatingType
static func == (lhs: OutdatedPackage, rhs: OutdatedPackage) -> Bool
{
return lhs.package.name == rhs.package.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(package.name)
}
}

View File

@ -7,6 +7,8 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
@Observable
class InstallationProgressTracker

View File

@ -6,6 +6,7 @@
//
import Foundation
import CorkModels
struct RealTimeTerminalLine: Identifiable, Hashable, Equatable
{

View File

@ -1,92 +0,0 @@
//
// Brew Package Details.swift
// Cork
//
// Created by David Bureš on 18.07.2024.
//
import Foundation
import CorkShared
enum PinningUnpinningError: LocalizedError
{
case failedWhileChangingPinnedStatus
var errorDescription: String?
{
switch self
{
case .failedWhileChangingPinnedStatus:
return String(localized: "error.package-details.couldnt-pin-unpin")
}
}
}
@Observable @MainActor
class BrewPackageDetails
{
// MARK: - Immutable properties
/// Name of the package
let name: String
let description: String?
let homepage: URL
let tap: BrewTap
let installedAsDependency: Bool
let dependencies: [BrewPackageDependency]?
let outdated: Bool
let caveats: String?
let deprecated: Bool
let deprecationReason: String?
let isCompatible: Bool?
var dependents: [String]?
// MARK: - Init
init(
name: String,
description: String?,
homepage: URL,
tap: BrewTap,
installedAsDependency: Bool,
dependents: [String]? = nil,
dependencies: [BrewPackageDependency]? = nil,
outdated: Bool,
caveats: String? = nil,
deprecated: Bool,
deprecationReason: String? = nil,
isCompatible: Bool?
){
self.name = name
self.description = description
self.homepage = homepage
self.tap = tap
self.installedAsDependency = installedAsDependency
self.dependents = dependents
self.dependencies = dependencies
self.outdated = outdated
self.deprecated = deprecated
self.deprecationReason = deprecationReason
self.caveats = caveats
self.isCompatible = isCompatible
}
// MARK: - Functions
func loadDependents() async
{
AppConstants.shared.logger.debug("Will load dependents for \(self.name)")
let packageDependentsRaw: String = await shell(AppConstants.shared.brewExecutablePath, ["uses", "--installed", name]).standardOutput
let finalDependents: [String] = packageDependentsRaw.components(separatedBy: "\n").dropLast()
AppConstants.shared.logger.debug("Dependents loaded: \(finalDependents)")
dependents = finalDependents
}
}

View File

@ -1,16 +0,0 @@
//
// Package Dependency.swift
// Cork
//
// Created by David Bureš on 27.02.2023.
//
import Foundation
struct BrewPackageDependency: Identifiable, Hashable
{
let id: UUID = .init()
let name: String
let version: String
let directlyDeclared: Bool
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import CorkModels
@Observable
class SearchResultTracker

View File

@ -1,20 +0,0 @@
//
// Terminal Output.swift
// Cork
//
// Created by David Bureš on 12.02.2023.
//
import Foundation
struct TerminalOutput
{
var standardOutput: String
var standardError: String
}
enum StreamedTerminalOutput
{
case standardOutput(String)
case standardError(String)
}

View File

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import CorkModels
protocol DismissablePane: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct BrewfileImportProgressView: View
{

View File

@ -10,6 +10,7 @@ import CorkShared
import SwiftUI
import ButtonKit
import Defaults
import CorkModels
struct AddFormulaView: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct SearchResultRow: View, Sendable
{
@ -130,7 +131,7 @@ struct SearchResultRow: View, Sendable
do
{
let parsedPackageInfo: BrewPackageDetails = try await searchedForPackage.loadDetails()
let parsedPackageInfo: BrewPackage.BrewPackageDetails = try await searchedForPackage.loadDetails()
description = parsedPackageInfo.description

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct InstallationInitialView: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct TopPackagesSection: View
{
@ -15,7 +16,7 @@ struct TopPackagesSection: View
let packageTracker: TopPackagesTracker
let trackerType: PackageType
let trackerType: BrewPackage.PackageType
private var packages: [BrewPackage]
{

View File

@ -7,6 +7,8 @@
import SwiftUI
import CorkShared
import CorkModels
import CorkTerminalFunctions
struct InstallingPackageView: View
{

View File

@ -7,6 +7,7 @@
import CorkShared
import SwiftUI
import CorkModels
struct PresentingSearchResultsView: View
{
@ -186,7 +187,7 @@ struct PresentingSearchResultsView: View
private struct SearchResultsSection: View
{
let sectionType: PackageType
let sectionType: BrewPackage.PackageType
let packageList: [BrewPackage]

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct InstallationSearchingView: View, Sendable
{

View File

@ -7,6 +7,8 @@
import CorkShared
import SwiftUI
import CorkModels
import CorkTerminalFunctions
struct AdoptingAlreadyInstalledCaskView: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct AnotherProcessAlreadyRunningView: View
{

View File

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import CorkModels
struct BinaryAlreadyExistsView: View, Sendable
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct InstallationFatalErrorView: View
{

View File

@ -9,6 +9,7 @@ import CorkNotifications
import CorkShared
import SwiftUI
import Defaults
import CorkModels
struct InstallationFinishedSuccessfullyView: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct SudoRequiredView: View, Sendable
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct WrongArchitectureView: View, Sendable
{

View File

@ -8,6 +8,7 @@
import CorkShared
import SwiftUI
import Defaults
import CorkModels
struct LicensingView: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import Defaults
import CorkModels
struct Licensing_BoughtView: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import Defaults
import CorkModels
struct Licensing_DemoView: View
{

View File

@ -9,6 +9,7 @@ import SwiftUI
import CorkShared
import ButtonKit
import Defaults
import CorkModels
struct Licensing_NotBoughtOrActivatedView: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import Defaults
import CorkModels
struct Licensing_SelfCompiledView: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
enum MaintenanceSteps
{

View File

@ -8,6 +8,7 @@
import CorkShared
import SwiftUI
import Defaults
import CorkModels
struct MaintenanceFinishedView: View
{

View File

@ -7,6 +7,8 @@
import SwiftUI
import CorkShared
import CorkModels
import CorkTerminalFunctions
struct MaintenanceRunningView: View
{

View File

@ -37,7 +37,8 @@ struct AdoptionResultsList: View
}
}
}
.frame(height: 100, alignment: .leading)
.listStyle(.bordered(alternatesRowBackgrounds: true))
.frame(minHeight: 100)
}
}
}

View File

@ -7,6 +7,7 @@
import CorkShared
import SwiftUI
import CorkModels
struct MassAdoptionStage_Adopting: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
typealias AdoptionProcessResult = Result<BrewPackagesTracker.AdoptableApp, MassAppAdoptionView.AdoptionAttemptFailure>

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBarItem: View
{

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import CorkNotifications
import CorkModels
struct MenuBar_CachedDownloadsCleanup: View
{

View File

@ -9,6 +9,7 @@ import ButtonKit
import CorkNotifications
import CorkShared
import SwiftUI
import CorkModels
struct MenuBar_OrphanCleanup: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBar_PackageInstallation: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBar_PackageOverview: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct MenuBar_PackageUpdating: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct PackagePreview: View
{

View File

@ -7,6 +7,8 @@
import CorkShared
import SwiftUI
import CorkModels
import ApplicationInspector
struct PackageDetailView: View, Sendable, DismissablePane
{
@ -39,7 +41,7 @@ struct PackageDetailView: View, Sendable, DismissablePane
var isInPreviewWindow: Bool = false
@State private var packageDetails: BrewPackageDetails? = nil
@State private var packageDetails: BrewPackage.BrewPackageDetails? = nil
@State private var caskExecutable: Application? = nil
@ -238,10 +240,10 @@ private extension BrewPackagesTracker
struct FastPackageComparableRepresentation: Hashable
{
let name: String
let type: PackageType
let type: BrewPackage.PackageType
let versions: [String]
init(name: String, type: PackageType, versions: [String])
init(name: String, type: BrewPackage.PackageType, versions: [String])
{
self.name = name
self.type = type

View File

@ -8,13 +8,14 @@
import CorkShared
import Defaults
import SwiftUI
import CorkModels
struct BasicPackageInfoView: View
{
@Default(.caveatDisplayOptions) var caveatDisplayOptions: PackageCaveatDisplay
let package: BrewPackage
let packageDetails: BrewPackageDetails
let packageDetails: BrewPackage.BrewPackageDetails
let isLoadingDetails: Bool

View File

@ -7,6 +7,7 @@
import SwiftUI
import Defaults
import CorkModels
struct DependencyList: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct PackageDependencies: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import CorkShared
import CorkModels
struct PackageDetailHeaderComplex: View
{
@ -22,7 +23,7 @@ struct PackageDetailHeaderComplex: View
var isInPreviewWindow: Bool
@Bindable var packageDetails: BrewPackageDetails
@Bindable var packageDetails: BrewPackage.BrewPackageDetails
let isLoadingDetails: Bool

View File

@ -9,6 +9,7 @@ import SwiftUI
import CorkShared
import ButtonKit
import Defaults
import CorkModels
struct PackageModificationButtons: View
{
@ -21,7 +22,7 @@ struct PackageModificationButtons: View
@Environment(OutdatedPackagesTracker.self) var outdatedPackagesTracker: OutdatedPackagesTracker
let package: BrewPackage
@Bindable var packageDetails: BrewPackageDetails
@Bindable var packageDetails: BrewPackage.BrewPackageDetails
let isLoadingDetails: Bool

View File

@ -7,6 +7,7 @@
import SwiftUI
import ButtonKit
import CorkModels
struct PinUnpinButton: View
{

View File

@ -6,6 +6,8 @@
//
import SwiftUI
import ApplicationInspector
import CorkModels
struct PackageSystemInfo: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct PackageListItem: View
{

View File

@ -8,6 +8,8 @@
import Foundation
import SwiftUI
import CorkShared
import CorkModels
import CorkTerminalFunctions
struct ReinstallCorruptedPackageView: View
{

View File

@ -7,6 +7,7 @@
import SwiftUI
import ButtonKit
import CorkModels
struct SudoRequiredForRemovalSheet: View, Sendable
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import ApplicationInspector
/// Show the icon of a linked app
struct AppIconDisplay: View

View File

@ -7,6 +7,7 @@
import SwiftUI
import CorkShared
import CorkModels
struct CheckForOutdatedPackagesButton: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct DeleteCachedDownloadsButton: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct OpenMaintenanceSheetButton: View
{

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
struct InstallPackageButton: View
{

Some files were not shown because too many files have changed in this diff Show More