~ More work… (at least the errors are in the app now)

This commit is contained in:
David Bureš 2025-11-10 18:27:54 +01:00
parent 6db193811a
commit 094caff1cb
No known key found for this signature in database
54 changed files with 285 additions and 418 deletions

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

@ -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: BrewPackage.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,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

@ -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

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

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

@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import CorkModels
protocol DismissablePane: 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 PackagePreview: View
{

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

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

View File

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

View File

@ -9,6 +9,7 @@ import SwiftUI
import CorkShared
import ButtonKit
import Defaults
import CorkModels
/// Button for uninstalling packages
struct UninstallPackageButton: View

View File

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

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import CorkModels
/// Package name that contains only the name of the package, not its version in the `package@version` format
struct SanitizedPackageName: View

View File

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

View File

@ -7,7 +7,7 @@
import Foundation
enum CachedDownloadDeletionError: LocalizedError
public enum CachedDownloadDeletionError: LocalizedError
{
case couldNotReadContentsOfCachedFormulaeDownloadsFolder(associatedError: String)
@ -15,7 +15,7 @@ enum CachedDownloadDeletionError: LocalizedError
case couldNotReadContentsOfCachedDownloadsFolder(associatedError: String)
var errorDescription: String?
public var errorDescription: String?
{
switch self
{

View File

@ -7,11 +7,11 @@
import Foundation
enum PackageSynchronizationError: LocalizedError
public enum PackageSynchronizationError: LocalizedError
{
case synchronizationReturnedNil
var errorDescription: String?
public var errorDescription: String?
{
switch self
{

View File

@ -7,9 +7,8 @@
import Foundation
import CorkShared
import CorkModels
extension CachedDownloadsTracker
public extension CachedDownloadsTracker
{
/// Load cached downloads and assign their types
@MainActor

View File

@ -8,8 +8,9 @@
import CorkShared
import Foundation
import SwiftUI
import CorkTerminalFunctions
extension BrewPackagesTracker
public extension BrewPackagesTracker
{
@MainActor
func uninstallSelectedPackage(

View File

@ -8,12 +8,13 @@
import CorkShared
import Foundation
import SwiftUI
import CorkModels
extension BrewPackagesTracker
public extension BrewPackagesTracker
{
/// Synchronizes installed packages and cached downloads
func synchronizeInstalledPackages(cachedDownloadsTracker: CachedDownloadsTracker) async throws(PackageSynchronizationError)
func synchronizeInstalledPackages(
cachedDownloadsTracker: CachedDownloadsTracker
) async throws(PackageSynchronizationError)
{
AppConstants.shared.logger.debug("Will start synchronization process")

View File

@ -11,11 +11,11 @@ import SwiftUI
import CorkModels
import CorkTerminalFunctions
enum OutdatedPackageRetrievalError: LocalizedError
public enum OutdatedPackageRetrievalError: LocalizedError
{
case homeNotSet, couldNotDecodeCommandOutput(String), otherError(String)
var errorDescription: String?
public var errorDescription: String?
{
switch self
{
@ -29,7 +29,7 @@ enum OutdatedPackageRetrievalError: LocalizedError
}
}
extension OutdatedPackagesTracker
public extension OutdatedPackagesTracker
{
/// This struct alows us to parse the JSON output of the outdated package function. It is not used outside this function
fileprivate struct OutdatedPackageCommandOutput: Codable
@ -78,7 +78,10 @@ extension OutdatedPackagesTracker
}
/// Load outdated packages into the outdated package tracker
private func getOutdatedPackagesInternal(brewPackagesTracker: BrewPackagesTracker, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async throws -> Set<OutdatedPackage>
private nonisolated func getOutdatedPackagesInternal(
brewPackagesTracker: BrewPackagesTracker,
forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType
) async throws -> Set<OutdatedPackage>
{
// First, we have to pull the newest updates
await shell(AppConstants.shared.brewExecutablePath, ["update"])
@ -137,13 +140,17 @@ extension OutdatedPackagesTracker
// MARK: - Helper functions
private func getOutdatedFormulae(from intermediaryArray: [OutdatedPackageCommandOutput.Formulae], brewPackagesTracker: BrewPackagesTracker, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async -> Set<OutdatedPackage>
private nonisolated func getOutdatedFormulae(
from intermediaryArray: [OutdatedPackageCommandOutput.Formulae],
brewPackagesTracker: BrewPackagesTracker,
forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType
) async -> Set<OutdatedPackage>
{
var finalOutdatedFormulaTracker: Set<OutdatedPackage> = .init()
for outdatedFormula in intermediaryArray
{
if let foundOutdatedFormula = brewPackagesTracker.successfullyLoadedFormulae.first(where: { $0.name == outdatedFormula.name })
if let foundOutdatedFormula = await brewPackagesTracker.successfullyLoadedFormulae.first(where: { $0.name == outdatedFormula.name })
{
finalOutdatedFormulaTracker.insert(.init(
package: foundOutdatedFormula,

View File

@ -8,7 +8,7 @@
import Foundation
import CorkModels
extension URL
public extension URL
{
/// Determine a package's type type from its URL
var packageType: BrewPackage.PackageType

View File

@ -7,9 +7,8 @@
import CorkShared
import Foundation
import CorkModels
extension BrewPackagesTracker
public extension BrewPackagesTracker
{
/// Parent function for loading installed packages from disk
/// Abstracts away the function ``loadInstalledPackagesFromFolder(packageTypeToLoad:)``, transforming errors thrown by ``loadInstalledPackagesFromFolder(packageTypeToLoad:)`` into displayable errors
@ -17,7 +16,7 @@ extension BrewPackagesTracker
/// - packageTypeToLoad: Which ``PackageType`` to load
/// - appState: ``AppState`` used to display loading errors
/// - Returns: A set of loaded ``BrewPackage``s for the specified ``PackageType``
public func loadInstalledPackages(
func loadInstalledPackages(
packageTypeToLoad: BrewPackage.PackageType, appState: AppState
) async -> BrewPackages?
{
@ -91,7 +90,7 @@ private extension BrewPackagesTracker
{
/// This gets URLs to all package folders in a folder.
/// `/opt/homebrew/Caskroom/microsoft-edge/`
let urlsInParentFolder: [URL] = try getContentsOfFolder(targetFolder: packageTypeToLoad.parentFolder, options: [.skipsHiddenFiles])
let urlsInParentFolder: [URL] = try packageTypeToLoad.parentFolder.getContents(options: [.skipsHiddenFiles])
AppConstants.shared.logger.debug("Loaded contents of folder: \(urlsInParentFolder)")
@ -207,7 +206,7 @@ private extension BrewPackagesTracker
{
/// Gets URL to installed versions of a package provided as ``packageURL``
/// `/opt/homebrew/Cellar/cmake/3.30.5`, `/opt/homebrew/Cellar/cmake/3.30.4`
let versionURLs: [URL] = try getContentsOfFolder(targetFolder: packageURL, options: [.skipsHiddenFiles])
let versionURLs: [URL] = try packageURL.getContents(options: [.skipsHiddenFiles])
guard !versionURLs.isEmpty
else

View File

@ -8,9 +8,8 @@
import CorkShared
import Foundation
import SwiftData
import CorkModels
extension BrewPackagesTracker
public extension BrewPackagesTracker
{
enum AdoptableCasksLoadingError: LocalizedError
{
@ -38,6 +37,7 @@ extension BrewPackagesTracker
}
/// Get a list of casks that can be adopted into the Homebrew updating mechanism
nonisolated
func getAdoptableCasks(
cacheUsePolicy: HomebrewDataCacheUsePolicy
) async throws(AdoptableCasksLoadingError) -> [AdoptableApp]

View File

@ -7,7 +7,6 @@
import CorkShared
import Foundation
import CorkModels
import CorkTerminalFunctions
enum BrewPackageInfoLoadingError: LocalizedError
@ -258,7 +257,7 @@ extension BrewPackage
/// Load package details
@MainActor
func loadDetails() async throws -> BrewPackageDetails
func loadDetails() async throws -> BrewPackage.BrewPackageDetails
{
let decoder: JSONDecoder = {
let decoder: JSONDecoder = .init()

View File

@ -18,7 +18,7 @@ public extension TapTracker
do
{
let contentsOfTapFolder: [URL] = try getContentsOfFolder(targetFolder: AppConstants.shared.tapPath, options: .skipsHiddenFiles)
let contentsOfTapFolder: [URL] = try AppConstants.shared.tapPath.getContents(options: .skipsHiddenFiles)
AppConstants.shared.logger.debug("Contents of tap folder: \(contentsOfTapFolder)")
@ -28,7 +28,7 @@ public extension TapTracker
do
{
let contentsOfTapRepoParent: [URL] = try getContentsOfFolder(targetFolder: tapRepoParentURL, options: .skipsHiddenFiles)
let contentsOfTapRepoParent: [URL] = try tapRepoParentURL.getContents(options: .skipsHiddenFiles)
for repoURL in contentsOfTapRepoParent
{

View File

@ -6,9 +6,8 @@
//
import Foundation
import CorkModels
extension BrewPackagesTracker
public extension BrewPackagesTracker
{
/// Update a ``BrewPackage``'s property in-place
/// Used to update the UI when a property on ``BrewPackage`` changes

View File

@ -0,0 +1,36 @@
//
// Get Contents of Folder.swift
// Cork
//
// Created by David Bureš - P on 09.11.2025.
//
import Foundation
import CorkShared
public extension URL
{
/// Get the contents of a folder as a list of URLs to items in it
func getContents(
options: FileManager.DirectoryEnumerationOptions? = nil
) throws -> [URL]
{
do
{
if let options
{
return try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil, options: options)
}
else
{
return try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil)
}
}
catch let folderReadingError
{
AppConstants.shared.logger.error("\(folderReadingError.localizedDescription)")
throw folderReadingError
}
}
}

View File

@ -22,16 +22,27 @@ public typealias BrewPackages = Set<Result<BrewPackage, BrewPackage.PackageLoadi
/// A representation of a Homebrew package
public struct BrewPackage: Identifiable, Equatable, Hashable, Codable, Sendable
{
public init(name: String, type: BrewPackage.PackageType, installedOn: Date?, versions: [String], url: URL?, sizeInBytes: Int64?, downloadCount: Int?) {
public init(
name: String,
type: BrewPackage.PackageType,
isTagged: Bool? = nil,
isPinned: Bool? = nil,
installedOn: Date?,
versions: [String],
url: URL?,
installedIntentionally: Bool? = nil,
sizeInBytes: Int64?,
downloadCount: Int?
) {
self.id = .init()
self.name = name
self.type = type
self.isTagged = false
self.isPinned = false
self.isTagged = isTagged ?? false
self.isPinned = isPinned ?? false
self.installedOn = installedOn
self.versions = versions
self.url = url
self.installedIntentionally = true
self.installedIntentionally = self.type == .cask ? true : installedIntentionally ?? false // If the package is cask, it was installed intentionally. If it's a formula, check if an override was provided, and if not, set it to false
self.sizeInBytes = sizeInBytes
self.downloadCount = downloadCount
self.isBeingModified = false
@ -43,14 +54,14 @@ public struct BrewPackage: Identifiable, Equatable, Hashable, Codable, Sendable
let type: PackageType
var isTagged: Bool = false
public var isPinned: Bool = false
public var isPinned: Bool
public let installedOn: Date?
public let versions: [String]
public let url: URL?
public var installedIntentionally: Bool = true
public var installedIntentionally: Bool
public let sizeInBytes: Int64?
@ -177,7 +188,7 @@ public struct BrewPackage: Identifiable, Equatable, Hashable, Codable, Sendable
isTagged.toggle()
}
enum PinnedStatus
public enum PinnedStatus
{
case pinned
case unpinned
@ -188,7 +199,7 @@ public struct BrewPackage: Identifiable, Equatable, Hashable, Codable, Sendable
/// Optionally specify which status to change the package to.
///
/// This function only changes the pinned status in the UI. Use the function ``performPinnedStatusChangeAction(appState:brewPackagesTracker:)`` to trigger a pinned status change in Homebrew.
mutating func changePinnedStatus(to status: PinnedStatus? = nil)
public mutating func changePinnedStatus(to status: PinnedStatus? = nil)
{
if let status
{
@ -208,7 +219,7 @@ public struct BrewPackage: Identifiable, Equatable, Hashable, Codable, Sendable
/// Perform a pinned status change in Homebrew.
///
/// For changing the pinned status of the package in the UI, use the function ``changePinnedStatus(to:)``
func performPinnedStatusChangeAction(appState: AppState, brewPackagesTracker: BrewPackagesTracker) async
public func performPinnedStatusChangeAction(appState: AppState, brewPackagesTracker: BrewPackagesTracker) async
{
/// We need to get the number of packages that were pinned before the action, because if there's only one and it gets unpinned, the whole folder with pinned packages is deleted - therefore, there would be a bug where unpinning the last package would make it seem like the whole process failed
async let numberOfPinnedPackagesBeforePinChangeAction: Int = await brewPackagesTracker.successfullyLoadedFormulae.filter { $0.isPinned }.count
@ -257,7 +268,7 @@ public struct BrewPackage: Identifiable, Equatable, Hashable, Codable, Sendable
await brewPackagesTracker.applyPinnedStatus(namesOfPinnedPackages: brewPackagesTracker.getNamesOfPinnedPackages(atPinnedPackagesPath: pinnedPackagesPath))
}
mutating func changeBeingModifiedStatus(to setState: Bool? = nil)
public mutating func changeBeingModifiedStatus(to setState: Bool? = nil)
{
let packageName: String = self.name

View File

@ -9,23 +9,23 @@ import Charts
import Foundation
import SwiftUI
struct CachedDownload: Identifiable, Hashable
public struct CachedDownload: Identifiable, Hashable
{
var id: UUID = .init()
public var id: UUID = .init()
let packageName: String
let sizeInBytes: Int
public let packageName: String
public let sizeInBytes: Int
var packageType: CachedDownload.CachedDownloadType?
public var packageType: CachedDownload.CachedDownloadType?
enum CachedDownloadType: String, CustomStringConvertible, Plottable
public enum CachedDownloadType: String, CustomStringConvertible, Plottable
{
case formula
case cask
case other
case unknown
var description: String
public var description: String
{
switch self
{
@ -40,7 +40,7 @@ struct CachedDownload: Identifiable, Hashable
}
}
var color: Color
public var color: Color
{
switch self
{

View File

@ -7,11 +7,9 @@
import Foundation
import CorkShared
import CorkModels
extension CachedDownloadsTracker
{
@MainActor
func assignPackageTypeToCachedDownloads(brewPackagesTracker: BrewPackagesTracker)
{
var cachedDownloadsTracker: [CachedDownload] = .init()

View File

@ -7,7 +7,7 @@
import Foundation
public struct OutdatedPackage: Identifiable, Equatable, Hashable
public struct OutdatedPackage: Identifiable, Equatable, Hashable, Sendable
{
public init(package: BrewPackage, installedVersions: [String], newerVersion: String, updatingManagedBy: PackageUpdatingType) {
self.package = package
@ -17,7 +17,7 @@ public struct OutdatedPackage: Identifiable, Equatable, Hashable
self.updatingManagedBy = updatingManagedBy
}
public enum PackageUpdatingType
public enum PackageUpdatingType: Sendable
{
/// The package is updating through Homebrew
case homebrew
@ -46,7 +46,7 @@ public struct OutdatedPackage: Identifiable, Equatable, Hashable
public var isMarkedForUpdating: Bool
public var updatingManagedBy: PackageUpdatingType
public let updatingManagedBy: PackageUpdatingType
public static func == (lhs: OutdatedPackage, rhs: OutdatedPackage) -> Bool
{

View File

@ -11,24 +11,24 @@ import Defaults
import DefaultsMacros
@Observable @MainActor
class OutdatedPackagesTracker
public class OutdatedPackagesTracker
{
@ObservableDefault(.displayOnlyIntentionallyInstalledPackagesByDefault) @ObservationIgnored var displayOnlyIntentionallyInstalledPackagesByDefault: Bool
@ObservableDefault(.includeGreedyOutdatedPackages) @ObservationIgnored var includeGreedyOutdatedPackages: Bool
enum OutdatedPackageDisplayStage: Equatable
public enum OutdatedPackageDisplayStage: Equatable
{
case checkingForUpdates, showingOutdatedPackages, noUpdatesAvailable, erroredOut(reason: String)
}
var isCheckingForPackageUpdates: Bool = true
public var isCheckingForPackageUpdates: Bool = true
var outdatedPackages: Set<OutdatedPackage> = .init()
public var outdatedPackages: Set<OutdatedPackage> = .init()
var errorOutReason: String?
public var errorOutReason: String?
var displayableOutdatedPackages: Set<OutdatedPackage>
public var displayableOutdatedPackages: Set<OutdatedPackage>
{
/// Depending on whether greedy updating is enabled:
/// - If enabled, include packages that are also self-updating
@ -54,7 +54,7 @@ class OutdatedPackagesTracker
}
}
var outdatedPackageDisplayStage: OutdatedPackageDisplayStage
public var outdatedPackageDisplayStage: OutdatedPackageDisplayStage
{
if let errorOutReason
{
@ -78,16 +78,13 @@ class OutdatedPackagesTracker
}
}
extension OutdatedPackagesTracker
public extension OutdatedPackagesTracker
{
func setOutdatedPackages(to packages: Set<OutdatedPackage>)
{
self.outdatedPackages = packages
}
}
extension OutdatedPackagesTracker
{
func checkForUpdates()
{
self.errorOutReason = nil

View File

@ -0,0 +1,97 @@
//
// Brew Package details.swift
// CorkModels
//
// Created by David Bureš - P on 10.11.2025.
//
import Foundation
import CorkShared
import CorkTerminalFunctions
enum PinningUnpinningError: LocalizedError
{
case failedWhileChangingPinnedStatus
var errorDescription: String?
{
switch self
{
case .failedWhileChangingPinnedStatus:
return String(localized: "error.package-details.couldnt-pin-unpin")
}
}
}
public extension BrewPackage
{
/// MOre information about this Homebrew package
@Observable @MainActor
class BrewPackageDetails
{
// MARK: - Immutable properties
/// Name of the package
public let name: String
public let description: String?
public let homepage: URL
public let tap: BrewTap
public let installedAsDependency: Bool
public let dependencies: [BrewPackageDependency]?
public let outdated: Bool
public let caveats: String?
public let deprecated: Bool
public let deprecationReason: String?
public let isCompatible: Bool?
public var dependents: [String]?
// MARK: - Init
public 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
public 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

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

View File

@ -142,7 +142,7 @@ public class BrewPackagesTracker
}
}
extension BrewPackagesTracker
public extension BrewPackagesTracker
{
var numberOfInstalledFormulae: Int

View File

@ -10,14 +10,14 @@ import SwiftUI
import CorkShared
@Observable @MainActor
class CachedDownloadsTracker
public class CachedDownloadsTracker
{
var cachedDownloads: [CachedDownload] = .init()
public var cachedDownloads: [CachedDownload] = .init()
private var cachedDownloadsTemp: [CachedDownload] = .init()
/// Calculate the size of the cached downloads dynamically without accessing the file system for the operation
var cachedDownloadsSize: Int
public var cachedDownloadsSize: Int
{
return cachedDownloads.reduce(0) { $0 + $1.sizeInBytes }
}

View File

@ -7,7 +7,7 @@
import Foundation
extension URL
public extension URL
{
var directorySize: Int64
{

View File

@ -5,14 +5,13 @@
// Created by David Bureš on 19.08.2023.
//
import CorkShared
import Foundation
enum DataDownloadingError: LocalizedError
public enum DataDownloadingError: LocalizedError
{
case invalidResponseCode(responseCode: Int?), noDataReceived, invalidURL, couldntExecuteRequest(error: String)
var errorDescription: String?
public var errorDescription: String?
{
switch self
{
@ -35,7 +34,7 @@ enum DataDownloadingError: LocalizedError
}
}
func downloadDataFromURL(
public func downloadDataFromURL(
_ url: URL,
parameters: [URLQueryItem]? = nil,
cachingPolicy: URLRequest.CachePolicy = .useProtocolCachePolicy

View File

@ -11,6 +11,14 @@ public struct TerminalOutput
{
public var standardOutput: String
public var standardError: String
public init(
standardOutput: String,
standardError: String
) {
self.standardOutput = standardOutput
self.standardError = standardError
}
}
public enum StreamedTerminalOutput

View File

@ -156,7 +156,9 @@ let corkModelsTarget: ProjectDescription.Target = .target(
dependencies: [
.target(corkSharedTarget),
.target(corkNotificationsTarget),
.external(name: "FactoryKit")
.external(name: "FactoryKit"),
.external(name: "Defaults"),
.external(name: "DefaultsMacros"),
],
settings: .settings(configurations: [
.debug(