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

# Conflicts:
#	Cork/Localizable.xcstrings
#	Cork/Logic/App Adoption/Adopt Package.swift
#	Cork/Models/Packages/Brew Package Details.swift
#	Cork/Views/Mass App Adoption/Components/Adoption Results List.swift
#	Cork/Views/Mass App Adoption/Mass Adoption Stages/Mass Adoption Stage - Adopting.swift
#	Cork/Views/Mass App Adoption/Mass App Adoption View.swift
#	Cork/Views/Packages/Package Details/Sub-Views/Package System Info.swift
#	Cork/Views/Start Page/Start Page.swift
#	Cork/Views/Start Page/Sub-Views/Adoptable Packages Box.swift
#	Modules/Packages/PackagesModels/Logic/Package Loading/Load Up Installed Packages.swift
#	Modules/Packages/PackagesModels/Logic/Packages/Load Up Package Info.swift
#	Modules/Packages/PackagesModels/Trackers/Brew Packages Tracker.swift
#	Modules/Shared/Logic/Networking/Download Data From URL.swift
#	Project.swift
This commit is contained in:
David Bureš 2025-12-06 20:12:21 +01:00
commit 5ce0743699
No known key found for this signature in database
217 changed files with 5557 additions and 1522 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 ?? "")
@ -657,13 +533,8 @@ struct CorkApp: App
.environment(appDelegate.appState)
.environment(outdatedPackagesTracker)
Button
{
appDelegate.appState.showSheet(ofType: .fullUpdate)
} label: {
Text("navigation.menu.packages.update")
}
.keyboardShortcut("r", modifiers: [.control, .command])
UpgradePackagesButton(appState: appDelegate.appState)
.keyboardShortcut("r", modifiers: [.control, .command])
}
@ViewBuilder
@ -721,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")
}
@ -729,9 +600,10 @@ struct CorkApp: App
Text("debug.action.ui")
}
}
// MARK: - Functions
// MARK: - App badge
func setAppBadge(outdatedPackageNotificationType: OutdatedPackageNotificationType)
{
if outdatedPackageNotificationType == .badge || outdatedPackageNotificationType == .both
@ -746,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()
}
}

File diff suppressed because it is too large Load Diff

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
{
@ -99,7 +100,15 @@ extension TopPackagesTracker
if normalizedDownloadNumber > downloadsCutoff
{
return .init(name: rawTopFormula.formula, type: .formula, installedOn: nil, versions: .init(), sizeInBytes: nil, downloadCount: normalizedDownloadNumber)
return .init(
name: rawTopFormula.formula,
type: .formula,
installedOn: nil,
versions: .init(),
url: nil,
sizeInBytes: nil,
downloadCount: normalizedDownloadNumber
)
}
else
{
@ -155,7 +164,15 @@ extension TopPackagesTracker
if normalizedDownloadNumber > downloadsCutoff
{
return .init(name: rawTopCask.cask, type: .cask, installedOn: nil, versions: .init(), sizeInBytes: nil, downloadCount: normalizedDownloadNumber)
return .init(
name: rawTopCask.cask,
type: .cask,
installedOn: nil,
versions: .init(),
url: nil,
sizeInBytes: nil,
downloadCount: normalizedDownloadNumber
)
}
else
{

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,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,11 +7,13 @@
import Foundation
import CorkShared
import CorkModels
import CorkTerminalFunctions
@Observable
class InstallationProgressTracker
{
var packageBeingInstalled: PackageInProgressOfBeingInstalled = .init(package: .init(name: "", type: .formula, installedOn: nil, versions: [], sizeInBytes: 0, downloadCount: nil), installationStage: .downloadingCask, packageInstallationProgress: 0)
var packageBeingInstalled: PackageInProgressOfBeingInstalled = .init(package: .init(name: "", type: .formula, installedOn: nil, versions: [], url: nil, sizeInBytes: 0, downloadCount: nil), installationStage: .downloadingCask, packageInstallationProgress: 0)
var numberOfPackageDependencies: Int = 0
var numberInLineOfPackageCurrentlyBeingFetched: Int = 0
@ -205,7 +207,7 @@ class InstallationProgressTracker
AppConstants.shared.logger.info("Package is Cask")
AppConstants.shared.logger.debug("Installing package \(package.name, privacy: .public)")
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", package.name])
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", package.name])
installationProcess = process
for await output in stream
{

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
{
@ -126,11 +127,11 @@ struct SearchResultRow: View, Sendable
do
{
let searchedForPackage: BrewPackage = .init(name: searchedForPackage.name, type: searchedForPackage.type, installedOn: Date(), versions: [], sizeInBytes: nil, downloadCount: nil)
let searchedForPackage: BrewPackage = .init(name: searchedForPackage.name, type: searchedForPackage.type, installedOn: Date(), versions: [], url: nil, sizeInBytes: nil, downloadCount: nil)
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
{
@ -129,6 +130,7 @@ struct PresentingSearchResultsView: View
AppConstants.shared.logger.debug("Would preview package \(selectedPackage.name)")
}
.disabled(foundPackageSelection == nil)
.labelStyle(.titleOnly)
}
@ViewBuilder
@ -185,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
{
@ -28,11 +29,11 @@ struct InstallationSearchingView: View, Sendable
for formula in await foundFormulae
{
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil, downloadCount: nil))
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], url: nil, sizeInBytes: nil, downloadCount: nil))
}
for cask in await foundCasks
{
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil, downloadCount: nil))
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], url: nil, sizeInBytes: nil, downloadCount: nil))
}
packageInstallationProcessStep = .presentingSearchResults

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

@ -27,23 +27,11 @@ struct InstallationTerminatedUnexpectedlyView: View
if usableLiveTerminalOutput.isEmpty
{
OutlinedPillText(text: "add-package.install.installation-terminated.no-terminal-output-provided", color: .secondary)
noOutputProvided
}
else
{
DisclosureGroup
{
List
{
ForEach(usableLiveTerminalOutput)
{ outputLine in
Text(outputLine.line)
}
}
.frame(height: 100, alignment: .leading)
} label: {
Text("action.show-terminal-output")
}
someOutputProvided
}
}
}
@ -54,4 +42,34 @@ struct InstallationTerminatedUnexpectedlyView: View
usableLiveTerminalOutput = terminalOutputOfTheInstallation
}
}
@ViewBuilder
var noOutputProvided: some View
{
Text("add-package.install.installation-terminated.no-terminal-output-provided")
.font(.subheadline)
.foregroundStyle(.secondary)
.frame(alignment: .leading)
Spacer()
}
@ViewBuilder
var someOutputProvided: some View
{
DisclosureGroup
{
List
{
ForEach(usableLiveTerminalOutput)
{ outputLine in
Text(outputLine.line)
}
}
.frame(maxHeight: 100, alignment: .leading)
} label: {
Text("action.show-terminal-output")
}
}
}

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,9 @@ 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
@Environment(BrewPackagesTracker.self) var brewPackagesTracker: BrewPackagesTracker
@ -92,9 +96,15 @@ struct PackageDetailView: View, Sendable, DismissablePane
isShowingExpandedCaveats: $isShowingExpandedCaveats
)
PackageDependencies(dependencies: packageDetails?.dependencies, isDependencyDisclosureGroupExpanded: $isShowingExpandedDependencies)
PackageDependencies(
dependencies: packageDetails?.dependencies,
isDependencyDisclosureGroupExpanded: $isShowingExpandedDependencies
)
PackageSystemInfo(package: packageStructureToUse)
PackageSystemInfo(
package: packageStructureToUse,
caskExecutable: caskExecutable
)
}
}
}
@ -157,6 +167,19 @@ struct PackageDetailView: View, Sendable, DismissablePane
erroredOut = (true, packageInfoDecodingError.localizedDescription)
}
}
.task(id: package.id)
{ // For casks, try to load the application executable
if package.type == .cask
{
AppConstants.shared.logger.info("Package is cask, will see what the app's location is for url \(package.url as NSObject?)")
if let packageURL = package.url
{
AppConstants.shared.logger.info("Will try to load app icon for URL \(packageURL)")
caskExecutable = try? .init(from: packageURL)
}
}
}
}
}
@ -217,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
{
@ -43,7 +44,7 @@ struct DependencyList: View
{
TableColumn("package-details.dependencies.results.name")
{ dependency in
SanitizedPackageName(package: .init(name: dependency.name, type: .formula, installedOn: nil, versions: [dependency.version], sizeInBytes: nil, downloadCount: nil), shouldShowVersion: false)
SanitizedPackageName(package: .init(name: dependency.name, type: .formula, installedOn: nil, versions: [dependency.version], url: nil, sizeInBytes: nil, downloadCount: nil), shouldShowVersion: false)
}
TableColumn("package-details.dependencies.results.version")
{ dependency in

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
@ -36,6 +37,7 @@ struct PackageModificationButtons: View
if package.type == .formula
{
PinUnpinButton(package: package)
.labelStyle(.titleOnly)
}
Spacer()
@ -52,12 +54,14 @@ struct PackageModificationButtons: View
if !allowMoreCompleteUninstallations
{
UninstallPackageButton(package: package)
.labelStyle(.titleOnly)
}
else
{
Menu
{
PurgePackageButton(package: package)
.labelStyle(.titleOnly)
} label: {
Text("action.uninstall-\(package.name)")
} primaryAction: {

View File

@ -7,6 +7,7 @@
import SwiftUI
import ButtonKit
import CorkModels
struct PinUnpinButton: View
{
@ -23,7 +24,9 @@ struct PinUnpinButton: View
{
await package.performPinnedStatusChangeAction(appState: appState, brewPackagesTracker: brewPackagesTracker)
} label: {
Text(package.isPinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))")
let labelText: LocalizedStringKey = package.isPinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))"
Label(labelText, systemImage: "pin.fill")
}
.asyncButtonStyle(.leading)
.disabledWhenLoading()

View File

@ -6,10 +6,14 @@
//
import SwiftUI
import ApplicationInspector
import CorkModels
struct PackageSystemInfo: View
{
let package: BrewPackage
let caskExecutable: Application?
@State private var isShowingCaskSizeHelpPopover: Bool = false
@ -19,22 +23,57 @@ struct PackageSystemInfo: View
{
Section
{
LabeledContent
{
Text(installedOnDate.formatted(.packageInstallationStyle))
} label: {
Text("package-details.install-date")
}
caskInstalledAsLine
installedOnDateLine(installedOnDate)
if let packageSize = package.sizeInBytes
{
LabeledContent
{
Text(packageSize.formatted(.byteCount(style: .file)))
} label: {
Text("package-details.size")
}
}
packageSizeLine(package.sizeInBytes)
}
}
}
@ViewBuilder
var caskInstalledAsLine: some View
{
if let caskExecutable
{
LabeledContent
{
AppIconDisplay(
displayType: .asIconWithAppNameDisplayed(
usingApp: caskExecutable,
namePosition: .besideAppIcon
),
allowRevealingInFinderFromIcon: true
)
} label: {
Text("package-details.installed-as")
}
}
}
@ViewBuilder
func installedOnDateLine(_ installedOnDate: Date) -> some View
{
LabeledContent
{
Text(installedOnDate.formatted(.packageInstallationStyle))
} label: {
Text("package-details.install-date")
}
}
@ViewBuilder
func packageSizeLine(_ packageSize: Int64?) -> some View
{
if let packageSize = package.sizeInBytes
{
LabeledContent
{
Text(packageSize.formatted(.byteCount(style: .file)))
} label: {
Text("package-details.size")
}
}
}

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

@ -0,0 +1,134 @@
//
// App Icon Display.swift
// Cork
//
// Created by David Bureš - P on 22.10.2025.
//
import SwiftUI
import ApplicationInspector
/// Show the icon of a linked app
struct AppIconDisplay: View
{
enum DisplayType
{
case asIcon(
usingApp: Application
)
case asIconWithAppNameDisplayed(
usingApp: Application,
namePosition: AppNamePosition
)
case asPathControl(
usingURL: URL
)
enum AppNamePosition
{
case besideAppIcon
case underAppIcon
}
}
let displayType: DisplayType
let allowRevealingInFinderFromIcon: Bool
var body: some View
{
switch displayType
{
case .asIcon(let usingApp):
ApplicationIconImage(
app: usingApp,
allowRevealingInFinderThroughIcon: allowRevealingInFinderFromIcon
)
case .asIconWithAppNameDisplayed(let usingApp, let namePosition):
switch namePosition
{
case .besideAppIcon:
HStack(alignment: .center, spacing: 5)
{
ApplicationIconImage(
app: usingApp,
allowRevealingInFinderThroughIcon: allowRevealingInFinderFromIcon
)
applicationName(app: usingApp)
}
case .underAppIcon:
VStack(alignment: .center, spacing: 5)
{
ApplicationIconImage(
app: usingApp,
allowRevealingInFinderThroughIcon: allowRevealingInFinderFromIcon
)
applicationName(app: usingApp)
}
}
case .asPathControl(let usingURL):
AppIconDisplay_AsPathControl(urlToApp: usingURL)
}
}
@ViewBuilder
func applicationName(app: Application) -> some View
{
Text(app.name)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
private struct ApplicationIconImage: View
{
let app: Application
let allowRevealingInFinderThroughIcon: Bool
var body: some View
{
if let appIconImage = app.iconImage
{
appIconImage
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35)
.contextMenu {
Button
{
app.url.revealInFinder(.openParentDirectoryAndHighlightTarget)
} label: {
Label("action.reveal-\(app.name)-in-finder", systemImage: "finder")
}
}
}
}
}
private struct AppIconDisplay_AsPathControl: NSViewRepresentable
{
typealias NSViewType = NSPathControl
let urlToApp: URL
func makeNSView(context _: Context) -> NSPathControl
{
let pathControl: NSPathControl = .init()
pathControl.url = urlToApp
if let lastPathItem = pathControl.pathItems.last
{
pathControl.pathItems = [lastPathItem]
}
return pathControl
}
func updateNSView(_: NSPathControl, context _: Context)
{}
}

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